DTO 생성자 표준은 금지 목록을 적은 뒤 그 목록이 저절로 지켜진다고 했다.
DTO에는 @Setter, @NoArgsConstructor, @Data를 쓰지 않는다.
필드를 final로 선언해두면 이 셋이 애초에 컴파일되지 않아, 실수 자체가 불가능해진다.
원칙 1의 예시 주석도 같은 말을 한다 — 「final이므로 @Setter는 아예 컴파일되지 않는다」. 규율이 아니라 컴파일러가 막는다는 것이 이 규칙의 강제 수단이었다.
표준을 증명하는 방식을 정할 때 이미 이 문장을 의심했다. 「내가 아는 Lombok 동작으로는 final 필드에 @Setter나 @Data를 붙여도 컴파일 에러 없이 setter만 생기지 않는다. 아직 확인하지 않았다.」 이 글은 그걸 확인한 실험이다.
주장 — final 필드 DTO에 @Setter·@NoArgsConstructor·@Data를 붙이면 컴파일되지 않는다
claim.md의 주장은 표준 글 문장 그대로다. 근거 종류는 1(기술 사실)이다.
주장이 「컴파일되지 않는다」이므로 판정은 javac 종료 코드로만 한다. setter가 실제로 생기는지는 기록만 했다. 「컴파일은 되는데 setter는 안 생긴다」와 「컴파일이 막힌다」는 강제 수단으로서 전혀 다르기 때문이다.
반증 조건 — 변형 넷 중 하나라도 컴파일되면 반증이다
- refuted — 변형 넷 중 하나라도
javac종료 코드가 0이다 - supported — 넷 모두 0이 아니다
- inconclusive — 대조군이 컴파일되지 않는다
대조군을 둔 이유는 환경이 틀려서 전부 실패하는 경우를 supported로 읽지 않기 위해서다. 대조군까지 실패하면 규칙이 맞은 게 아니라 컴파일러가 안 돈 것이다.
설계 — 빌드 도구 없이 javac에 Lombok만 걸어 다섯 파일을 따로 컴파일한다
대조군은 표준이 권하는 DTO 그대로다. @Getter, final 필드 둘, 2인자 생성자. 여기에 금지 어노테이션을 하나씩 더해 변형 넷을 만들었다.
Control 대조군
SetterOnClass 클래스에 @Setter
SetterOnField final 필드마다 @Setter
NoArgsConstructor 클래스에 @NoArgsConstructor
Data 클래스에 @Data
@Setter를 클래스와 필드 두 자리에 나눈 이유가 있다. Lombok은 클래스에 붙인 어노테이션을 「해당하는 필드에만」 적용하는 식으로 다룰 수 있어서, 두 자리의 진단이 다를 수 있었다.
컴파일은 Gradle을 거치지 않고 JDK 21 javac에 Lombok 1.18.48을 annotation processor로 걸어 직접 불렀다. 빌드 도구의 경고 설정이 끼어들지 않게 하려는 것이다. 컴파일된 변형은 javap로 public setter 수를 셌다.
결과 — 막힌 것은 @NoArgsConstructor 하나였다
run.sh가 쓴 원본 출력이다.
PROBE_ENV lombok=1.18.48 javac=21.0.9
PROBE variant=Control exit=0 errors=0 warnings=0 setters=0 noargs=0
PROBE variant=Data exit=0 errors=0 warnings=0 setters=0 noargs=0
PROBE variant=NoArgsConstructor exit=1 errors=1 warnings=0 setters=- noargs=-
DIAG NoArgsConstructor | variants/NoArgsConstructor.java:6: error: variable productName might not have been initialized
PROBE variant=SetterOnClass exit=0 errors=0 warnings=0 setters=0 noargs=0
PROBE variant=SetterOnField exit=0 errors=0 warnings=2 setters=0 noargs=0
DIAG SetterOnField | variants/SetterOnField.java:10: warning: Not generating setter for this field: Setters cannot be generated for final fields.
(DIAG는 한 줄씩만 옮겼다. 전문은 result.md에 있다.)
| 변형 | 컴파일 | 진단 | 생긴 setter |
|---|---|---|---|
@Setter 클래스 |
된다 | 없음 | 0 |
@Setter 필드 |
된다 | 경고 2 | 0 |
@Data |
된다 | 없음 | 0 |
@NoArgsConstructor |
안 된다 | 에러 1 | — |
대조군이 컴파일됐고 변형 셋이 종료 코드 0이다. 판정은 refuted다.
예상과 달랐던 점 — 경고조차 필드에 붙였을 때만 나온다
반증은 예상했다. 예상 밖이었던 것은 경고가 나오는 자리다.
@Setter를 필드에 붙이면 필드마다 「final 필드에는 setter를 만들 수 없다」는 경고가 나온다@Setter를 클래스에 붙이거나@Data를 쓰면 경고도 없다. Lombok은 클래스 단위 어노테이션을 「만들 수 있는 필드에만 만든다」로 다루고, 만들 수 있는 필드가 없어도 조용히 넘어간다
실수로 섞여 들어오는 모양은 대개 클래스 머리에 붙은 @Data나 @Setter다. 그 모양이 가장 조용하다.
@NoArgsConstructor가 막힌 이유도 Lombok이 아니라 Java다. 기본 생성자가 생기면 final 필드가 초기화되지 않은 채 남으므로 javac가 거부한다. 「final이 막아준다」는 설명은 이 하나에서만 참이었다.
버전도 적어둔다. 이 실험은 Lombok 1.18.48로 돌렸고, setter 되돌아감 실험은 Boot가 관리하는 1.18.46을 썼다. 두 버전에서 같은 결과가 나오는지는 재지 않았다.
이 결과가 표준에 남기는 것 — 금지 목록을 무엇이 지킬지 이슈에서 정한다
결과만 보면 금지 어노테이션이 붙은 DTO도 setter는 생기지 않는다. 객체가 불변이라는 원칙 1의 목표는 final이 여전히 지킨다. 무너진 것은 「실수 자체가 불가능하다」, 즉 금지 목록의 강제 수단이다. 금지된 어노테이션이 코드에 소리 없이 남을 수 있다.
표준 글 원칙 1과 원칙 3에 > 반증: 줄을 달고 kind: 모순 이슈로 넘겼다. 거기서 정할 것은 금지 목록을 계속 둘 것인가, 둔다면 컴파일러 대신 무엇이 지킬 것인가다.
정리
final필드 DTO에서 컴파일이 막히는 것은@NoArgsConstructor하나다. 막는 쪽도 Lombok이 아니라 Java다@Setter와@Data는 컴파일되고 setter만 빠진다. 클래스에 붙이면 경고도 없다- 불변은 지켜지지만 금지 목록은 지켜지지 않는다. 강제 수단을 다시 정해야 한다
자신만의 철학을 만들어가는 중입니다.
댓글남기기