Files
api2/docs/core-module-design.md
T
2026-07-20 17:49:24 +09:00

281 lines
14 KiB
Markdown

# Core 모듈 설계
## 목표
- core는 admin/front가 공유하는 데이터 접근 계층과 인프라를 제공한다.
- admin/front는 각 서버의 업무 정책, 트랜잭션, 요청과 응답 조립, 인증 주체 확인, 서버별 운영 설정을 담당한다.
- 화면 전용 Form과 VO는 core에 두지 않는다. 단, core 업무 처리의 입력과 결과를 표현하는 내부 DTO/VO는 둔다.
## 패키지 구조
```text
com.alist.api.core
├─ common
├─ config
└─ modules
├─ standard
└─ bespoke
```
## standard
`standard`는 여러 기능에서 재사용할 수 있는 **단일 테이블** 데이터 처리를 둡니다.
- 단일 테이블의 조회, 등록, 수정, 삭제와 필요한 전용 조회 projection
- 한 테이블 중심의 Mapper, Service, 내부 DTO/VO
- admin/front 업무 Service가 호출한다.
- Form, 화면 전용 VO, HTTP 요청/응답 객체에 의존하지 않음
- 등록과 수정의 실제 SQL 처리는 특별한 사유가 없으면 standard가 소유한다.
- 단일 테이블에서 조회 조건이나 조회 필드가 추가되어도 새 Service를 만들지 않는다. 해당 테이블의 기존 standard Service, Mapper, DTO/VO 계약에 메서드 또는 선택 조건을 추가한다.
## bespoke
`bespoke`는 단일 테이블 standard 계약으로 해결되지 않는 복합 데이터 처리를 둡니다. bespoke는 특정 목적에 맞춰 제작한 처리라는 뜻입니다.
- 여러 테이블 JOIN 조회
- standard로 처리할 수 없는 일괄 처리, `INSERT ... SELECT`, 성능 최적화 SQL
- 예외적인 `JOIN UPDATE` 등 복합 DML
bespoke는 전용 Mapper와 그 Mapper를 호출하는 Service를 함께 둡니다. 단일 테이블 처리와 범용 CRUD는 bespoke마다 중복하지 않고 standard로 올립니다. admin/front의 업무 Service가 정책과 트랜잭션을 소유하며, core bespoke는 복합 데이터 접근 책임에 집중합니다.
## 업무 흐름과 호출 기준
```text
Controller -> admin/front 업무 Service
admin/front 업무 Service -> standard Service
admin/front 업무 Service -> bespoke Service
bespoke Service -> bespoke Mapper
```
- core standard와 core bespoke는 서로의 Service 또는 Mapper를 호출하지 않는다.
- standard와 bespoke를 함께 써야 하는 업무 흐름은 반드시 admin/front 업무 Service가 각각 호출해 조합한다.
- 따라서 core는 독립적인 단일 테이블 접근(standard)과 복합 SQL 접근(bespoke)만 제공하고, 두 계층 사이의 호출 순서나 트랜잭션 경계는 갖지 않는다.
```text
관리자 회원가입
UserController -> admin UserService
-> TestUserService
-> TestUserTokenService
회원과 토큰을 함께 조회하는 관리자 화면
MemberController -> admin MemberService
-> bespoke.testUser.TestUserMemberService
-> TestUserMemberMapper.testUserMemberList
```
- standard는 단일 테이블을 기준으로 실제 `select`, `insert`, `update`, `delete`를 처리한다. 단일 테이블의 인증용 조회처럼 필요한 필드 구성이 다른 경우도 standard의 전용 조회 메서드로 둔다.
- admin/front 업무 Service는 어떤 standard/bespoke를 어떤 순서로 호출할지, 어떤 정책을 적용할지, 어디까지 하나의 트랜잭션으로 묶을지를 결정한다.
- Controller는 core standard/bespoke Service 또는 Mapper를 직접 호출하지 않는다.
- 여러 테이블 조인은 보통 bespoke 조회 Mapper에 둔다.
- 여러 테이블을 하나의 SQL로 갱신해야 하는 `JOIN UPDATE`는 bespoke 수정 Mapper의 예외다.
- standard/bespoke의 첫 판단 기준은 SQL이 접근하는 테이블 수다. 단일 테이블이면 standard, 여러 테이블 JOIN·복합 DML이면 bespoke다. 정책과 트랜잭션 경계는 admin/front 업무 Service가 담당한다.
## 업무 모듈 파일 구조
```text
modules/standard/testUser
├─ mapper
│ └─ TestUserMapper.java
├─ service
│ └─ TestUserService.java
├─ dto
└─ vo
resources/mapper/standard/testUser
└─ TestUserMapper.xml
modules/bespoke/notice
├─ mapper
│ └─ NoticeNoticeMapper.java
├─ service
│ └─ NoticeNoticeService.java
├─ dto
└─ vo
resources/mapper/bespoke/notice
└─ NoticeNoticeMapper.xml
```
- Mapper와 Service는 각각 `mapper`, `service` 패키지로 분리한다.
- DTO와 VO는 `dto`, `vo` 패키지로 분리한다.
- Mapper XML namespace는 `mapper` 패키지를 포함한 Mapper 인터페이스 전체 경로와 일치해야 한다.
## 테이블·업무 모듈 기반 명명
core와 실행 모듈의 Service 이름은 역할을 임의로 줄이지 않고 기준 테이블과 업무 모듈을 조합한다.
```text
standard
{Table}Service
bespoke
{BaseTable}{BusinessModule}Service
```
- `{Table}``{BaseTable}`은 기준 테이블의 Java 이름이다. 예: `test_user` -> `TestUser`, `NOTICE` -> `Notice`.
- standard는 해당 테이블의 기본 CRUD이므로 별도 업무 모듈 접미사를 붙이지 않는다. 예: `TestUserService`, `NoticeService`.
- bespoke는 기준 테이블 뒤에 이를 사용하는 업무 모듈명을 붙인다. 예: `TestUserAuthService`, `TestUserTokenAuthService`, `NoticeSupportService`.
- 업무 모듈명은 `admin`, `front` 같은 실행 서버명이 아니라 `auth`, `support`처럼 실제 기능 목적을 사용한다.
- `Query`, `Custom`, `Bespoke`, `Etc`처럼 처리 방식만 표현하는 접미사는 사용하지 않는다.
- bespoke Service에는 이름에 포함된 업무 모듈 목적에 맞는 메서드만 둔다. `TestUserAuthService`에는 `test_user`를 기준으로 하는 여러 테이블 인증 JOIN 조회만 둔다. 단일 `TEST_USER` 조회는 `TestUserService`에 둔다.
- JOIN이 있더라도 SQL이 시작하고 결과를 소유하는 기준 테이블에 맞춰 bespoke 패키지를 정한다. 예: API Key와 refresh token 기준 인증 JOIN 조회는 `bespoke.testUserToken`에 둔다.
### bespoke 메서드 명명
실행 모듈 Service는 API 업무 흐름을 기준으로, core bespoke는 해당 흐름을 위해 기준 테이블에서 반환하는 데이터 형태를 기준으로 이름을 정한다.
```text
admin/front AuthService.selectLogin
-> core TestUserAuthService.testUserLoginView
-> core TestUserTokenService.updateTestUserTokenModify
-> core TestUserService.updateTestUserModify
```
```text
admin/front Service
{select|insert|update|delete}{기능명}
core bespoke Service / Mapper / XML id
{기준테이블명}{기능명}{List|View|Count|Summary|...}
```
- core bespoke 메서드명에서는 `select`, `insert`, `update`, `delete` 접두어를 사용하지 않는다.
- 기준 테이블명은 반드시 앞에 둔다. 예: `testUserLoginView`, `noticeSupportList`.
- 기능명은 이를 호출하는 admin/front Service의 기능 흐름을 따른다. 한 흐름에서 여러 core 조회가 필요하면 각 기준 테이블명을 붙여 여러 메서드를 호출한다.
- 조회 메서드는 `List`, `View`, `Count`, `Summary`, `Tree` 등 반환 형태를 반드시 마지막에 붙인다.
- 조회 조건은 DTO에 둔다. `ByUserIdx`, `ByApiKey`처럼 조건을 메서드명에 붙이지 않는다.
- standard로 처리 가능한 단일 테이블 CRUD와 조회는 bespoke에 만들지 않는다. 예외적인 bespoke CUD도 같은 명명 방식으로 기준 테이블과 기능명을 사용한다. 예: `noticeSupportAdd`, `noticeSupportModify`.
- bespoke Service 메서드, Mapper 인터페이스 메서드, Mapper XML `id`는 정확히 같은 이름을 사용한다.
실행 모듈에서 하나의 업무 패키지가 여러 테이블 기능을 포함할 때도 테이블별 Service를 분리한다.
```text
admin/modules/support
├─ NoticeController
├─ FaqController
└─ service
├─ NoticeSupportService
└─ FaqSupportService
```
`SupportService` 하나에 Notice와 FAQ 로직을 함께 넣지 않는다. 반면 `admin/modules/auth/AuthService`처럼 하나의 인증 흐름을 조합하는 실행 모듈 업무 Service는 기능명만 사용하며, core standard/bespoke Service를 호출해 처리한다.
## 표준 DTO/VO 규칙
standard의 각 업무 모듈은 API 동작 이름을 기준으로 DTO와 VO를 분리한다. 이 객체들은 core 내부에서 Mapper와 Service 사이, 또는 core Service와 실행 모듈 업무 Service 사이에 전달되는 업무 객체이며 화면 전용 응답 객체가 아니다.
```text
modules/standard/notice
├─ NoticeMapper.java
├─ NoticeService.java
├─ dto
│ ├─ NoticeAddDto.java
│ ├─ NoticeViewDto.java
│ ├─ NoticeListDto.java
│ ├─ NoticeModifyDto.java
│ └─ NoticeDeleteDto.java
└─ vo
├─ NoticeAddVo.java
├─ NoticeViewVo.java
├─ NoticeListVo.java
├─ NoticeListItemVo.java
├─ NoticeModifyVo.java
└─ NoticeDeleteVo.java
```
- 기본 동작명은 `add`, `view`, `list`, `modify`, `delete`로 통일한다.
- 목록은 검색/페이징 조건인 `ListDto`, 목록 행인 `ListItemVo`, 목록과 페이징 결과를 담는 `ListVo`를 분리한다.
- 변경 결과 VO는 동작이 드러나는 boolean과 대상 PK를 함께 둔다. 예: `NoticeModifyVo(updated, noticeIdx)`, `NoticeDeleteVo(deleted, noticeIdx)`. 등록 결과에는 `added`와 생성 PK 등 필요한 결과 값을 함께 둔다.
- admin/front Form은 실행 모듈의 업무 Service까지 전달할 수 있다. 업무 Service는 Form 값을 core DTO로 변환하고, core VO는 필요할 때만 실행 모듈의 화면 전용 VO로 조립한다.
## core VO와 실행 모듈 VO
core VO는 Mapper 조회 결과와 core 내부 업무 처리에 필요한 데이터를 표현한다. admin/front VO는 HTTP 응답으로 실제 클라이언트에 전달할 필드만 표현한다.
```text
core Mapper
-> core VO
-> admin/front 업무 Service
-> admin/front 화면/API VO
-> Controller
```
- core VO는 특정 화면에 묶이지 않고 업무·DB 기준으로 필요한 데이터를 담는다.
- admin/front 업무 Service는 core VO에서 각 API에 필요한 필드만 골라 실행 모듈 VO로 조립한다.
- 같은 core VO라도 admin과 front는 서로 다른 외부 VO를 사용할 수 있다.
- 비밀번호, refresh token, 내부 인증값 같은 민감 정보는 일반 core VO에 기본으로 넣지 않는다. 꼭 필요한 인증 처리에는 전용 core VO를 사용하고, admin/front 외부 VO에는 절대 옮기지 않는다.
- Controller는 core DTO/VO를 import하거나 반환 타입으로 사용하지 않는다. core DTO/VO는 admin/front 업무 Service까지만 전달한다.
- Controller가 필요한 값은 반드시 해당 실행 모듈 Service가 조립한 admin/front VO로 받는다. core 조회 결과를 Controller에서 외부 VO로 변환하지 않는다.
```text
AuthController
-> admin AuthService.selectLoginChecked
-> core TestUserTokenAuthService.testUserTokenLoginCheckedView
-> TestUserTokenLoginCheckedViewVo (core 내부)
-> AuthLoginCheckedVo (admin 외부 응답용)
-> ApiResponse<AuthLoginCheckedVo>
```
## 단일 수정과 상태 변경
단일 필드 변경도 별도 `UseModifyDto`, `StatusModifyDto`를 늘리지 않는다. 기본적으로 하나의 `{Domain}ModifyDto`를 사용한다.
```text
NoticeModifyDto
- noticeIdx // 수정 대상
- noticeType // 선택
- targetScope // 선택
- title // 선택
- content // 선택
- pinYn // 선택
- startDate // 선택
- endDate // 선택
- useYn // 선택
- updateMember // 항상 기록
```
- Service는 DTO에 담긴 값을 그대로 Mapper에 전달한다.
- Mapper XML에서는 `<set>`을 사용하지 않는다. 항상 갱신하는 수정일 컬럼을 `SET`의 첫 항목으로 두고, null이 아닌 선택 필드는 `<if>`와 앞쪽 쉼표로 뒤에 붙인다. 따라서 사용 상태만 바꾸는 경우에도 `NoticeModifyDto``noticeIdx`, `useYn`, `updateMember`만 넣어 호출한다.
- `START_DATE`, `END_DATE`처럼 null로 명시적 초기화가 필요한 컬럼은 단순 null 제외 규칙으로는 처리할 수 없다. 초기화 요구가 생기면 전용 초기화 플래그 또는 해당 업무에 맞는 bespoke SQL을 추가한다.
## 변경 결과 VO
수정과 삭제는 다음 기준으로 결과를 반환한다.
```text
NoticeModifyVo
- updated // update 영향 행 수가 1 이상이면 true
- noticeIdx // 수정 요청 대상 PK
NoticeDeleteVo
- deleted // delete(논리 삭제) 영향 행 수가 1 이상이면 true
- noticeIdx // 삭제 요청 대상 PK
```
- `updated`, `deleted`는 SQL 영향 행 수로 계산한다.
- 변경 결과 VO에는 내부 처리용 `resultCode`를 둔다. 성공은 `2002` 또는 `2005`, 수정값 없음은 `4001`, 대상 없음은 `2003`처럼 Service가 반환한다.
- Controller는 `resultCode`에 맞는 `ApiResponseCode`를 선택한다. `resultCode``@JsonIgnore`로 외부 응답 본문에는 직접 노출하지 않는다.
- PK를 함께 반환하면 호출한 쪽이 실제 처리 대상을 명확히 확인할 수 있다.
## common과 config
- `common`: JWT, 공통 예외, ApiResponse, 페이징, 유틸, 시스템 전반 공통 기능
- `config`: DataSource, MyBatis, 캐시, 필터처럼 공통 인프라를 구성하는 클래스
- 파일 업로드처럼 테이블 CRUD에 속하지 않는 공통 기능은 `common` 또는 `config`의 책임에 맞춰 둡니다.
## admin과 front
```text
api-admin / api-front
├─ Api*Application
├─ config
│ ├─ SecurityConfig
│ └─ OpenApiConfig
└─ modules
└─ Controller, 업무 Service, Form, 화면 VO
```
- Security와 OpenAPI는 서버별 정책과 문서를 가지므로 각 실행 모듈에 둡니다.
- Controller는 Form 검증, 업무 Service 호출, 서버별 응답 VO 조립을 담당합니다.
- 업무 Service는 Form을 받아 필요한 core standard/bespoke DTO로 변환하고, 업무 정책과 트랜잭션을 처리합니다.