Files
api2/docs/code-conventions.md
T
2026-07-20 17:49:24 +09:00

126 lines
20 KiB
Markdown

# 코드 컨벤션
## 파일 생성 규칙
- core 업무 모듈은 `modules.standard``modules.bespoke` 기준으로 나눈다. standard는 단일 테이블의 모든 조회·등록·수정·삭제와 필요한 전용 조회를 담당하고, bespoke는 여러 테이블 JOIN·집계·복합 SQL을 위한 전용 데이터 처리다.
- 업무 모듈 패키지명은 단어를 이어 쓰는 lower camel case로 작성한다. 예: `testUser`, `testUserToken`, `testCorsAllowedList`.
- admin/front의 새 API는 `modules/{업무 메뉴 1 depth}` 단위로 Controller, 업무 Service, Form, 화면 VO를 둔다. 이 1 depth는 테이블명이 아니라 서비스 메뉴와 업무 경계 기준이다. 예: 백오피스의 관리자 계정, 권한, 메뉴 관리는 모두 `modules.backOffice`에 둔다.
- 하나의 업무 메뉴 모듈에는 여러 Controller와 업무 Service가 들어갈 수 있다. 예: `backOffice`에는 `AdminMemberController`/`AdminMemberService`, `AccountController`/`AccountService`, `MenuController`/`MenuService`를 함께 둔다.
- admin/front의 패키지명은 lower camel case로 작성한다. 예: `modules.backOffice`. 업무 모듈이 이미 기능 경계이므로 Form과 외부 VO는 기본적으로 해당 모듈의 `form`, `vo` 바로 아래에 둔다. 예: `modules.backOffice.form.AdminMemberAddForm`, `modules.backOffice.vo.AdminMemberListVo`. 클래스명은 `AdminMemberController`, `AdminMemberService`처럼 UpperCamelCase를 사용한다.
- core에는 Controller, Form, 화면 전용 VO를 두지 않는다. 다만 core Service/Mapper의 업무 입력과 결과를 위한 내부 DTO/VO는 둔다.
- core standard Service는 `{Table}Service`로 작성한다. 예: `TestUserService`, `NoticeService`.
- core bespoke Service는 `{BaseTable}{AdminFrontServiceName}Service`로 작성한다. 예: `TestUserAuthService`, `TestUserAdminMemberService`, `NoticeNoticeService`.
- `{AdminFrontServiceName}`은 admin/front 업무 Service의 이름에서 마지막 `Service`를 뺀 값이다. 기준 테이블명과 같은 이름이 반복되어도 생략하거나 상위 패키지명으로 치환하지 않는다. 예: `modules.support.NoticeService`가 호출하는 core bespoke는 `NoticeSupportService`가 아니라 `NoticeNoticeService`로 작성한다.
- core bespoke 명명에 패키지명이나 1 depth 모듈명은 사용하지 않는다. 패키지 위치는 구조 변경으로 달라질 수 있으므로, 변경에 강한 기준 테이블명과 admin/front 업무 Service명을 조합한다.
- 따라서 `backOffice`는 admin 실행 모듈의 패키지 경계일 뿐 core bespoke 명명에 사용하지 않는다. `backOffice.AdminMemberService`의 core bespoke는 `TestUserBackOfficeService`가 아니라 `TestUserAdminMemberService`로 작성한다.
- `admin`, `front` 같은 실행 서버명과 `Query`, `Custom`, `Bespoke` 같은 구현 방식명은 core bespoke 이름에 사용하지 않는다.
- 하나의 실행 모듈에 여러 테이블 기능이 있으면 Service도 테이블별로 나눈다. 예: `support` 모듈의 `NoticeSupportService`, `FaqSupportService`. `SupportService` 하나에 서로 다른 테이블 업무를 모으지 않는다.
- core bespoke Service/Mapper 메서드는 `{BaseTable}{Function}{List|View|Count|Summary|...}`로 작성한다. 예: `testUserLoginView`, `testUserTokenRefreshView`, `noticeSupportList`.
- bespoke 메서드의 `{Function}`은 호출하는 admin/front Service의 기능명을 따르며, core는 앞에 기준 테이블명을 붙인다. 예: `AuthService.selectLogin``testUserLoginView`, `testUserTokenLoginView`를 호출할 수 있다.
- bespoke 메서드에는 `select`, `insert`, `update`, `delete` 접두어와 `ByUserIdx` 같은 조회 조건을 넣지 않는다. 조건은 DTO에 둔다.
- 요청 검증이나 JSON 바인딩이 필요하면 `form` 패키지를 함께 만든다.
- 클래스명은 역할이 바로 드러나게 `도메인명 + 역할` 형식을 유지한다. 예: `ApiService`, `ApiMapper`, `ApiVo`
- Mapper 인터페이스를 추가하면 같은 이름의 XML을 `src/main/resources/mapper/{도메인경로}` 아래 함께 만든다.
- 단순 예시 코드와 운영 코드는 섞지 말고 패키지로 분리한다.
- 설정성 클래스는 `config` 하위 역할별 패키지에 둔다. 예: `config.datasource`, `config.cache`.
- 단순 환경값(String, 숫자, boolean 등)은 사용하는 클래스의 필드에 `@Value`로 주입한다. 예: `@Value("${file.storage.root-path}") private String rootPath;`.
- 목록, Map, 중첩 객체처럼 구조화된 설정은 `@ConfigurationProperties`로 묶는다. 단순 값 몇 개만을 위해 별도 Properties 클래스를 만들지 않는다.
## DTO/VO/Form 규칙
- core DTO는 Mapper와 core Service 사이의 내부 데이터 전달 용도다. admin/front Form과 화면 전용 VO를 재사용하지 않는다.
- `Dto`는 core Service와 Mapper 사이의 파라미터 성격 값 객체로 정의한다.
- `Dto`는 요청 처리에 필요한 저장, 수정, 로그 적재 같은 작업 파라미터를 담는 용도로 우선 사용한다.
- `Vo`는 Mapper 에서 Service, Controller 로 반환하는 값 객체로 정의한다.
- `Vo`는 Mapper 조회 결과를 담을 수 있고, Service 에서 비즈니스 로직 처리 후 필요한 데이터를 가공해서 Controller 로 반환하는 용도로 사용한다.
- core VO는 Mapper 조회 결과와 core 내부 업무 처리를 위한 값 객체다. 화면/API 응답 전용 VO가 아니다.
- admin/front VO는 외부 HTTP 응답 전용이며, 실행 모듈 업무 Service가 core VO에서 필요한 필드만 골라 조립한다.
- 동일한 core VO를 사용하더라도 admin/front는 목적에 따라 서로 다른 외부 VO를 사용한다.
- Controller는 core DTO/VO를 import하지 않고, core DTO/VO를 직접 반환하거나 외부 VO로 변환하지 않는다. 변환은 admin/front 업무 Service에서 끝낸다.
- core VO를 admin/front 외부 VO로 바꾸는 단순 필드 복사는 별도 `Converter`, `Assembler`, 변환 전용 private 메서드로 분리하지 않는다. 해당 업무 Service 메서드 안에서 외부 VO를 생성하고 필요한 필드를 직접 설정한다.
- 목록 변환은 업무 Service 메서드 안에서 `for`문으로 목록 행 VO를 생성해 결과 목록에 추가한다. 변환 규칙이 여러 Service에서 반복되거나 독립된 정책을 가지는 시점에만 별도 변환 클래스를 검토한다.
- Controller의 Service 메서드 반환 타입은 원칙적으로 해당 실행 모듈의 VO 또는 단순 처리 결과여야 한다. 예: `AuthService.loginChecked(...)``AdminAuthUserVo`가 아닌 `AuthLoginCheckedVo`를 반환한다.
- 비밀번호, refresh token, 내부 인증값은 일반 core VO와 admin/front 외부 VO에 기본으로 포함하지 않는다. 인증 처리에서만 필요한 값은 별도 내부 VO로 제한한다.
- `Form`은 실행 모듈의 요청 바인딩과 입력 검증에 사용하고, `@Valid` 와 Jakarta Validation 어노테이션을 우선 사용한다. 실행 모듈의 업무 Service가 Form을 받아 core DTO로 변환할 수 있지만, core는 Form에 의존하지 않는다.
- Form은 실행 모듈의 Controller 메서드명을 기준으로, core DTO/VO는 업무 모듈과 동작명을 기준으로 이름을 맞춘다. 예: `NoticeAddForm` -> `NoticeAddDto`, `NoticeListDto` -> `NoticeListVo`.
- standard 업무 모듈의 기본 DTO/VO 동작명은 `Add`, `View`, `List`, `Modify`, `Delete`다. 목록 행은 `{Domain}ListItemVo`를 사용한다.
- 단일 상태 변경도 기본적으로 별도 DTO를 만들지 않고 `{Domain}ModifyDto`의 선택 필드로 처리한다. Mapper XML은 `<set>` 없이 작성하고, 항상 갱신하는 수정일 컬럼 뒤에 `<if>`로 값이 전달된 필드만 추가 수정한다.
- 신규 API 에서는 path 동작명과 다른 표현을 지양한다. 예: 목록은 `Search` 보다 `List`, 상세는 `Detail` 보다 `View`, 수정은 `Update` 보다 `Modify` 를 우선 사용한다.
- 현재 코드처럼 DTO/VO는 Lombok `@Getter`, 필요한 경우에만 `@Setter`를 사용한다.
- Form 안에는 단순한 값 정리 메서드를 둘 수 있다. core DTO 변환은 기본적으로 실행 모듈 업무 Service가 담당한다.
- Form 에서 전달되는 문자열은 업무 Service에서 core DTO로 변환할 때 `trim()`과 기본값 치환을 적용한다.
- DTO 안에 연관된 다른 DTO 변환이 꼭 필요할 때만 최소한의 보조 메서드를 둔다.
- 외부 응답에 노출되면 안 되는 내부 필드는 응답에 사용될 수 있는 객체에서 `@JsonIgnore`로 숨긴다.
- 변경 결과 VO의 boolean 필드는 동작이 드러나도록 작성한다. 예: 등록은 `added`, 수정/상태 변경은 `updated`, 논리 삭제는 `deleted`.
- `ModifyVo`, `DeleteVo` 등 변경 결과 VO에는 처리 대상 PK도 함께 둔다. 예: `NoticeModifyVo``updated`, `noticeIdx`; `NoticeDeleteVo``deleted`, `noticeIdx`.
- 변경 결과 VO에는 내부 처리용 `resultCode`를 두고 Service가 성공/실패 코드를 설정한다. `resultCode``@JsonIgnore`로 숨기고, Controller가 이를 기준으로 `ApiResponseCode`를 선택한다.
- Controller 요청/응답 타입은 가능한 한 Form/Dto/Vo 로 명시한다. `Map<String, Object>` 는 응답 구조가 고정되지 않는 임시 디버깅, 외부 라이브러리 passthrough 같은 예외 상황에서만 제한적으로 사용한다.
- 응답 필드가 단순 boolean 몇 개뿐이어도 운영 API 에서는 VO 를 만든다. 예: 로그인 상태 확인 응답은 `Map<String, Object>` 보다 `AdminAuthLoginCheckedVo` 를 사용한다.
- 목록 조회 API 는 특별한 사유가 없으면 기본적으로 페이징을 적용한다. core 내부 페이징 기준은 `PagingRequest`, `PagingResponse`를 사용한다.
## Swagger/OpenAPI 문서화 규칙
- 신규 또는 수정되는 Controller, Form, VO에는 Swagger 테스트 편의성을 위한 설명을 반드시 남긴다.
- Controller 클래스에는 `@Tag`를 사용해 API 그룹명과 설명을 작성한다.
- `@Tag`의 번호는 Controller 생성 순서가 아니라 admin/front의 업무 메뉴 1 depth 기준 알파벳 그룹으로 관리한다. 예: `backOffice``B` 그룹이며 그 안의 Controller는 `B-1. 관리자 계정 관리`, `B-2. SSO 클라이언트 관리`, `B-3. 스케줄 관리`처럼 작성한다. 같은 1 depth 안에서는 기능 추가 순서에 따라 하위 번호를 증가시킨다.
- Controller 메서드에는 `@Operation`으로 `summary``description`을 작성한다.
- `@RequestBody` Form 클래스에는 클래스 레벨 `@Schema(description = "...")`를 작성한다.
- `@ModelAttribute` 검색 Form 필드에도 `@Schema`를 작성해 Swagger query parameter 설명과 예시가 보이게 한다.
- Form 필드에는 `@Schema``description`, `example`을 작성한다.
- 필수 입력값은 Jakarta Validation 어노테이션과 `@Schema(requiredMode = Schema.RequiredMode.REQUIRED)`를 함께 사용한다.
- 코드값 필드는 허용 값을 `description`에 명시한다. 예: `userType: A 관리자, T 교사, S 학생`, `withdrawStatus: N 정상, P 탈퇴대기, Y 탈퇴완료`.
- VO 클래스에는 클래스 레벨 `@Schema(description = "...")`를 작성한다.
- VO 필드에는 `@Schema`로 응답 값의 의미를 작성한다.
- 토큰, 임시 비밀번호처럼 Swagger 테스트에 필요한 응답 필드는 VO에 명시하되 `description`에 용도를 적는다.
- 비밀번호, DB 조회용 내부 식별자, `resultCode`처럼 외부 응답에 노출하지 않는 필드는 `@JsonIgnore`를 사용하고 필요하면 `@Schema(hidden = true)`도 함께 사용한다.
- 클라이언트가 직접 전달하지 않는 내부 계산 필드나 서버 전용 getter는 `@Schema(hidden = true)`로 숨긴다.
- 페이징 요청에서 클라이언트는 `page`, `size`만 전달하고, `limit`, `offset`은 Swagger에 노출하지 않는다.
- 페이징 응답의 `rowStartNum`처럼 계산 방식이 필요한 값은 프론트 사용 방법까지 `description`에 남긴다.
## Mapper/MyBatis 규칙
- standard Mapper에는 단일 테이블에서 재사용할 수 있는 표준 CRUD와 전용 조회를 둔다. 조회 조건 또는 조회 projection이 추가되어도 해당 테이블의 기존 standard Service/Mapper에 메서드를 추가한다.
- bespoke Mapper에는 업무 전용 조인 조회를 우선 둔다. 여러 테이블을 함께 갱신해야 하는 `JOIN UPDATE`, 일괄 처리, `INSERT ... SELECT`, 성능 최적화 SQL은 예외적으로 둘 수 있다. bespoke Mapper는 core bespoke Service를 통해 호출한다.
- standard Service/Mapper 메서드는 `{select|insert|update|delete}{Table}{Action}`으로 작성한다. 예: `selectTestUserList`, `insertTestUserAdd`, `updateTestUserModify`.
- bespoke Service/Mapper 메서드는 앞 동작 접두어 없이 `{BaseTable}{Function}{ResultShape}`로 작성한다. 예: `testUserLoginView`, `noticeSupportList`. XML `id`도 동일하게 맞춘다.
- 삭제 API도 기본적으로 물리 삭제가 아니라 상태 변경으로 처리하므로 standard는 `update...Delete` 형태를 우선 사용한다.
- 물리 삭제가 정책상 명확히 필요한 예외 상황에서만 `delete` SQL과 `delete...` 메서드명을 사용한다.
- SQL 을 여러 줄로 작성할 때 콤마는 다음 줄 앞에 둔다. 예: `SELECT COL1`, 다음 줄 `, COL2`.
- Mapper XML `namespace`는 인터페이스의 전체 경로와 정확히 일치시킨다.
- XML의 `id`는 Mapper 메서드명과 동일하게 맞춘다.
- 조회 결과 타입은 `resultType`, 저장/수정 파라미터는 DTO 필드명과 매핑되는 프로퍼티명을 그대로 사용한다.
- Mapper XML 안 SQL 블록 시작부 주석은 현재 코드처럼 `/*Mapper.method*/` 형식을 유지한다.
- Mapper XML 위치는 `src/main/resources/mapper/**/*.xml` 이다.
- `map-underscore-to-camel-case: true` 설정으로 DB 컬럼 `user_idx` 는 Java 필드 `userIdx` 로 자동 매핑된다.
- `useGeneratedKeys`, `keyProperty` 를 사용하는 insert 가 있으므로 신규 PK 생성 테이블은 현재 패턴을 먼저 확인한다.
- 상태 집계나 이력성 데이터는 단건 update 외에 이벤트 로그 insert 가 같이 필요한지 확인한다.
- 동적 수정 SQL에서 `<set>`은 사용하지 않는다. `SET UPDATE_AT = NOW()`처럼 항상 갱신하는 컬럼을 먼저 두고, 선택 필드는 `, COLUMN = #{value}` 형태의 `<if>`로 이어 붙인다. null은 기본적으로 "수정하지 않음"을 뜻한다. null로 컬럼 값을 비워야 하는 요구는 별도 플래그 또는 업무 전용 SQL로 명확히 구분한다.
## Java/Spring 코드 스타일
- Java 코드는 현재 프로젝트 스타일에 맞춰 탭/들여쓰기와 import 정렬을 유지한다.
- 선언문, 메서드 반환 대입, 단순 setter, 삼항식이 한 번에 읽히면 줄 길이만으로 줄바꿈하지 않고 한 줄로 작성한다. 여러 줄 정렬은 한 줄에 담기 어려운 인자, 조건, SQL처럼 가독성이 실제로 좋아지는 경우에만 사용한다.
- Mapper 인터페이스의 메서드 선언은 한 줄로 작성하고, 메서드 선언 사이에는 한 줄을 띄운다.
- `if`, `for`, `while` 조건문은 한 줄이어도 항상 중괄호 블록을 사용한다. Service 호출 결과를 지역 변수에 담은 뒤 후속 조건문을 시작하기 전에는 한 줄을 띄워 흐름을 구분한다.
- 코드는 초급 개발자가 읽어도 흐름을 따라갈 수 있도록 직관적으로 작성한다. 과한 축약, 기교적인 표현, 불필요하게 복잡한 체이닝보다 명확한 변수명과 단계적인 흐름을 우선한다.
- Java 코드에서 메서드 호출, 어노테이션 인자, 생성자 인자 등을 여러 줄로 작성할 때 콤마는 다음 줄 앞에 둔다. 예: `summary = "회원 목록 조회"` 다음 줄 `, description = "..."`
- 기능 수정/추가 시 가능하면 Red-Green-Refactor 흐름을 따른다. 먼저 실패하는 테스트나 재현 가능한 검증 조건을 만들고(Red), 최소 구현으로 통과시키며(Green), 이후 컨벤션과 가독성에 맞게 정리한다(Refactor). 테스트 작성이 어려운 경우에는 최소 실행 검증 절차를 먼저 정하고 결과를 보고한다.
- Lombok은 반복 보일러플레이트 제거에만 절제해서 사용한다. `@RequiredArgsConstructor`는 사용하지 않고, 의존성 주입은 명시적인 생성자로 작성한다.
- 로그는 `Slf4j`를 사용하고 반복문 내부 대량 출력은 지양한다.
- 새로운 기능은 가능하면 역할이 드러나는 패키지로 분리한다.
- Service, Config 등 다른 Spring Bean 의존성은 생성자 주입을 기본으로 하고, `@RequiredArgsConstructor` 대신 명시적인 생성자를 사용한다. 단, 단순 환경값은 위 설정 규칙에 따라 `@Value` 필드 주입을 사용한다.
- Service 에서 DB 상태를 바꾸는 메서드는 필요한 범위에서 `@Transactional`을 사용하고, 조회 전용은 `readOnly = true`를 우선 검토한다.
- 문자열 입력값은 현재 코드처럼 필요한 지점에서 `trim()` 처리하고, null 가능성 여부를 먼저 확인한다.
- 지역 변수명은 `result`, `list`, `vo`, `dto` 처럼 범용적인 이름보다 도메인과 객체 종류가 드러나게 작성한다. 예: `List<AdminMemberVo> adminMemberList`, `AdminMemberListVo adminMemberListVo`, `AdminMemberListDto adminMemberListDto`.
- Service 메서드의 Form, DTO, VO 파라미터와 지역 변수도 범용명 `form`, `dto`, `vo`를 사용하지 않는다. 예: `AdminMemberListForm adminMemberListForm`, `SsoListForm ssoListForm`, `TestUserAdminMemberListDto testUserAdminMemberListDto`.
- 같은 타입 또는 같은 의미의 변수가 여러 개 필요해도 객체 종류를 유지하고, 구분이 필요한 경우 숫자 suffix 를 제한적으로 사용한다. 예: `AdminMemberVo adminMemberVo1`, `AdminMemberVo adminMemberVo2`.
## Controller/Service 규칙
- Controller 는 요청/응답 조립과 인증 주체 확인에 집중하고, core Service 또는 Mapper를 직접 호출하지 않는다. core DTO/VO도 Controller에 노출하지 않는다.
- admin/front 업무 Service가 Form을 받아 업무 정책, standard/bespoke 호출 순서, 트랜잭션 경계를 담당한다.
- admin/front 업무 Service 메서드는 `{select|insert|update|delete}{Controller 기능명}`으로 작성한다. 예: Controller의 `ssoList()``selectSsoList()`, `ssoAdd()``insertSsoAdd()`, `ssoDelete()``updateSsoDelete()`를 호출한다. 논리 삭제는 실제 DB 작업이 수정이므로 `update`를 사용한다.
- admin/front 업무 Service의 public 메서드는 Controller가 호출한 API 한 건의 흐름을 가능한 한 해당 메서드 안에서 완결한다. Form 값 정리, core Service 호출, 결과 VO 조립을 별도 모듈 보조 메서드로 잘게 나누지 않는다.
- `to...`, `create...`, `select...` 같은 private 보조 메서드는 단순 코드 분리를 위해 먼저 만들지 않는다. 같은 로직이 여러 업무 Service에 반복되거나, 독립적인 정책과 복잡도를 가질 때 리팩터링 대상으로 분리한다.
- core bespoke Service는 bespoke Mapper를 통한 JOIN·집계·복합 SQL 데이터 접근을 담당한다. 단일 테이블의 조회·등록·수정·삭제는 core standard Service와 Mapper에 둔다.
- Controller 응답은 `ResponseEntity<ApiResponse<T>>` 를 기본으로 사용한다. 파일 다운로드처럼 바이너리 응답이 필요한 경우만 예외로 둔다.
- Controller 에서는 `@RequestBody`, `@PathVariable`, `@RequestHeader`, `@CookieValue` 를 명시적으로 선언해 요청 출처를 드러낸다.
- 인증 사용자 확인은 모듈 구현에 따라 `HttpSession` 또는 `SecurityContextHolder` 를 사용하므로 기존 방식을 먼저 맞춘다.
- 목록 조회는 Controller 에서 `@ModelAttribute` 검색 Form 을 받고, Service 에서 `totalCount` 와 목록을 조회한 뒤 `PagingResponse#setPaging(...)` 으로 페이징 정보를 채운다.
- 신규 API path 의 동작명은 CRUD 성격에 맞춰 통일한다. 목록은 `list`, 상세 조회는 `view`, 등록은 `add`, 수정은 `modify`, 삭제는 `delete` 를 사용한다.
- Controller 메서드명은 path 동작과 기능을 그대로 표현한다. 예: `ssoList`, `ssoView`, `ssoAdd`, `ssoModify`, `ssoDelete`, `adminMemberDelete`. HTTP path와 Controller 메서드는 API 기능을 나타내고, 실제 DB 작업 성격은 Controller가 호출하는 Service 메서드명에서 표현한다. admin/front 분리 서버에서는 path에 `/admin` 또는 `/front` 접두어를 붙이지 않는다.