SON Kiseok Technical Blog

Java/Spring 업무 코드에 '무엇'보다 '왜'를 남기는 주석 기준

코드가 말하지 못하는 것

Java/Spring 업무 코드를 인수인계하거나 오랜만에 다시 보면 문법보다 판단 근거를 이해하는 데 시간이 더 걸립니다.

@Transactional, @PreAuthorize, Repository 호출과 예외 처리를 읽으면 코드가 무엇을 하는지는 알 수 있습니다. 하지만 다음 질문은 코드만으로 답하기 어려울 수 있습니다.

  • 왜 이 상태에서만 변경을 허용하는가?
  • 같은 요청이 두 번 들어오면 어느 계층이 중복을 막는가?
  • 선조회 이후에도 DB 유니크 제약이 필요한 이유는 무엇인가?
  • 외부 모델이나 API가 잘못된 값을 반환하면 어디서 중단하는가?
  • 권한이 없는 사용자에게 403404 중 어떤 응답을 주는가?
  • 이 순서를 바꾸면 어떤 테스트와 문서를 함께 수정해야 하는가?

반대로 모든 줄에 설명을 붙이면 코드보다 주석이 더 많아집니다.

// 요청을 저장한다.
repository.save(request);

// 상태를 반환한다.
return request.getStatus();

이런 주석은 코드가 이미 말하는 내용을 반복합니다. 변경할 때 같이 고치지 않으면 오히려 잘못된 정보를 남깁니다.

코딩 도구가 자동으로 설명을 생성하는 환경에서는 이 문제가 더 커질 수 있습니다. 생성 속도보다 어떤 설명을 남기고 무엇을 생략할지에 대한 기준이 먼저 필요합니다.

코드와 설명의 수명이 다르다

주석의 역할을 코드를 한글로 번역하는 것으로 잡으면 설명이 많아도 유지보수에 도움이 되지 않습니다.

코드와 설명이 담당해야 할 정보의 수명이 다르기 때문입니다.

구분 우선 표현할 내용
코드 타입, 실행 순서, 조건, 계산과 호출 관계
Javadoc 타입과 메서드가 지켜야 하는 지속적인 계약
인라인 주석 해당 코드 가까이에서만 이해할 수 있는 이유와 제약
테스트 설명한 계약이 실제로 지켜지는지 확인하는 동작 증거
설계 문서 여러 컴포넌트에 걸친 구조와 대안 선택

업무 규칙을 코드 밖 문서에만 두면 구현과 멀어집니다. 반대로 모든 설계를 소스 주석에 넣으면 변경 범위와 책임이 불분명해집니다. 설명 수준을 나누는 것이 필요합니다.

기준: 무엇은 코드에, 왜는 설명에

기준은 간단합니다.

코드는 무엇을 보여주고, 설명은 코드만으로 알기 어려운 , 제약, 실패 방식과 변경 영향을 알려 줍니다.

설명은 세 단계로 나눕니다.

1. 타입 수준 Javadoc

업무 책임이 있는 클래스와 인터페이스에는 다음 내용을 필요한 범위에서 작성합니다.

  • 목적과 책임
  • 주요 입력과 출력 또는 협력 객체
  • 핵심 업무와 보안 기준
  • 실패 시 동작과 부작용
  • 수정할 때 함께 확인할 테스트와 문서

2. 메서드 수준 Javadoc

다음 메서드는 이름만으로 계약을 파악하기 어렵기 때문에 설명할 가치가 큽니다.

  • Controller와 애플리케이션 서비스의 공개 진입점
  • 상태를 변경하는 도메인 메서드
  • 트랜잭션, 멱등성, 잠금과 동시성 규칙이 있는 메서드
  • 권한과 객체 접근 범위를 결정하는 메서드
  • DB, 파일, 메시지 브로커, LLM과 외부 API를 사용하는 메서드
  • 단위, 시간대, 식별자와 외부 응답을 변환하는 메서드

3. 인라인 주석

