lombok의 @Builder를 추가했더니 잭슨에서 에러가?
문제 발생
한가로운 오후 개발환경에 api 응답 에러가 난다고 문의가 들어왔습니다.
ES에 에러 로그를 확인해보니 이러한 에러 로그가.. 줄줄..
(실제 환경을 포스팅할 수 없어 예제 코드를 작성하여 에러를 유도함)
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `com.example.lomboktest.SampleDto` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator);
nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.example.lomboktest.SampleDto` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)<EOL> at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 2, column: 5]]
문제가 발생한 dto를 확인해봅시다.
@Getter
public class SampleDto {
@Builder.Default
private String test = "tesT";
@Builder
public SampleDto(String test) {
this.test = test;
}
}
음.. 생성자를 선언하고 빌더를 선언하였는데 무엇이 문제일까?
에러 로그를 자세히 확인해봅시다.
JSON parse error:
Cannot construct instance of `com.example.lomboktest.SampleDto`
(although at least one Creator exists):
cannot deserialize from Object value (no delegate- or property-based Creator);
직역하자면, JSON 파싱 에러 SampleDto를 생성할 수 없다. (최소 한개의 생성자가 존재하지만)
객체값으로부터 역직렬화를 할 수 없다. (delegate 또는 property 기반의 생성자가 없음)
사실 딱 보자마자 알아봤다. 하지만 지식은 output을 해야 오래 기억에 남는법.. 그래서 정리하며 포스팅한다.
생성자가 존재하는데, 왜?
눈치채신분들은 아시겠지만, spring은 기본적으로 직렬화/역직렬화에 잭슨 라이브러리를 기본으로 사용하고 있고 그 잭슨 라이브러리는 기본생성자가 없으면 동작하지 않습니다.
자, 에러 메세지를 출력한 클래스를 추적해봅시다.
protected Object deserializeFromObjectUsingNonDefault(JsonParser p,
DeserializationContext ctxt) throws IOException
{
final JsonDeserializer<Object> delegateDeser = _delegateDeserializer();
if (delegateDeser != null) {
final Object bean = _valueInstantiator.createUsingDelegate(ctxt,
delegateDeser.deserialize(p, ctxt));
if (_injectables != null) {
injectValues(ctxt, bean);
}
return bean;
}
if (_propertyBasedCreator != null) {
return _deserializeUsingPropertyBased(p, ctxt);
}
// 25-Jan-2017, tatu: We do not actually support use of Creators for non-static
// inner classes -- with one and only one exception; that of default constructor!
// -- so let's indicate it
Class<?> raw = _beanType.getRawClass();
if (ClassUtil.isNonStaticInnerClass(raw)) {
return ctxt.handleMissingInstantiator(raw, null, p,
"non-static inner classes like this can only by instantiated using default, no-argument constructor");
}
return ctxt.handleMissingInstantiator(raw, getValueInstantiator(), p,
"cannot deserialize from Object value (no delegate- or property-based Creator)");
}
에러로그를 전달하는 메서드 이름이 deserializeFromObjectUsingNonDefault 요놈이다.
이름부터 심상치않다.
좀더 찾아보자.
/*
/**********************************************************
/* Public API implementation; instantiation from JSON Object
/**********************************************************
*/
@Override
public Object createUsingDefault(DeserializationContext ctxt) throws IOException
{
if (_defaultCreator == null) { // sanity-check; caller should check
return super.createUsingDefault(ctxt);
}
try {
return _defaultCreator.call();
} catch (Exception e) { // 19-Apr-2017, tatu: Let's not catch Errors, just Exceptions
return ctxt.handleInstantiationProblem(_valueClass, null, rewrapCtorProblem(ctxt, e));
}
}
메서드를 꾸준히 따라가다보니 문제 발생 근원지를 찾을 수 있었다.
if (_defaultCreator == null) { // sanity-check; caller should check
이 부분이 바로 기본생성자가 null 인지 확인하고 null이면 super.createUsingDefault(ctxt); 메서드가 호출될 것이고 에러로그가 출력된 곳으로 바인딩되어 실제 에러로그가 출력될 것입니다.
반대로 기본생성자가 있다면 호출되는 _defaultCreator.call(); 를 들어가보면 기본생성자를 사용하는지 살펴보면 확실히 알 수 있을 것 같네요.
@Override
public final Object call() throws Exception {
// 31-Mar-2021, tatu: Note! This is faster than calling without arguments
// because JDK in its wisdom would otherwise allocate `new Object[0]` to pass
return _constructor.newInstance((Object[]) null);
}
그렇다면 에러로그에 있던 no delegate- or property-based 는 뭐지?
우리가 지나쳐온 코드에 답이 존재합니다.
위의 코드 중 에러메세지 출력된 메서드를 확인해볼까요?
protected Object deserializeFromObjectUsingNonDefault(JsonParser p,
DeserializationContext ctxt) throws IOException
{
final JsonDeserializer<Object> delegateDeser = _delegateDeserializer();
if (delegateDeser != null) {
final Object bean = _valueInstantiator.createUsingDelegate(ctxt,
delegateDeser.deserialize(p, ctxt));
if (_injectables != null) {
injectValues(ctxt, bean);
}
return bean;
}
if (_propertyBasedCreator != null) {
return _deserializeUsingPropertyBased(p, ctxt);
}
// 25-Jan-2017, tatu: We do not actually support use of Creators for non-static
// inner classes -- with one and only one exception; that of default constructor!
// -- so let's indicate it
Class<?> raw = _beanType.getRawClass();
if (ClassUtil.isNonStaticInnerClass(raw)) {
return ctxt.handleMissingInstantiator(raw, null, p,
"non-static inner classes like this can only by instantiated using default, no-argument constructor");
}
return ctxt.handleMissingInstantiator(raw, getValueInstantiator(), p,
"cannot deserialize from Object value (no delegate- or property-based Creator)");
}
deserializeFromObjectUsingNonDefault 메서드명을 살펴보면 기본생성자 없이 역직렬화하는 메서드입니다.
이 메서드가 탔다면 바로 에러를 찍어야하는데 세부 로직을 보면
final JsonDeserializer<Object> delegateDeser = _delegateDeserializer();
if (delegateDeser != null) {
final Object bean = _valueInstantiator.createUsingDelegate(ctxt,
delegateDeser.deserialize(p, ctxt));
if (_injectables != null) {
injectValues(ctxt, bean);
}
return bean;
}
delegateDeser != null delegate가 존재한다면 createUsingDelegate 메서드를 통해 객체를 생성하는 로직이 들어가 있습니다.
지금 저의 상황은 delegateDeser == null 이기 때문에 마지막 구문의 에러로그가 출력된 것을 볼 수 있습니다.
또한 그 밑의 로직인
if (_propertyBasedCreator != null) {
return _deserializeUsingPropertyBased(p, ctxt);
}
_propertyBasedCreator 도 아니기 때문에 저런 에러가 찍힌것입니다.
정리
결국, 저의 문제는 Controller에서 API 요청 스펙을 전달하는 DTO의 객체로써 기본생성자가 없었기에 나타난 문제였습니다.
간단히 기본생성자를 추가해서 에러를 해결할 수 있었습니다.
롬복의 @builder는 기본생성자를 생성해주지 않기에 이런 상황이 발생하였습니다.. 😱
우리 모두 하나만 기억합시다.
잭슨의 object mapper는 기본생성자를 필요로한다! (기본생성자는 private 이어도 된다! 리플렉션)
번외로 @JsonProperty, @JsonAutoDetect 등을 사용하는 프로퍼티 기반 객체 or 생성자를 위임한 경우라면 필요하지 않다!
끝.
댓글남기기