

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이 "이건 단일 값 객체네?"라고 판단하고, 특수한 역직렬화 경로를 시도하게 됩니다.
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)
public class PostRequest {
private String title;
public PostRequest() {
}
public PostRequest(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
단순하고 잘 작동합니다. 하지만 필드를 final로 선언할 수 없다는 단점이 있습니다.
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 필드를 유지하면서 역직렬화도 정상적으로 작동합니다.
public record PostRequest(String title) {
}
Java의 record는 데이터 중심 객체를 쉽고 명확하게 만들기 위한 문법입니다.
getter, equals, toString, hashCode 자동 생성final위 코드는 컴파일 시 다음과 같이 변환됩니다 (대략적으로):
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() 아님) => 기억 하기!!!| 제약 사항 | 설명 |
|---|---|
final 클래스 | 다른 클래스가 상속 불가 |
| 필드 수정 불가 | 모든 필드는 final이며 불변 |
명시적 extends 불가 | extends는 자동으로 java.lang.Record를 상속함 |
abstract 또는 interface 구현 불가 | 로직 중심보다는 데이터 중심용 구조 |
toString, equals, hashCode 모두 자동public record PostRequest(String title, String content) {
}
→ getter는 title()과 content()로 자동 생성됩니다.
Spring에서 DTO를 설계할 때, Jackson의 동작 방식을 이해하고 상황에 맞는 방법을 선택하는 것이 중요합니다.
특히 record를 적극 활용하면 코드도 간결해지고, 불변성도 보장할 수 있어 추천드립니다!