Skip to content

코딩 컨벤션

dictionary-api가 실제로 지키는 코딩 컨벤션이다. 도구가 자동으로 강제하는 규칙과, 코드 작성·리뷰 시 직접 신경 써야 하는 규칙을 구분한다.

코드 포매팅 (spotless, 자동 강제)

Section titled “코드 포매팅 (spotless, 자동 강제)”
  1. Google Java Style Guide를 spotless로 강제한다.
    build.gradle
    spotless {
    java {
    targetExclude 'build/generated/querydsl/**' // QClass 생성 파일 제외
    googleJavaFormat().aosp().formatJavadoc(false) // 구글 자바 포맷 (4칸 들여쓰기)
    removeUnusedImports()
    trimTrailingWhitespace()
    endWithNewline()
    }
    }
  2. 명령어
    Terminal window
    ./gradlew spotlessCheck # 포맷 문제 확인(문제 있으면 에러)
    ./gradlew spotlessApply # 포맷 문제 자동 수정
  3. spotlessApply는 compileJava(빌드) 시점에 자동 실행된다. 커밋 시점에는 자동 적용되지 않는다 — pre-commit 훅이 spotlessCheck만 수행해서, 문제가 있으면 커밋을 차단할 뿐이다(자세한 내용은 깃훅 가이드 참고). 이 경우 spotlessApply를 직접 실행한 뒤 다시 커밋해야 한다.

final 키워드 명시 (도구가 강제하지 않음, 직접 신경 써야 함)

Section titled “final 키워드 명시 (도구가 강제하지 않음, 직접 신경 써야 함)”
  1. 필드(생성자 주입 대상)/메소드 파라미터/로컬 변수/예외 catch 변수/상수(static final)는 재할당하지 않는 한 전부 final을 명시한다.
    private final BoardRepository boardRepository; // 생성자 주입 필드
    public SearchStdictResponseDto findStdict(final String word) { ... } // 파라미터
    final SessionUser sessionUser = ServletUtils.getSessionUser(); // 로컬 변수
    } catch (final Exception e) { ... } // 예외 변수
  2. 재할당이 꼭 필요한 경우에만 final을 생략한다 — final이 없는 변수를 보면 “이 값은 메소드 안에서 바뀐다”는 신호로 바로 읽히게 하려는 의도다. 조건별로 값을 조립하는 로직은 재할당 대신 메소드를 분리하거나 삼항 연산자로 처리해 final을 유지한다.
  3. spotless/Google Java Format이 자동으로 강제하는 규칙이 아니다(final 유무를 검사하지 않는다).
  4. 이유: (1) 이미 정착된 관례라 새 코드도 맞춰야 스타일이 갈라지지 않고, (2) 값이 한 번만 세팅된다는 걸 코드만 보고 알 수 있어 가독성이 좋고, (3) 실수로 인한 재할당을 컴파일 타임에 잡아준다.

DTO 상속 구조 — 공통 봉투(envelope)만 상속

Section titled “DTO 상속 구조 — 공통 봉투(envelope)만 상속”
  1. “DTO가 부모 클래스를 가진다”는 말은 기능별 payload DTO가 아니라, 요청/응답을 감싸는 공통 봉투 클래스에만 해당한다.
    AbstractDto<T> (payload 필드 + 재변환 헬퍼)
    ├─ RequestDto<T>
    │ └─ PaginationRequestDto<T>
    └─ ResponseDto<T>
    ├─ ListResponseDto<T> extends ResponseDto<List<T>>
    └─ PaginationResponseDto<T> extends ResponseDto<List<T>>
  2. BoardCreateRequestDto/BoardResponseDto 같은 기능별 payload DTO는 부모 클래스 없는 순수 POJO다 — 컨트롤러가 RequestDto<BoardCreateRequestDto>처럼 제네릭으로 감싸 쓸 뿐이다.
  3. 같은 접두어를 쓰는 payload DTO끼리도 서로 상속하지 않는다. 필드가 완전히 같아도 공통 부모 클래스로 추출해 중복을 없애지 않고 필드 중복을 그대로 허용한다 — 위임 생성자 보일러플레이트 대비 얻는 이득이 적기 때문이다.
  4. 공통 봉투 클래스 쪽에만 상속을 쓰는 이유: (1) payload/재변환 헬퍼처럼 실제로 공유하는 구현을 한 곳에만 두기 위해서, (2) 상속을 쓰면 Jackson이 부모 필드를 같은 레벨의 평면(flat) JSON으로 얹어줘서 컴포지션보다 코드가 적게 든다, (3) PaginationRequestDto/ListResponseDto처럼 하위 클래스마다 진짜 is-a 관계로 새 필드/의미를 추가한다.

Context 클래스 — 중간 표현이 필요할 때

Section titled “Context 클래스 — 중간 표현이 필요할 때”
  1. 기본 흐름: 컨트롤러가 RequestDto의 payload를 서비스에 넘기고, 서비스가 반환한 Entity를 컨트롤러가 XxxResponseDto.from(entity)로 변환한다 — Entity가 자연스러운 중간 표현 역할을 한다.
  2. tools처럼 DB를 거치지 않는 순수 계산 기능은 Entity가 없다. 이런 경우에 한해 Context 클래스를 만들어 그 자리를 대신한다(RequestDto.toContext() → 서비스가 Context를 채워 반환 → XxxResponseDto.from(context)).
  3. Context 클래스는 dto/context/ 하위 패키지에 별도 파일로 둔다 — RequestDto/ResponseDto가 있는 네임스페이스 클래스 안에 중첩하지 않는다. 컨트롤러↔서비스 사이에서만 쓰이는 내부 전용 객체라 API 계약의 일부가 아니라는 것을 위치로 드러낸다.
  1. api/<feature>/dto에 있는 응답 DTO는 그 기능 컨트롤러가 클라이언트에게 돌려주는 응답 계약이지, 여러 기능이 공유하는 모델이 아니다.
  2. 다른 기능이 같은 데이터가 필요해지는 시점에도, 그 DTO를 공용 위치로 옮기거나 직접 import해서 재사용하지 않고 자기 이름을 붙인 응답 DTO를 새로 만든다.
  3. 단, 이 금지는 기능 간(cross-feature) 재사용에만 적용된다 — 한 기능 안의 API끼리(find/findAll/ findByPage/create/update)는 응답 DTO 하나를 그대로 공유한다.
  4. 이유: 한 기능의 응답 DTO를 다른 기능이 재사용하면, 원래 기능이 자기 화면 요구사항 때문에 필드를 바꿀 때 그 다른 기능의 API 응답까지 의도치 않게 함께 바뀌는 결합이 생긴다.