Spring에서 DTO 필드가 1개일 때 기본 생성자가 필요한 이유 (feat. Record, @JsonCreator)

Chaaehyun·2025년 4월 21일

스프링 스터디

목록 보기
1/10
post-thumbnail

Spring에서 @RequestBody로 DTO를 받을 때, 필드가 하나인 DTO는 때때로 기본 생성자 없이는 역직렬화 오류가 발생합니다.

음.. 코드 상에서 실행 시, 오류 안나는데요?

당장 IntelliJ에서 오류가 나지는 않지만 메인 코드를 실행했을 때, title 값이 넘어오지 않는 경우가 발생합니다.
오류가 나지는 않지만, 값이 매핑되지 않는 현상은 실제로 꽤 흔하게 발생합니다.
이것은 Silent Failure (조용한 실패) 라고 부르며, Spring + Jackson 조합에서 아주 중요한 함정 중 하나입니다.

이런 경우, 오류가 나지 않으니 어디서 잘못된거야?! 하기 충분합니다.

지금부터 왜 이런 일이 발생하는지 차근차근 알아보겠습니다.



📌 문제 상황

아래와 같이 필드가 하나인 DTO를 정의했다고 가정해봅시다.

public class PostRequest {
    private final String title;

    public PostRequest(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }
}

클라이언트에서 아래와 같은 JSON을 전송했다고 하면,

{
  "title": "hello"
}

Spring 환경에서는 내부적으로 Jackson이 이 JSON을 PostRequest 객체로 변환합니다.

그런데 필드가 하나뿐이면 Jackson이 "이건 단일 값 객체네?"라고 판단하고, 특수한 역직렬화 경로를 시도하게 됩니다.



💡 필드가 2개 이상일 경우는 어떨까요?

public class PostRequest {
    private final String title;
    private final String content;

    public PostRequest(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public String getTitle() {
        return title;
    }

    public String getContent() {
        return content;
    }
}

이 경우에는 Jackson이 "복합 객체"로 판단하고, 필드명을 기반으로 알아서 매핑하기 때문에 기본 생성자 없이도 문제 없이 동작합니다.



⚠️ 여기서 잠깐! 오류가 발생할 수도 있어요.

다음과 같은 오류가 발생할 수도 있고, 발생하지 않고 값이 null로 들어가는 경우도 있습니다.

Jackson은 "title"이라는 필드를 어디에 넣어야 할지 모르기 때문에 다음과 같은 오류를 발생시킬 수 있습니다:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `PostRequest` (no Creators, like default constructor, exist)


☑️ 해결 방법 1: 기본 생성자 추가

public class PostRequest {
    private String title;

    public PostRequest() {
    }

    public PostRequest(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }
}

단순하고 잘 작동합니다. 하지만 필드를 final로 선언할 수 없다는 단점이 있습니다.



☑️ 해결 방법 2: @JsonCreator + @JsonProperty 사용

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class PostRequest {
    private final String title;

    @JsonCreator
    public PostRequest(@JsonProperty("title") String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }
}

Jackson에게 "이 생성자를 사용해서 title을 주입하라"고 명시해줍니다.
final 필드를 유지하면서 역직렬화도 정상적으로 작동합니다.



✅ 해결 방법 3: Java Record 사용 (Java 16+) => 추천 방법!!

public record PostRequest(String title) {
}

Java의 record데이터 중심 객체를 쉽고 명확하게 만들기 위한 문법입니다.

  • getter, equals, toString, hashCode 자동 생성
  • 모든 필드는 final
  • 기본 생성자 없이도 Jackson이 잘 작동

🧱 record가 자동으로 만들어주는 것

위 코드는 컴파일 시 다음과 같이 변환됩니다 (대략적으로):

public final class PostRequest extends java.lang.Record {
    private final String title;

    public PostRequest(String title) {
        this.title = title;
    }

    public String title() {
        return title;
    }

    @Override
    public boolean equals(Object o) {
        // 필드 값 기반 equals
    }

    @Override
    public int hashCode() {
        // 필드 값 기반 hashCode
    }

    @Override
    public String toString() {
        return "PostRequest[title=" + title + "]";
    }
}

⚙️ 작동 원리

  • 모든 필드는 private final로 선언됨 → 불변성 보장
  • 생성자는 모든 필드를 초기화하도록 자동 생성됨
  • 각 필드에 대해 getter가 자동 생성되며, 메서드 이름은 필드명과 동일
    (예: title()getTitle() 아님) => 기억 하기!!!

🔒 record의 제약 조건

제약 사항설명
final 클래스다른 클래스가 상속 불가
필드 수정 불가모든 필드는 final이며 불변
명시적 extends 불가extends는 자동으로 java.lang.Record를 상속함
abstract 또는 interface 구현 불가로직 중심보다는 데이터 중심용 구조

🧠 record를 DTO로 쓸 때 장점

  • 코드 간결성: 생성자, getter, toString, equals, hashCode 모두 자동
  • 불변성 유지: 데이터 객체로 이상적
  • 직렬화 매핑 간편: Jackson 등 라이브러리에서 생성자 기반 주입 가능
  • DTO 용도로 완벽하게 적합

❓ 예시: 필드 2개일 때 record 사용 방법

public record PostRequest(String title, String content) {
}

→ getter는 title()content()로 자동 생성됩니다.



✨ 마무리

Spring에서 DTO를 설계할 때, Jackson의 동작 방식을 이해하고 상황에 맞는 방법을 선택하는 것이 중요합니다.
특히 record를 적극 활용하면 코드도 간결해지고, 불변성도 보장할 수 있어 추천드립니다!

profile
세상을 사랑하는 컴퓨터학부생입니다 :>

0개의 댓글