인라인 주석은 다음처럼 코드 가까이에 있어야 의미가 있는 이유만 설명합니다.

  • 특정 저장 순서를 지키는 이유
  • 경합 상황의 최종 판정 기준
  • 실패 시 일부 데이터만 남지 않게 하는 방법
  • 외부 시스템 제약 때문에 필요한 우회
  • 변경할 때 함께 바뀌어야 하는 계약

단순 getter, 필드 대입만 하는 생성자, record 접근자와 이름이 충분히 명확한 한 줄 위임에는 설명을 붙이지 않습니다.

Java/Spring에 적용하기

합성 구매 승인 서비스를 예로 들면, 모델이 구매 요청 초안을 만들더라도 승인과 저장 가능 여부는 Spring 애플리케이션이 결정해야 합니다. 이 경계를 타입 설명에 남길 수 있습니다.

/**
 * 신뢰할 수 없는 자연어 요청을 검증된 구매 요청 초안으로 변환하는 서비스.
 *
 * <p><strong>입력/출력:</strong> 사용자 요청과 서버가 조회한 정책 근거를 받아
 * 아직 승인되지 않은 초안을 반환한다.
 *
 * <p><strong>핵심 기준:</strong> 모델은 초안만 제안하며 승인, 상태 전이와
 * 저장 가능 여부는 애플리케이션과 도메인 규칙이 최종 결정한다.
 *
 * <p><strong>수정 시 주의:</strong> 모델 출력 계약을 바꾸면 구조 검증,
 * 실패 시나리오 테스트와 아키텍처 문서를 함께 확인한다.
 */
public class PurchaseDraftService {
}

멱등성을 처리하는 메서드는 반환값뿐 아니라 같은 키에 다른 입력이 들어왔을 때의 정책을 설명해야 합니다.

/**
 * 같은 요청자의 재시도를 하나의 구매 초안으로 수렴시킨다.
 *
 * <p>같은 멱등성 키와 같은 입력이면 기존 결과를 반환하고, 같은 키에 다른 입력이
 * 들어오면 기존 결과를 덮어쓰지 않고 충돌로 중단한다.
 *
 * @param idempotencyKey 재시도 사이에 유지되는 요청 식별키
 * @param requestText 신뢰할 수 없는 자연어 구매 요청
 * @return 새로 생성했거나 동일 입력으로 확인한 구매 요청
 */
public PurchaseRequest createDraft(String idempotencyKey, String requestText) {
    // 구현 생략
}

선조회와 저장 사이의 경합을 처리한다면 다음처럼 이유를 인라인으로 남깁니다.

PurchaseRequest existing = repository.findByActorAndKey(actor, key).orElse(null);
if (existing != null) {
    return requireSameInput(existing, fingerprint);
}

// 애플리케이션 선조회만으로는 최초 요청끼리의 경합을 제거할 수 없다.
// DB 고유 제약을 사용하는 INSERT ... ON CONFLICT DO NOTHING이 승자를 정한다.
repository.insertIfAbsent(newDraft);
PurchaseRequest winner = repository.findByActorAndKey(actor, key)
        .orElseThrow(IllegalStateException::new);
return requireSameInput(winner, fingerprint);

위 코드는 트랜잭션 경계를 생략한 구조 예시입니다. PostgreSQL의 충돌 무시 구문이나 별도 저장 트랜잭션처럼, 제약 위반 뒤 현재 JPA 트랜잭션이 rollback-only가 되지 않는 방식을 선택해야 합니다. 단순 save()는 flush 시점이 늦어 예외를 같은 블록에서 잡지 못할 수 있으므로 실제 구현에서는 사용 중인 DB와 트랜잭션 방식에 맞는 테스트가 필요합니다.

주석을 추가하기 전에 메서드 이름과 구조도 함께 봐야 합니다. 설명이 여러 문단 필요할 정도로 한 메서드에 책임이 몰려 있다면 먼저 검증, 저장과 외부 호출을 분리하는 편이 낫습니다.

