콘텐츠로 이동

코딩 컨벤션

FillMap 백엔드 프로젝트의 코딩 컨벤션. 모든 커밋에 강제 적용.

Java 코딩 스타일

네이버 Java 코딩 컨벤션 준수.

  • 들여쓰기: 하드탭
  • 중괄호: K&R 스타일 (개행 문자 아님)
  • 최대 줄 길이: 120자
  • 한 줄 하나의 문장: 세미콜론 뒤 개행

Import 순서

static import
─ 빈 줄 ─
java.*
javax.*
─ 빈 줄 ─
org.*
─ 빈 줄 ─
lombok.*
─ 빈 줄 ─
com.*

각 그룹 사이에 빈 줄 하나. 알파벳 정렬.

네이밍

DTO

  • Request: XxxRequestDto (예: UserSignupRequestDto)
  • Response: XxxResponseDto (예: UserProfileResponseDto)
  • 클래스 → 파일명 완전 일치

Entity / 도메인

  • 클래스명: 명사 단수 (User, Video, Grid)
  • 테이블명 (DB): 명사 복수 소문자 (users, videos, grids)

서비스 / 컨트롤러

  • Service 인터페이스: XxxService (예: GridQueryService)
  • Service 구현체: XxxServiceImpl (예: GridQueryServiceImpl)
  • Controller: XxxController (예: GridController)

Enum

  • 클래스명: PascalCase (예: VideoStatus, ProcessingStatus)
  • 상수명: SCREAMING_SNAKE_CASE (예: ACTIVE, BLINDED, UPLOADED)

커밋 메시지

git hook으로 강제됨. 형식:

MSG-{번호} {타입}: {요약}

타입: feat, fix, refactor, docs, test, chore, style

예시: - MSG-78 feat: GridEncoder 유틸리티 및 테스트 4건 추가 - MSG-86 fix: GIST 인덱스 누락 수정 - MSG-1 chore: PR 템플릿 추가

제목 가독성 (2026-08-04 신설)

커밋 이력은 코드를 같이 안 본 사람(리뷰 추적·나중의 복기)이 읽는 1차 자료다. 제목을 압축 명사 나열로 쓰지 않는다:

  • 제목 = 코드를 안 연 사람이 무엇이 바뀌었는지 아는 문장. 동작·효과 중심으로 쓴다 ("무효 토큰 자동 삭제" O, "deleteAllByTokens 신설" X). 가운뎃점(·) 나열 금지.
  • 전문용어는 제목에서 흔한 말로 풀고, 정확한 클래스명·수치·근거는 커밋 본문(둘째 줄 이하)에 적는다.
  • 커밋당 한 관심사 원칙은 그대로 — 관심사가 여럿이라 제목이 나열이 되면 커밋을 쪼갠다.
MSG-179 fix: 컨슈머 poll 튜닝·말폼드 비재시도 분류               (X) 나열 압축체
MSG-179 fix: 소비 지연 시 그룹 이탈 방지, 깨진 메시지는 즉시 폐기  (O) 읽히는 문장

본문은 기본적으로 쓰지 않는다 (2026-08-06 신설)

제목 한 줄이 기본이다. 본문은 제목으로 설명되지 않는 근거가 있을 때만 2~3문장 붙인다 (실측 수치, 기각한 대안처럼 나중에 복기할 값). 제목이 다 말하는데 본문을 덧붙이면 군더더기다.

본문을 길게 쓰고 싶어지면 먼저 커밋에 관심사가 여럿 섞였는지 의심하고 쪼갠다. 소제목·표· 여러 문단이 필요하다면 그건 커밋이 아니라 PR 본문이나 스펙 작업 로그에 갈 내용이다.

브랜치 명명

git flow 브랜치 타입만 쓴다. 커밋 메시지 타입(feat·fix·chore·docs…)을 브랜치 접두어로 가져다 쓰지 않는다chore/, refactor/, docs/는 git flow에 없다.

feature/MSG-{번호}-{짧은-설명}   # 일반 작업 전부 (기능·수정·리팩터링·문서·설정)
hotfix/MSG-{번호}-{짧은-설명}    # 운영 긴급 수정
release/{버전}                   # 릴리스 준비

티켓 번호와 설명은 하이픈으로 잇는다(슬래시 아님).

feature/MSG-142-bench                 (O)
feature/MSG-142/bench                 (X)  슬래시
chore/MSG-1-pr-template               (X)  git flow에 없는 타입

