콘텐츠로 이동

Ch 3. sys0101 깊이 보기

이 챕터를 마치면

  • sys0101 공통코드 관리 화면의 데이터 조회 요청이 8단계를 거쳐 DB까지 가는 경로를 설명할 수 있습니다.
  • 각 단계에서 어떤 파일이 어떤 역할을 하는지 코드를 보고 확인할 수 있습니다.
  • 저장·삭제 흐름이 조회 흐름과 어떻게 다른지 설명할 수 있습니다.

1. 왜 sys0101인가

sys0101 공통코드 관리는 hera-v4의 대표 참조 구현입니다.

  • 트리뷰 + 그리드 레이아웃을 모두 사용합니다.
  • 페이지네이션 조회, 등록, 수정, 삭제, 엑셀 업로드가 모두 구현되어 있습니다.
  • ViewRouter, Thymeleaf, JS, webapp API, Feign client, system API, Service, Mapper 8개 계층이 빠짐없이 등장합니다.

이 챕터에서는 "그리드 목록 조회" 흐름을 따라 8단계를 순서대로 추적합니다.


2. 전체 흐름 한눈에 보기

sequenceDiagram
    participant Browser
    participant VR as 1. Sys0101ViewRouter
    participant HTML as 2. sys0101.html
    participant JS as 3. sys0101.js
    participant WA as 4. Sys0101ApiController (webapp)
    participant FC as 5. CommonCodeClient (Feign)
    participant GW as 6. API Gateway
    participant SA as 7. CommonCodeApiController (system)
    participant SV as 8. CommonCodeServiceImpl
    participant MP as 9. CommonCodeMapper

    Browser->>VR: GET /sys/0101
    VR-->>Browser: sys0101.html + sys0101.js

    Browser->>WA: GET /api/sys/common-codes/pageable
    WA->>FC: commonCodeClient.findAllOnPageable(pageRequest)
    FC->>GW: GET /api/v1/sys/common-codes/pageable (with @RequestBody)
    GW->>SA: route to bootstrap-system
    SA->>SV: super.doFindAllOnPageable(pageRequest)
    SV->>MP: QueryWrapper 조건으로 페이지 조회
    MP-->>SV: Page
    SV-->>SA: PageResponse
    SA-->>FC: PageResponse
    FC-->>WA: PageResponse
    WA-->>Browser: JSON 응답 (그리드 데이터)

3. 단계별 추적

단계 1 — ViewRouter: 화면 요청 수신

파일: hera-webapp/…/view/sys/Sys0101ViewRouter.java

브라우저에서 /sys/0101에 접근하면 Sys0101ViewRouter가 요청을 받습니다.

@Slf4j
@Controller
public class Sys0101ViewRouter extends BaseViewRouter {

    @GetMapping("/sys/0101")
    public String view(ModelMap model) throws Exception {
        prepareBaseInfo(model, SYS_0101);  // (1)
        return "sys/sys0101";              // (2)
    }
}
  • (1) prepareBaseInfo 호출 — BaseViewRouter가 세션 사용자, 다국어 메시지, 사이드 메뉴를 모델에 채웁니다.
  • (2) 뷰 이름 "sys/sys0101" 반환 — Thymeleaf가 templates/sys/sys0101.html을 찾아 렌더링합니다.

ViewRouter는 데이터를 직접 조회하지 않습니다. HTML 뼈대와 공통 모델만 준비합니다.


단계 2 — Thymeleaf: HTML 렌더링

파일: hera-webapp/src/main/resources/templates/sys/sys0101.html

Thymeleaf는 ViewRouter가 채운 모델로 HTML을 완성합니다.

<!-- 그리드 컨테이너 -->
<div id="gridContent">
    <div id="codeGrid"></div>
</div>

<!-- 검색바 (서버 사이드 렌더링 조각) -->
<div id="searchBar" th:replace="~{sys/sys0101-s01 :: searchBar}"></div>