계층별로는 다음 관점을 우선합니다.

계층 설명할 내용
Controller/API 신뢰하지 않는 입력과 응답의 정보 노출 경계
Application Service 업무 흐름, 트랜잭션, 멱등성과 경합 처리
Domain 허용 상태 전이와 불변조건
Persistence 유니크 제약, 잠금과 조회 결과의 업무 의미
External Adapter timeout, 출력 검증, fallback과 재시도 정책
Security 역할 권한과 객체 권한의 분리, 존재 여부 은닉 기준
Test 경합, 롤백과 실패 주입 장치의 의도

설명도 검증 대상이다

주석은 많다고 좋은 것이 아니므로 존재 여부와 내용의 정확성을 나누어 검증합니다.

자동 검증

  • Checkstyle 규칙이나 커스텀 검사를 구성한 프로젝트라면 지정한 타입과 메서드의 Javadoc 누락을 검사합니다.
  • Javadoc/doclint 생성 단계에서 태그, 링크와 문법 오류를 확인합니다.
  • 컴파일과 단위·통합 테스트로 설명한 계약의 실제 동작을 확인합니다.
  • git diff --check로 불필요한 공백과 형식 오류를 확인합니다.

일반적인 Maven 프로젝트에서는 다음 검증을 사용할 수 있습니다.

./mvnw verify
./mvnw javadoc:javadoc
git diff --check

프로젝트에 Checkstyle이나 Javadoc 검증이 구성되지 않았다면 명령을 완료 기준으로 기록하기 전에 먼저 빌드 구성을 확인해야 합니다.

코드 리뷰

자동 검사는 설명의 존재와 형식은 확인할 수 있지만 사실 여부까지 판단하지는 못합니다. 리뷰에서는 다음을 확인합니다.

  • 설명이 실제 코드와 테스트에 일치하는가?
  • 코드가 이미 말하는 내용을 반복하지 않는가?
  • 업무 규칙, 신뢰 경계, 실패 방식과 변경 영향이 드러나는가?
  • 오래된 TODO와 주석 처리한 코드가 남아 있지 않은가?
  • 고객 식별정보, 내부 경로, 인증정보와 운영 데이터가 없는가?
  • 검증하지 않은 성능이나 보안을 보장한다고 표현하지 않는가?

팀 작업 기준으로 유지하기

주석 품질을 개인 습관에만 맡기지 않으려면 저장소의 작업 기준과 연결해야 합니다.

  1. 재사용 가능한 코드 설명 표준을 저장소에 둡니다.
  2. Agent 작업 지침과 리뷰 체크리스트에서 표준을 참조합니다.
  3. 보안, 상태 변경, 외부 I/O, 트랜잭션과 동시성 코드를 우선 검토합니다.
  4. 관련 동작을 바꾸는 커밋에서 Javadoc과 주석도 함께 검토합니다.
  5. 단순 getter까지 강제하지 않도록 자동 검사 예외 범위를 명확히 합니다.
  6. 설명과 구현이 충돌하면 테스트와 설계 문서를 함께 확인해 의도한 계약을 다시 결정합니다.

이 글에서 사용한 상세 기준과 체크리스트는 Java/Spring 코드 설명과 주석 표준에 별도로 정리했습니다. 다른 저장소에서는 공개 범위, 언어와 검증 도구를 조정해 기본 표준으로 재사용할 수 있습니다.

마무리

좋은 주석은 코드 양을 늘리는 작업이 아니라 업무 규칙을 유지 가능한 계약으로 바꾸는 작업입니다. 트랜잭션, 멱등성, 권한과 외부 시스템 경계처럼 변경 위험이 큰 곳부터 이유를 남기고 테스트와 함께 검토하면 됩니다. 결국 중요한 것은 주석의 개수가 아니라, 다음 개발자가 안전하게 변경할 만큼 판단 근거가 남아 있는지입니다.

전체 글 보기