Java/Spring 업무 코드에 '무엇'보다 '왜'를 남기는 주석 기준
코드가 말하지 못하는 것
Java/Spring 업무 코드를 인수인계하거나 오랜만에 다시 보면 문법보다 판단 근거를 이해하는 데 시간이 더 걸립니다.
@Transactional, @PreAuthorize, Repository 호출과 예외 처리를 읽으면 코드가 무엇을 하는지는 알 수 있습니다. 하지만 다음 질문은 코드만으로 답하기 어려울 수 있습니다.
- 왜 이 상태에서만 변경을 허용하는가?
- 같은 요청이 두 번 들어오면 어느 계층이 중복을 막는가?
- 선조회 이후에도 DB 유니크 제약이 필요한 이유는 무엇인가?
- 외부 모델이나 API가 잘못된 값을 반환하면 어디서 중단하는가?
- 권한이 없는 사용자에게
403과404중 어떤 응답을 주는가? - 이 순서를 바꾸면 어떤 테스트와 문서를 함께 수정해야 하는가?
반대로 모든 줄에 설명을 붙이면 코드보다 주석이 더 많아집니다.
// 요청을 저장한다.
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와 주석 처리한 코드가 남아 있지 않은가? - 고객 식별정보, 내부 경로, 인증정보와 운영 데이터가 없는가?
- 검증하지 않은 성능이나 보안을 보장한다고 표현하지 않는가?
팀 작업 기준으로 유지하기
주석 품질을 개인 습관에만 맡기지 않으려면 저장소의 작업 기준과 연결해야 합니다.
- 재사용 가능한 코드 설명 표준을 저장소에 둡니다.
- Agent 작업 지침과 리뷰 체크리스트에서 표준을 참조합니다.
- 보안, 상태 변경, 외부 I/O, 트랜잭션과 동시성 코드를 우선 검토합니다.
- 관련 동작을 바꾸는 커밋에서 Javadoc과 주석도 함께 검토합니다.
- 단순 getter까지 강제하지 않도록 자동 검사 예외 범위를 명확히 합니다.
- 설명과 구현이 충돌하면 테스트와 설계 문서를 함께 확인해 의도한 계약을 다시 결정합니다.
이 글에서 사용한 상세 기준과 체크리스트는 Java/Spring 코드 설명과 주석 표준에 별도로 정리했습니다. 다른 저장소에서는 공개 범위, 언어와 검증 도구를 조정해 기본 표준으로 재사용할 수 있습니다.
마무리
좋은 주석은 코드 양을 늘리는 작업이 아니라 업무 규칙을 유지 가능한 계약으로 바꾸는 작업입니다. 트랜잭션, 멱등성, 권한과 외부 시스템 경계처럼 변경 위험이 큰 곳부터 이유를 남기고 테스트와 함께 검토하면 됩니다. 결국 중요한 것은 주석의 개수가 아니라, 다음 개발자가 안전하게 변경할 만큼 판단 근거가 남아 있는지입니다.