DTO 생성자 표준 원칙 3은 이렇게 적었다.

record는 canonical constructor를 자동으로 쓰지만, class는 명시해야 한다.

그래서 모든 Request DTO의 생성자에 @JsonCreator@JsonProperty를 붙이는 것이 규칙이 됐다. 그런데 「명시하지 않으면 안 된다」는 돌려본 적이 없는 문장이었다. 표준을 증명하는 방식을 정하고 고른 첫 시범이 이 원칙이고, 이 글은 그 실험 셋 중 하나다.


주장 — @JsonCreator가 없으면 class는 다인자 생성자로 역직렬화되지 않는다

돌리기 전에 주장을 한 문장으로 좁혔다.

@JsonCreator 없이 인자 둘 이상의 생성자만 가진 class는,
Spring Boot가 설정한 ObjectMapper가 그 생성자로 역직렬화하지 못한다.

「Spring Boot가 설정한」을 넣은 이유는 표준을 쓰는 곳이 Spring 프로젝트이기 때문이다. Controller에 들어오는 Request를 만드는 것은 new ObjectMapper()가 아니라 Boot가 컨텍스트에 등록한 매퍼다. 설정 없는 매퍼로 재면 실제 프로젝트와 다른 것을 재게 된다.

근거 종류는 1(기술 사실)이다. 같은 입력을 넣고 되는지 안 되는지만 보면 된다.


반증 조건 — 두 Boot 중 하나라도 값이 채워지면 반증이다

결과를 보기 전에 claim.md에 선을 그었다.

  • refuted — 두 환경 중 하나라도 예외 없이 끝나고 productName=mouse, quantity=3이 채워진다
  • supported — 두 환경 모두 예외로 실패한다
  • inconclusive — 환경이 뜨지 않았거나, 예외 없이 끝났는데 값이 다르다

「하나라도」로 정한 이유는 표준이 버전을 가리지 않고 「명시해야 한다」고 말하기 때문이다. 한 버전에서라도 명시 없이 되면 그 문장은 참이 아니다.


설계 — Boot가 등록한 매퍼를 3.5.16과 4.1.1에서 같은 타입으로 부른다

대상 타입은 가장 단순하게 두었다.

// Jackson 어노테이션도 기본 생성자도 setter 도 없다. 생성자는 이것 하나다
public class NoCreatorRequest {
    private final String productName;
    private final Integer quantity;

    public NoCreatorRequest(String productName, Integer quantity) { ... }
    // getter 둘
}

Lombok을 쓰지 않았다. 이 실험은 Jackson이 생성자를 찾는지만 본다. Lombok이 끼면 결과가 어느 쪽 동작인지 가를 수 없다.

환경은 둘이다. 표준을 쓰는 프로젝트의 Boot 버전을 모르고, Boot 4부터 기본 Jackson이 2(com.fasterxml)에서 3(tools.jackson)으로 바뀌어 결과가 갈릴 수 있었다. 둘 다 Boot Gradle 플러그인을 적용해 컴파일했다. 실제 프로젝트가 그렇게 빌드하기 때문이다.

판정에 쓰지 않고 기록만 한 값이 둘 있다. 설정 없는 매퍼의 결과와 생성자 파라미터 이름이 바이트코드에 남았는지다. 결과가 주장과 다를 때 왜 다른지를 가르려고 넣었다.

재현은 proofs/jackson-class-constructor-detection/run.sh 하나다. Docker 안에서 JDK 21·Gradle 8.14.3으로 돈다.


결과 — 두 Boot 모두 성공했고 설정 없는 Jackson 2 매퍼만 실패했다

run.sh가 쓴 원본 출력이다.

PROBE boot=3.5.16 jackson=2.21.4 java=21.0.9 params=true spring=ok:mouse,3 plain=fail:InvalidDefinitionException
PROBE boot=4.1.1 jackson=3.1.5 java=21.0.9 params=true spring=ok:mouse,3 plain=ok:mouse,3

판정은 refuted다. Boot가 준 매퍼는 두 버전 모두 @JsonCreator 없이 2인자 생성자로 값을 채웠다.

jackson=2.21.4는 처음에 이상해 보였다. Boot 3.5.0은 Jackson 2.19를 쓰기 때문이다. Boot 3.5.16의 spring-boot-dependencies pom을 열어보니 jackson-bom.version이 2.21.4였다. 패치 버전이 올라가며 Jackson도 올라간 것이고, 실험이 엉뚱한 버전을 부른 것이 아니다.


예상과 달랐던 점 — 표준의 문장은 설정 없는 Jackson 2 매퍼에서만 맞았다

plain 열이 답을 준다. Jackson 2를 설정 없이 쓰면 InvalidDefinitionException으로 실패한다. 표준 글의 문장은 이 조건에서는 참이다.

Boot 3.5에서 달라지는 이유로 확인한 것은 둘이다.

  • params=true — Boot Gradle 플러그인이 -parameters로 컴파일해 생성자 파라미터 이름이 남았다. Boot 소스의 JavaPluginAction에 그 인자가 있다(4.1 소스로 확인. 3.5는 출력값으로만 확인)
  • spring-boot-starter-json 3.5.16의 pom이 jackson-module-parameter-names를 끌어온다

이름이 남아 있고 그 이름을 읽는 모듈이 있으니 Jackson이 생성자 인자를 JSON 필드에 짝지을 수 있었다. Boot가 매퍼에 모듈을 등록하는 경로 자체는 확인하지 않았다. 결과가 그 설명과 맞을 뿐이다.

Boot 4.1에서는 설정 없는 Jackson 3 매퍼도 성공했다. 파라미터 이름 지원이 Jackson 3 기본에 들어간 것으로 보이지만 이것도 확인하지 않았다.

반증된 범위도 적어둔다. 무너진 것은 「명시하지 않으면 안 된다」는 필요성이다. 「@JsonCreator를 붙이면 그 생성자로만 만든다」와 「record는 자동이다」는 이 실험이 재지 않았다.


이 결과가 표준에 남기는 것 — 규칙은 그대로 두고 모순 이슈로 넘긴다

표준을 증명하는 방식 원칙 8대로 규칙을 바로 고치지 않았다. 표준 글 원칙 3에 > 반증: 줄을 달고 kind: 모순 이슈를 열었다.

이슈에서 따질 것은 @JsonCreator를 계속 붙일 이유가 필요성 말고도 남는가다. 생성자가 둘 이상일 때 어느 것을 쓸지, 컴파일 옵션이 빠진 빌드에서 조용히 실패하는지 같은 것들이다. 이유가 남으면 문장만 고치고, 없으면 규칙을 고친다.


정리

  • Boot가 준 매퍼는 3.5.16과 4.1.1 모두 @JsonCreator 없이 2인자 생성자로 역직렬화했다
  • 표준의 문장은 설정 없는 Jackson 2 매퍼에서만 참이었다. -parameters와 파라미터 이름 모듈이 차이를 만든 것으로 보인다 — 등록 경로는 확인하지 않았다
  • 반증된 것은 「명시해야 한다」는 필요성이다. 명시했을 때의 동작은 재지 않았다
  • 규칙은 모순 이슈에서 따진 뒤에 고친다

자신만의 철학을 만들어가는 중입니다.
최상단으로 이동했습니다!
확대 이미지

댓글남기기