95 lines
7.4 KiB
Markdown
95 lines
7.4 KiB
Markdown
# 코드 스타일
|
|
|
|
## 코드 스타일 규칙
|
|
|
|
### 1) 포맷/기본 규칙
|
|
- `.editorconfig` 기준을 준수한다.
|
|
- 인덴트: 공백 4칸.
|
|
- 줄바꿈: LF.
|
|
- 최대 라인 길이: 130.
|
|
- 파일 끝 개행 유지, trailing whitespace 제거.
|
|
|
|
### 2) import 규칙
|
|
- 와일드카드 import(`*`)를 사용하지 않는다.
|
|
- 사용하지 않는 import를 남기지 않는다.
|
|
- import alias(`as`)는 현재 코드베이스에서 사용 사례가 없으므로 지양한다.
|
|
- 기존 파일의 import 정렬/그룹 스타일을 그대로 맞춘다.
|
|
|
|
### 3) 네이밍 규칙
|
|
- 클래스/인터페이스/enum: PascalCase.
|
|
- 함수/변수/파라미터: camelCase.
|
|
- 상수: UPPER_SNAKE_CASE (`companion object` 내부 `const val`).
|
|
- Request/Response DTO는 `...Request`, `...Response` 접미사를 유지한다.
|
|
- 서비스/컨트롤러/리포지토리 명명은 역할 접미사(`Service`, `Controller`, `Repository`)를 유지한다.
|
|
- 인터페이스의 기본 구현체는 접미사 `Impl`을 사용하지 않고 접두사 `Default`를 사용한다. 예: `HomeRecommendationQueryRepository`의 기본 구현체는 `DefaultHomeRecommendationQueryRepository`로 명명한다.
|
|
|
|
### 4) 패키지/코드 배치 규칙
|
|
- 기존 로직을 수정하는 경우에는 기존 패키지 구조를 따른다.
|
|
- 기존 로직 수정이 아닌 신규 API나 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
|
|
- 신규 도메인 또는 신규 기능 패키지 생성 시 `kr.co.vividnext.sodalive.v2` 바로 아래에 도메인 패키지를 먼저 만들고, 그 아래에 경량 헥사고날 아키텍처를 적용한다.
|
|
- 클라이언트 공개 API의 화면/클라이언트 맞춤 조립 계층은 `kr.co.vividnext.sodalive.v2.api.{기능}` 하위에 둘 수 있다.
|
|
- 여러 API나 내부 기능에서 재사용될 수 있는 도메인 기능은 `kr.co.vividnext.sodalive.v2.{도메인}` 하위에 별도 패키지로 둔다.
|
|
- 공개 API 조립 패키지가 재사용 도메인 패키지를 호출하는 방향은 허용하지만, 재사용 도메인 패키지가 공개 API 조립 패키지에 의존하지 않는다.
|
|
- 신규 도메인 또는 신규 기능의 기본 패키지 구조는 `application`, `domain`, `port`, `adapter`, `dto`를 사용한다.
|
|
- `application`에는 use case, orchestration service, 트랜잭션 경계를 둔다.
|
|
- `domain`에는 순수 정책, 점수 계산, 값 객체, 도메인 모델, 스냅샷 모델처럼 인프라 의존이 없는 코드를 둔다.
|
|
- `port`에는 application이 필요로 하는 입력/출력 인터페이스를 둔다. 단순 내부 호출까지 억지로 port로 만들지 않는다.
|
|
- `adapter`에는 web controller, JPA/QueryDSL persistence, cache, scheduler 등 외부 입출력 구현을 둔다.
|
|
- `dto`에는 API 요청/응답 DTO와 adapter 경계에서 사용하는 DTO를 둔다.
|
|
- 기존 패키지를 수정하는 작업에는 헥사고날 패키지 구조를 강제로 적용하지 않는다.
|
|
- 기존 `kr.co.vividnext.sodalive.v2` 하위 코드가 이미 다른 구조로 작성되어 있으면 해당 작업의 범위 안에서만 기존 구조를 유지하고, 신규 도메인부터 헥사고날 구조를 적용한다.
|
|
|
|
### 5) 타입/널 처리
|
|
- Kotlin 타입 시스템을 활용하고 nullable(`?`)를 명시한다.
|
|
- 불필요한 `Any`/약한 타입을 피하고 구체 타입을 우선한다.
|
|
- 기존 코드에서 `!!` 사용이 많지만, 신규 코드는 가능한 안전 호출/가드절/명시적 예외로 대체를 우선 고려한다.
|
|
|
|
### 6) API/응답 규칙
|
|
- API 응답은 `ApiResponse.ok(...)`, `ApiResponse.error(...)` 패턴을 따른다.
|
|
- 컨트롤러는 도메인 예외를 직접 포맷하지 말고 `SodaException`을 던진다.
|
|
- 인증 사용자 필요 시 `@AuthenticationPrincipal(... ) member: Member?` 패턴 + null 가드절을 사용한다.
|
|
|
|
### 7) 예외 처리 규칙
|
|
- 비즈니스 예외는 `SodaException(messageKey = "...")` 우선 사용.
|
|
- 사용자 노출 문구는 하드코딩보다 `messageKey` 기반 i18n을 우선한다.
|
|
- 공통 예외 변환은 `SodaExceptionHandler`에서 수행하므로, 개별 컨트롤러에서 중복 처리하지 않는다.
|
|
- 예외를 삼키는 빈 `catch` 블록을 금지한다.
|
|
|
|
### 8) 트랜잭션 규칙
|
|
- 서비스 계층에서 `@Transactional`을 사용한다.
|
|
- 조회 위주 메서드는 `@Transactional(readOnly = true)`를 우선한다.
|
|
- 쓰기 로직은 메서드 단위 `@Transactional`로 경계를 명확히 한다.
|
|
|
|
### 9) 비동기/동시성 규칙
|
|
- 비동기 처리는 Kotlin Coroutines 패턴을 따른다.
|
|
- `CoroutineScope(Dispatchers.IO)` + `launch` + 예외 처리 패턴을 일관되게 유지한다.
|
|
- 생명주기 종료 시 scope 정리(`@PreDestroy`) 패턴을 참고한다.
|
|
- 다중 서버 인스턴스에서 같은 `@Scheduled` 작업이 동시에 실행될 수 있는 스케줄러는 Redisson 기반 분산 lock을 적용해 클러스터 전체에서 한 인스턴스만 작업을 실행하도록 한다.
|
|
- 스케줄러 분산 lock은 기존 `RedissonClient` bean을 재사용하고, lock key는 작업 목적이 드러나도록 `lock:{job-name}` 형식으로 고정한다.
|
|
- lock 획득 실패는 다른 인스턴스가 처리 중인 정상 skip으로 보고, 작업 본문은 lock을 획득한 경우에만 실행한다.
|
|
- lock 해제는 `finally`에서 `lock.isHeldByCurrentThread` 확인 후 `unlock()`한다.
|
|
- 스케줄러 작업 시간이 예측 가능하면 무기한 watchdog 의존보다 최악 실행 시간에 여유를 더한 명시적 `leaseTime`을 우선 검토한다.
|
|
|
|
### 10) 의존성 주입
|
|
- 생성자 주입(primary constructor + `private val`)을 기본으로 사용한다.
|
|
- 필드 주입보다 명시적 생성자 주입을 우선한다.
|
|
|
|
### 11) 주석
|
|
- 의미 단위별로 주석을 작성한다.
|
|
- 주석은 한 문장으로 간결하게 작성한다.
|
|
- 주석은 코드의 의도와 구조를 설명한다.
|
|
- 주석은 코드 변경 시 업데이트를 잊지 않는다.
|
|
|
|
### 12) DB 저장 시각과 응답 정밀도
|
|
- 상태 변경 시각을 DB에 저장하면서 API 응답에도 반환할 때는 먼저 해당 컬럼의 DDL 정밀도를 확인한다.
|
|
MySQL의 소수초 정밀도를 지정하지 않은 `TIMESTAMP`/`DATETIME`은 초 단위다. `(3)`/`(6)` 컬럼에 초 절삭을 일괄 적용하지 않는다.
|
|
- OS/JDK의 `LocalDateTime.now()`는 나노초까지 반환할 수 있고, H2/JDBC/MySQL은 저장 시 반올림·절삭할 수 있다.
|
|
로컬에서 DB 재조회값과 응답이 같았다는 사실만으로 다른 OS/JDK에서도 같다고 가정하지 않는다.
|
|
- 시각을 한 번 얻고 저장 컬럼의 정밀도·업무 계약에 맞게 정규화한 단일 변수를 엔티티와 응답에 공통 사용한다.
|
|
선물의 소수초 없는 TIMESTAMP 상태 시각은 기존 관례대로 `now.truncatedTo(ChronoUnit.SECONDS)`를 사용한다.
|
|
- 정밀도 조정은 해당 업무 저장 경계에서 처리한다. 응답만 절삭하거나 저장/응답에서 각각 `now()`를 호출하지 않는다.
|
|
시간대 변환은 별도 계약이며 이 문제를 고치기 위해 전역 `toUtcIso`, Jackson, timezone 또는 DDL을 임의 변경하지 않는다.
|
|
- 관련된 메서드만 수정하고 다른 도메인/상태 시각을 무단으로 일괄 정규화하지 않는다. 검증은 테스트 스타일의 왕복 규칙을 따른다.
|
|
- 선례: `82ce6f88`의 `GiftAdminService.cancelGift` 및 선물하기 계획 `P14-T4`.
|
|
같은 원인으로 수령확인 `deliveredAt` 비교가 재발해 `P16-R1`에서 이 규칙을 명시했다.
|