작업은 항상 새 브랜치에서 시작한다. develop·main에 직접 파일을 만들거나 수정하지 않는다 — 문서(docs/*.md) 작업도 예외 없다.

테스트

  • 프레임워크: JUnit 5 + AssertJ
  • 파일 위치: src/test/java/{패키지 미러링}/{클래스명}Test.java
  • 메서드명: 한국어 백틱 스타일
    @Test
    void 강남역_좌표는_같은_격자로_매핑된다() { ... }
    

3-Layer MVC

Controller → Service → Repository → DB
  • Controller는 얇게. Request 파싱 + Service 호출 + Response 변환만.
  • Service에 비즈니스 로직 집중.
  • Repository는 JpaRepository 상속 + native UPSERT는 @Modifying @Query nativeQuery = true.

영속 계층 — JPA 사용 방식 (MSG-334 명문화 · 2026-08-19 개정)

연관관계 매핑(@ManyToOne 등)을 기본으로 허용한다 (2026-08-19 팀원 K 확정 — MSG-334의 "연관관계 금지·id 보관" 원칙 번복). 번복 이유: id 보관 원칙이 모든 조인 조회를 native와 세타 조인·프로젝션으로 밀어붙여, JPA를 유지하기로 해놓고(아래 ADR) 영속 계층이 사실상 SQL 직서기가 됐다. 같은 날 멘토링(멘토)에서도 유지보수를 이유로 native 최소화, JPQL과 애플리케이션 수준 로직 우선, DB 기능의 유틸리티 계층 추상화가 권고됐다(2026-09-07 네이티브 쿼리 집중 라이브 코드 리뷰 예정). 신규 코드는 연관관계·JPQL·fetch join을 정상적으로 쓴다.

사용 수칙 (convention-reviewer 검사 대상):

  1. fetch는 전부 LAZY 명시. EAGER 금지.
  2. 단방향 @ManyToOne 우선. 컬렉션 매핑(@OneToMany)은 크기 상한이 분명한 자식에만, 근거 주석과 함께 단다. 역방향을 습관으로 달지 않는다.
  3. equals/hashCode/toString에서 연관 필드 제외 (프록시 초기화·순환 참조 방지).
  4. 조회는 파생 쿼리와 JPQL(fetch join·생성자 프로젝션) 우선. native는 PostgreSQL 전용 기능(ON CONFLICT upsert·PostGIS·advisory lock)과 대량 배치에 한정한다 — "일단 native" 관성이 이번 번복이 겨눈 대상이다. 불가피한 native는 흩어 두지 말고 전담 리포지토리나 유틸 계층으로 모은다(멘토 권고).
  5. N+1은 리뷰에서 잡는다: 루프 안 지연 로딩이 보이면 fetch join이나 @EntityGraph로 수렴.
  6. 무결성은 여전히 DB 소유: FK 제약·ON DELETE는 Flyway DDL이 보장한다 (JPA는 validate만).
  7. OSIV는 꺼져 있다 (spring.jpa.open-in-view: false, 2026-09-10 MSG-587). 지연 로딩은 트랜잭션 안에서만 된다 — 컨트롤러나 응답 직렬화 시점에 연관을 건드리면 LazyInitializationException이 난다. 필요한 것은 서비스 안에서 fetch join·@EntityGraph로 미리 채우거나 프로젝션으로 내린다. 다시 켜지 않는다 — 켜면 화면을 그리는 도중에 쿼리가 나가 어디서 몇 번 나가는지 추적이 끊긴다. 끄기 전 실측: 지연 로딩 접근 29곳이 전부 트랜잭션 안, 컨트롤러 엔티티 반환 0건, 전체 스위트 통과 (docs/audit/2026-09-08-backend-audit.md §3.4 · docs/spec/MSG-587.md).

기존 코드 소급 리팩터링은 하지 않는다. id 보관으로 짜인 기존 도메인(Video·UserGrid· Friendship·Report 등)은 그대로 두고 혼재를 허용한다(서지컬 원칙). 2026-09-07 멘토링 라이브 코드 리뷰 전까지 구조 변경 없이 기능 구현을 완료한다는 합의와도 같은 방향이다 — 기존 native의 정리는 그 리뷰에서 나올 개선점을 따라간다. 연관관계 없는 기존 엔티티에서 조인이 필요하면 종전대로 세타 조인 + 생성자 프로젝션을 쓴다 (FROM Report r, User ru WHERE ru.id = r.reporterId).

타 도메인 엔티티를 연관으로 참조할 때는 읽기 전용으로만 쓴다 — 상태 변경은 소유 도메인 서비스를 거친다. "두 도메인의 접점은 인터페이스로만"은 서비스 계층 원칙으로 유지된다.

연관관계 없이도 JPA를 유지하는 이유: 상태 전이 더티 체킹(Video.markBlinded() 류 도메인 메서드가 UPDATE문 없이 성립), @Lock 파생 쿼리·페이징 인프라, enum·시각 타입 자동 매핑, 영속성 컨텍스트의 트랜잭션 정합성(1차 캐시 — 같은 트랜잭션의 같은 행 = 같은 객체). PostgreSQL 전용 기능(ON CONFLICT upsert·PostGIS·advisory lock)은 native로 내린다. MyBatis 전면 전환 반려 판정·번복 트리거·대안(QueryDSL 포크·JdbcClient·jOOQ)은 ADR 정본 참조: 영속 계층 JPA 유지 — MyBatis 전환 반려 (2026-08-03, cf-29917209) — LLM-WIKI 04-decisions/ADR 영속 계층 JPA 유지 MyBatis 반려.md

시각(날짜·시간) 처리 (MSG-376 명문화)

API JSON 경계의 LocalDateTime은 전역 코덱(global/config/UtcLocalDateTimeJsonCodec, @JacksonComponent)이 UTC Z 표기로 주고받는다. 이 체계의 전제는 "naive LocalDateTime 값 = UTC"라는 관례인데, 코덱은 값이 진짜 UTC인지 검증할 수 없으므로 아래를 코드 리뷰에서 강제한다.

  • 인자 없는 LocalDateTime.now() 금지 (src/main 전체). 시스템 기본 시간대(로컬 개발 머신 KST)의 시각이 만들어져 UTC 표기가 붙는 순간 9시간 어긋난다. 서비스는 주입받은 Clock (BadgeAwardServiceImpl·VideoServiceImpl 선례), DI가 안 되는 엔티티 상태 전이 메서드는 LocalDateTime.now(ZoneOffset.UTC) (Report·Friendship·Video 선례 — MSG-376 Codex 적발 후 일괄 교정). 스케줄러·폴러가 Clock 주입 전이라면 같은 형태를 임시 허용한다 (AiBlurPoller는 MSG-379에서 Clock 주입으로 교체 완료 — 임시 허용 잔존은 StaleTokenCleaner 하나). 테스트 코드는 값 대조가 존에 민감할 때만 같은 기준을 적용한다.
  • 개별 필드 @JsonFormat 산발 적용 금지 — 시각 표기 수정은 코덱 한 곳(MSG-376 D-4).
  • KST 라벨이 필요하면 LocalDateTime이 아니라 LocalDate를 쓴다 (UploadHistoryResponseDto.uploadDate 선례). LocalDateTime이면 예외 없이 Z가 붙는다. "KST 시각 문자열" 요구는 와이어 계약 변경이라 논의 대상이다.
  • DTO의 시각 타입은 LocalDateTime(시각)·LocalDate(날짜 라벨)만DtoTimeTypeGuardTest가 classpath 스캔으로 강제한다. 가드는 *Dto 네이밍만 잡으므로 시각 필드를 갖는 응답 타입은 *Dto 네이밍을 지킨다. String에 시각을 담는 우회 금지 (가드가 못 잡는다 — 리뷰 몫).
  • 직렬화가 걸린 테스트는 스프링이 조립한 매퍼로 (@JsonTest·@WebMvcTest). new ObjectMapper() 손조립에는 코덱이 등록되지 않아 코덱 없는 세상을 검증하게 된다.
  • 시각을 쿼리 파라미터로 받지 않는다@RequestParam LocalDateTime은 Jackson이 아니라 스프링 컨버터 경로라 코덱 밖이다. 시각은 요청 본문으로 받는다.

Lombok 사용 원칙

  • @RequiredArgsConstructor 로 DI (@Autowired 지양)
  • @Getter 는 자유롭게, @Setter 는 지양 (불변성 우선)
  • @NoArgsConstructor(access = AccessLevel.PROTECTED) 로 JPA 요구사항 충족
  • @Builder 는 필드 4개 이상일 때만