Thymeleaf 렌더링이 끝나면 완성된 HTML과 sys0101.js 링크가 브라우저로 전달됩니다. 이후 데이터 조회는 브라우저의 JavaScript가 담당합니다.


단계 3 — JavaScript: Ajax 조회 요청

파일: hera-webapp/src/main/resources/static/js/sys/sys0101.js

페이지 로드 직후 findAllCode 함수가 호출되어 그리드 초기 데이터를 가져옵니다.

const apiUrl = '/api/sys';

const findAllCode = params => {
    $.get(apiUrl + '/common-codes/pageable', params, res => {})  // (1)
    .done(res => {
        Grid.refresh($codeGrid, res);                            // (2)
    })
    .fail(res => {
        App.alert('error', '공통코드 - 조회', App.parseAjaxError(res));
    });
}
  • (1) GET /api/sys/common-codes/pageable 호출 — 검색 조건과 페이지 정보를 쿼리 파라미터로 보냅니다.
  • (2) 응답 res를 받아 Grid.refresh로 그리드를 갱신합니다.

Grid.refresh

Grid.refresh는 hera-v4 공통 JS 함수입니다. PageResponse<VO> 형태의 JSON을 받아 pq-grid 컴포넌트를 자동으로 갱신합니다.


단계 4 — Webapp ApiController: 요청 수신 및 위임

파일: hera-webapp/…/api/sys/Sys0101ApiController.java

JS가 보낸 요청을 webapp의 Sys0101ApiController가 받습니다.

@RestController
@RequestMapping(SYS_API)              // "/api/sys"
public class Sys0101ApiController extends WebApiRouter {

    @Resource
    private CommonCodeClient commonCodeClient;

    @PreAuthorize("hasRole('ROLE_SYS0101_READ')")        // (1)
    @GetMapping("/common-codes/pageable")
    public PageResponse<CommonCodeVO> findAllOnPageable(
            PageRequest<CommonCodeVO, CommonCodeSP> pageRequest,
            CommonCodeSP commonCodeSP) {
        pageRequest.setSearchParams(commonCodeSP);        // (2)
        return commonCodeClient.findAllOnPageable(pageRequest);  // (3)
    }
}
  • (1) ROLE_SYS0101_READ 권한 없이 접근하면 403을 반환합니다.
  • (2) 화면에서 넘어온 검색 조건(commonCodeSP)을 pageRequest에 설정합니다.
  • (3) Feign client로 도메인 서비스에 위임합니다. webapp은 DB를 직접 건드리지 않습니다.

단계 5 — Feign Client: 도메인 서비스 호출

파일: hera-api-client/…/sys/CommonCodeClient.java

CommonCodeClient는 Feign 인터페이스입니다. 실제 HTTP 호출을 수행합니다.

@FeignClient(
    path = SYS_API_V1,           // "/api/v1/sys"
    name = "msa-api-gateway",
    url = "${api-gateway.sys:}",  // (1)
    contextId = "commonCodeClient",
    configuration = FeignClientConfig.class
)
public interface CommonCodeClient extends BaseClient<CommonCodeVO> {

    @GetMapping(value = "/common-codes/pageable",
                produces = MediaType.APPLICATION_JSON_VALUE)
    PageResponse<CommonCodeVO> findAllOnPageable(
        @RequestBody PageRequest<CommonCodeVO, CommonCodeSP> pageRequest  // (2)
    );
}
  • (1) api-gateway.sys 프로퍼티 값을 URL로 사용합니다. 로컬 환경에서는 사내 API Gateway 주소가 설정됩니다.
  • (2) GET 요청에 @RequestBody를 사용합니다. 복잡한 페이지·검색 조건을 쿼리스트링 대신 요청 body로 실어 보내려는 hera-v4의 의도적 패턴입니다.

Feign이 이 인터페이스를 구현 클래스로 자동 생성하고, 실제 HTTP 요청을 API Gateway로 보냅니다.


단계 6 — API Gateway → System ApiController

