문제
치매 환자 돌봄 서비스를 개발하던 중, 프론트엔드 개발자로부터 디바이스 토큰 저장 API 호출 시 500 Internal Server Error가 발생한다는 이슈를 전달받았습니다.

해당 API는 단순히 클라이언트로부터 전달받은 디바이스 토큰을 Redis에 저장하는 기능으로, 로직 자체는 복잡하지 않았습니다.
에러 메시지를 확인한 결과 JSON 데이터를 NotificationRequestDto.DeviceToken 객체로 역직렬화하는 과정에서 에러가 발생하고 있었습니다.

JSON parse error: Cannot construct instance of `kr.co.onehunnit.onhunnit.dto.notification.NotificationRequestDto$DeviceToken` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
해당 에러와 관련된 코드는 아래와 같습니다.
@RequiredArgsConstructor
@RestController
@RequestMapping("/notifications")
public class NotificationController {
private final NotificationService notificationService;
@PostMapping("/device-token")
public ResponseDto<String> getDeviceToken(HttpServletRequest request, @RequestBody NotificationRequestDto.DeviceToken requestDto) {
return ResponseUtil.SUCCESS("디바이스 토큰 저장에 성공하였습니다.", notificationService.saveDeviceToken(request.getHeader("Authorization"), requestDto));
}
}
public class NotificationRequestDto {
@Getter
@Builder
public static class DeviceToken {
private String deviceToken;
}
}
겉보기엔 전혀 문제가 없어 보였고, 이전에도 이와 같은 방식으로 DTO를 작성해왔기에 문제가 없을 것이라 생각했습니다.
다른 DTO 클래스에서는 문제가 없는데, DeviceToken 클래스에서만 역직렬화 에러가 발생하고 있습니다.
JSON 역직렬화 과정
Jackson은 내부적으로 ObjectMapper를 통해 JSON → Java 객체 변환을 수행할 때 Java Reflection 패키지를 사용합니다. 이때 객체 생성을 위해 기본 생성자를 필요로 하고, 생성된 객체에 필드를 주입하는 방식으로 역직렬화를 진행합니다.
이는 또 다른 의문으로 이어졌습니다.
"지금까지도 기본 생성자 없이 잘 동작했는데, 왜 이번에는 에러가 발생했을까?"
그동안 문제가 발생하지 않았던 이유는 Jackson 라이브러리가 제공하는 'jackson-module-parameter-names' 모듈이 기본 생성자가 없어도 역직렬화를 가능하게 도와주었기 때문입니다.
Jackson은 다음과 같은 시나리오로 역직렬화가 수행되게 합니다.
- @Builder는 모든 필드를 인자로 받는 package-private 생성자를 생성
- jackson-module-parameter-names 모듈이 해당 생성자의 매개변수 이름을 인식
- JSON의 키와 생성자의 파라미터 이름을 매핑하여 객체를 생성
즉, 기본 생성자가 없더라도 파라미터 기반 생성자가 존재하고, 매개변수 이름이 JSON 키와 일치한다면 역직렬화를 문제없이 수행할 수 있습니다.
단! 필드가 1개인 DTO는 기본 생성자가 없으면 역직렬화가 불가능하다고 합니다.
(이에 대한 원인은 다음 글에서 다룰 예정입니다.)
해결 시도
DeviceToken 클래스는 필드가 1개이기 때문에 기본 생성자를 생성하기 위해 @NoArgsConstructor를 추가했습니다.
public class NotificationRequestDto {
@Getter
@Builder
@NoArgsConstructor
public static class DeviceToken {
private String deviceToken;
}
}
그런데 @NoArgsConstructor를 추가하면 다음과 같은 컴파일 오류가 발생합니다.

Lombok 공식 문서를 보면, @Builder는 클래스 레벨에 적용될 경우 @AllArgsConstructor(내부적으로 모든 필드를 매개변수로 받는 생성자 생성)와 같은 기능을 내포합니다. 하지만 @NoArgsConstructor를 함께 사용하면 기본 생성자를 생성하고, @Builder는 이미 생성자가 있으니 @Builder가 필요로 하는 모든 필드를 받는 생성자를 생성하지 않게 됩니다. 따라서 @Builder는 내부적으로 사용할 생성자를 찾을 수 없게 되어 컴파일 오류가 발생합니다.

해결: @AllArgsConstructor 추가
위 문제는 다음과 같이 해결할 수 있습니다. 모든 필드를 받는 생성자(@AllArgsConstructor)를 추가해 주면 됩니다.
public class NotificationRequestDto {
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class DeviceToken {
private String deviceToken;
}
}
결과

디바이스 토큰 저장 API가 잘 동작되고 있는 것을 확인할 수 있습니다.
다음 과제
위에서 언급한 필드가 1개인 DTO는 기본 생성자가 없으면 역직렬화가 불가능한 이유를 공부하고자 합니다.
참고자료
@Builder
projectlombok.org
[Spring] 2. jackson을 이용한 data binding 이해하기(생성자, constructor)
저번 편에서는 getter/setter 에 대한 직렬화, 역직렬화가 어떻게 이루어지는지 알아보았습니다. 관련 내용을 아직 보지 않으셨다면 먼저 저번 편 글을 보시고 이번 게시글을 읽으시는 것을 추천드
beaniejoy.tistory.com
[싸피셜이 알려드림: 기술편] @Builder의 역직렬화 동작 원리 파헤치기
안녕하세요, 여러분! 👋 싸피 12기 싸피셜 기자단 안수진입니다! 저는 백엔드 개발자를 희망하고 있다는거 기억하고 계시나요?백엔드 개발을 공부하다 보면 종종 "어? 이게 왜 되지? 🤔" 하는 순
velog.io
'트러블슈팅' 카테고리의 다른 글
| JSON 역직렬화 문제 해결하기2 (JSON Parse error) (0) | 2025.05.14 |
|---|---|
| 순환 참조(Circular Reference) 문제 해결하기 (0) | 2024.09.03 |
| TimeZone 불일치 문제 해결하기(UTC, KST) (0) | 2024.08.31 |
| Signature expired: is now earlier than 에러 (0) | 2024.05.08 |
| CRLF, LF? (0) | 2024.03.09 |
느리더라도 단단하게 성장하고자 합니다!
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!