결정-0009: DTO 와 값 객체에 Java record 를 허용하되 가변 컴포넌트는 방어복사를 의무화한다
상태
승인됨
맥락
선례로 삼은 팀 프로젝트는 DTO와 값 객체를 record 가 아닌 Lombok class 로 통일하는 규칙을 운영합니다. 근거는 버그 패턴 탐지기의 가변 객체 참조 노출 경고를 회피하는 것과 팀 컨벤션 일치였습니다. 본 시스템도 같은 탐지기를 쓰므로(결정-0056) 이 규칙을 승계할지 판단해야 했습니다.
한편 jOOQ 는 생성자 기반 매핑으로 record 프로젝션을 1급 지원하고, 도메인 모델과 생성 레코드가 분리되는 구조(결정-0051)에서 DTO는 대부분 불변 값입니다. record 를 금지하면 보일러플레이트가 늘고 조회 결과 매핑에 변환 단계가 추가됩니다.
검증
기억이나 문헌이 아니라 이 환경에서 직접 측정했습니다. SpotBugs 4.10.3과 Java 21 에서 2026-07-24에 수행한 결과입니다.
| 프로브 | 결과 |
|---|---|
record(String, List, Map, byte[]) — 방어복사 없음 |
EI_EXPOSE_REP 4건 |
record(String, int, UUID, Instant, List) — 컴팩트 생성자에서 List.copyOf |
경고 0건 |
결론은 버그 패턴 탐지기와 record 가 근본적으로 충돌하지 않는다는 것입니다. 경고는 가변 컴포넌트를 방어복사 없이 노출할 때만 발생하며, 이는 오탐이 아니라 외부에서 내부 상태를 바꿀 수 있다는 실제 결함입니다. 컴팩트 생성자 한 줄로 해소되고 그 결과 진짜 불변성이 확보됩니다.
class 전환이 이 경고를 없애는 이유는 문제를 고쳐서가 아니라 Lombok 생성 메서드가 분석에서 제외되기 때문일 가능성이 높습니다. 경고를 가리는 쪽에 가깝습니다.
검토한 대안
record 를 금지하고 Lombok class 로 통일합니다
| 구분 | 내용 |
|---|---|
| 장점 | 선례를 그대로 승계해 판단 비용이 없습니다 |
| 단점 | 경고를 가릴 뿐 불변성 문제는 남습니다. 보일러플레이트와 매핑 변환 비용이 듭니다 |
| 기각 사유 | 위 측정이 경고의 원인을 실제 결함으로 특정했습니다 |
record 를 허용하되 탐지기 제외 필터로 경고를 억제합니다
| 구분 | 내용 |
|---|---|
| 장점 | 방어복사를 쓰지 않아도 됩니다 |
| 단점 | 실제 결함까지 함께 눈감게 됩니다 |
| 기각 사유 | 측정 결과 억제가 불필요했습니다. 불필요한 억제는 게이트의 신뢰를 깎습니다 |
record 를 허용하고 방어복사를 의무화합니다 (채택)
설정 변경 없이 경고가 0건이며 진짜 불변성을 확보합니다.
결정
- DTO와 값 객체, 조회 프로젝션 타깃은 record 를 기본으로 합니다.
- 가변 컴포넌트를 가지는 record 는 컴팩트 생성자에서 방어복사합니다. 컬렉션과 배열, 가변 객체가 그 대상입니다. 버그 패턴 탐지기가 이를 강제하므로 별도 리뷰 항목으로 두지 않습니다.
- 탐지기 제외 필터에 가변 객체 참조 노출 규칙을 추가하지 않습니다.
- 역직렬화가 필요한 요청 DTO도 record 로 씁니다. 프레임워크 제약으로 불가능한 사례가 나오면 그때 class 를 쓰고 사유를 주석으로 남깁니다.
결과
- 조회 결과를 jOOQ 에서 DTO로 바로 프로젝션할 수 있어 변환 코드가 줄어듭니다.
- 정적 분석 설정을 느슨하게 만들지 않고도 record 를 씁니다. 게이트의 신뢰가 유지됩니다.
- 감수하는 것은 컬렉션을 가진 record 마다 붙는 컴팩트 생성자 한 줄입니다.