API Gateway/api/v1/sys/** 경로를 bootstrap-system 서비스로 라우팅합니다.

파일: hera-system/…/sys/CommonCodeApiController.java

@RestController
@RequestMapping(SYS_API_V1)   // "/api/v1/sys"
public class CommonCodeApiController
        extends AuditApiController<CommonCode, CommonCodeVO, CommonCodeSP, CommonCodeService> {

    public CommonCodeApiController(CommonCodeService service) {
        super(service, CommonCode.class);
    }

    @GetMapping("/common-codes/pageable")
    public PageResponse<CommonCodeVO> findAllOnPageable(
            @RequestBody PageRequest<CommonCodeVO, CommonCodeSP> pageRequest) {
        return super.doFindAllOnPageable(pageRequest);  // (1)
    }
}
  • (1) super.doFindAllOnPageable은 부모 클래스(AuditApiController)가 제공하는 공통 메서드입니다. 페이지네이션 처리와 Service 호출을 내부에서 처리하고 PageResponse<VO>를 반환합니다.

Controller는 요청을 Service에 위임할 뿐, 직접 조건을 조립하거나 DB를 건드리지 않습니다.


단계 7 — Service: 검색 조건 조립

파일: hera-system/…/sys/code/common/CommonCodeServiceImpl.java

Service의 핵심 역할은 searchCondition에서 SP를 QueryWrapper로 변환하는 데 있습니다.

@Service @Primary
public class CommonCodeServiceImpl
        extends AuditServiceImpl<CommonCode, CommonCodeVO, CommonCodeSP, String>
        implements CommonCodeService {

    @Override
    protected QueryWrapper searchCondition(CommonCodeSP sp) {
        return QueryWrapper.create()
            .like(CommonCode::getCategoryCode, sp.getCategoryCode(), If::hasText)  // (1)
            .like(CommonCode::getCategoryName, sp.getCategoryName(), If::hasText)
            .like(CommonCode::getGroupCode,    sp.getGroupCode(),    If::hasText)
            .like(CommonCode::getGroupName,    sp.getGroupName(),    If::hasText)
            .like(CommonCode::getCode,         sp.getCode(),         If::hasText)
            .like(CommonCode::getName,         sp.getName(),         If::hasText)
            .eq(CommonCode::getUseYn,          sp.getUseYn(),        If::hasText);  // (2)
    }
}
  • (1) If::hasText 가드 — SP 필드 값이 비어 있으면 해당 조건을 자동으로 제외합니다. 화면에서 검색 조건을 입력하지 않아도 WHERE 절이 넘치지 않는 이유입니다.
  • (2) useYnBaseSP@Builder.Default로 기본값 "Y"가 설정되어 있습니다. 별도 입력 없이도 항상 사용 중인 코드만 조회합니다.

부모 클래스 AuditServiceImplsearchCondition이 반환한 QueryWrapper를 받아 MyBatis-Flex에 넘기고 페이지네이션 결과를 PageResponse<VO>로 래핑합니다.


단계 8 — Mapper: DB 조회

파일: hera-system/…/sys/code/common/CommonCodeMapper.java

@Mapper
public interface CommonCodeMapper extends MinuBaseMapper<CommonCode> {
    // 단순 페이지 조회는 MinuBaseMapper가 제공하는 메서드로 처리됩니다.
    // 커스텀 조회가 필요한 경우에만 여기에 메서드를 추가합니다.
}

CommonCodeMapperMinuBaseMapper<CommonCode>를 상속합니다. 단순 CRUD와 페이지네이션은 MyBatis-Flex가 QueryWrapper를 기반으로 SQL을 자동 생성합니다.

조인 조회나 복잡한 SQL이 필요하면 Mapper XML(common-code-mapper.xml)에 statement를 추가합니다.

common-code-mapper.xml 위치:
hera-system/src/main/resources/mybatis/sys/code/common/common-code-mapper.xml

4. 조회 흐름 요약

단계 파일 핵심 코드
1. ViewRouter Sys0101ViewRouter.java prepareBaseInfo(model, SYS_0101)return "sys/sys0101"
2. Thymeleaf sys0101.html 세션·메시지·메뉴 모델로 HTML 완성
3. JavaScript sys0101.js $.get('/api/sys/common-codes/pageable', params)
4. Webapp API Sys0101ApiController.java @PreAuthorize 검사 → commonCodeClient.findAllOnPageable(pageRequest)
5. Feign Client CommonCodeClient.java GET /api/v1/sys/common-codes/pageable + @RequestBody
6. System API CommonCodeApiController.java super.doFindAllOnPageable(pageRequest)
7. Service CommonCodeServiceImpl.java searchCondition(SP)QueryWrapper 조립
8. Mapper CommonCodeMapper.java MinuBaseMapper 페이지 조회 → PageResponse<VO> 반환

5. 저장 흐름

저장은 조회와 경로가 같지만 HTTP 메서드와 처리 클래스가 다릅니다.

sequenceDiagram
    participant JS as sys0101.js
    participant WA as Sys0101ApiController
    participant FC as CommonCodeClient (Feign)
    participant SA as CommonCodeApiController (system)
    participant SV as CommonCodeServiceImpl

    JS->>WA: POST /api/sys/common-code/code (CommonCodeVO)
    WA->>FC: commonCodeClient.save(codeVO)
    FC->>SA: POST /api/v1/sys/common-code/code (with @RequestBody)
    SA->>SV: super.doSave(codeVO)
    SV-->>SA: ResponseEntity (200 OK)
    SA-->>FC: ResponseEntity
    FC-->>WA: ResponseEntity
    WA-->>JS: 성공 응답

system ApiController에서 super.doSave(codeVO)를 호출하면, 부모 클래스가 @Transactional 안에서 저장 처리 후 ok() 또는 error()를 반환합니다.


6. 삭제 흐름

// sys0101.js
$.delete(apiUrl + '/common-code/category', JSON.stringify(params), res => {
    // 삭제 완료 후 트리뷰 갱신
});
// Sys0101ApiController.java
@DeleteMapping("/common-codes")
public ResponseEntity deleteCode(@RequestBody List<CommonCodeVO> voList) {
    return commonCodeClient.delete(voList);
}

삭제도 @RequestBody로 VO 목록을 전달합니다. system API Controller에서 super.doDelete(voList)를 호출해 트랜잭션 안에서 한 번에 삭제합니다.


7. 권한 모델

sys0101의 모든 API는 역할(Role) 기반으로 보호합니다.

Role 허용 동작
ROLE_SYS0101_READ 조회 (@GetMapping)
ROLE_SYS0101_CREATE 등록 (@PostMapping)
ROLE_SYS0101_UPDATE 수정 (@PutMapping)
ROLE_SYS0101_DELETE 삭제 (@DeleteMapping)

Role 문자열은 도메인코드_동작 형식입니다. SYS0101은 Ch 1에서 살펴본 도메인 코드입니다. 권한 없는 사용자가 API를 직접 호출하면 403 응답을 받습니다.


8. 이 챕터 요약

개념 핵심
8단계 ViewRouter → HTML → JS → webapp API → Feign → Gateway → system API → Service → Mapper
DB 분리 webapp은 DB에 직접 접근하지 않습니다. Feign → Gateway → system 경로를 거칩니다
GET + @RequestBody 복잡한 검색 조건과 페이지 정보를 body로 전달하는 hera-v4 고유 패턴
QueryWrapper Service의 searchCondition(SP)에서 조립. If::hasText로 빈 조건을 자동 제거
doXxx 공통 메서드 system ApiController는 super.doFindAllOnPageable, super.doSave 등을 호출합니다

다음 챕터 예고

Ch 4에서는 sys0101의 구조를 이해한 상태에서, 더 단순한 단일 그리드 메뉴 PT0101을 코드 생성기 없이 직접 손으로 작성합니다. ViewRouter, HTML, JS, webapp API, Feign client, system API, Service, Mapper, VO, SP를 처음부터 연결하는 과정을 따라갑니다.