배경
이번에 작업하던 코드에서 아래와 같은 에러가 발생했다.
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.frog.travelwithme.domain.buddyrecuirtment.controller.dto.BuddyDto$PostRecruitment`
(no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1904)
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1349)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1415)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:352)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4674)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3682)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:380)
... 150 more
테스트 코드를 통합 테스트를 진행하던 중 com.fasterxml.jackson.databind.exc.InvalidDefinitionException 예외가 발생했는데, 에러 내용을 살펴보면 기본 생성자를 생성할 수 없어서 Object를 역직렬화하지 못했다고 한다.
에러 내용을 기반으로 원인을 찾아보자
원인
Jackson
Jackson은 Java에서 객체를 JSON 문자열을 변환(역직렬화)하거나 JSON 문자열을 객체로 변환(직렬화)하는 기능을 제공하는 라이브러리이다.
만약 자바 객체에서 생성자가 없는 경우 Jackson에서 InvalidDefinitionException 예외를 발생한다.
BuddyRecruitmentIntegrationTest
class BuddyRecruitmentIntegrationTest extends BaseIntegrationTest {
...
@Test
@DisplayName("동행 작성 테스트")
void BuddyRecruitmentIntegrationTest1() throws Exception {
// given
CustomUserDetails userDetails = StubData.MockMember.getUserDetails();
TokenDto tokenDto = jwtTokenProvider.generateTokenDto(userDetails);
String accessToken = tokenDto.getAccessToken();
String refreshToken = tokenDto.getRefreshToken();
String encryptedRefreshToken = aes128Config.encryptAes(refreshToken);
BuddyDto.PostRecruitment postRecruitmentDto = StubData.MockBuddy.getPostRecruitment();
// when
String uri = UriComponentsBuilder.newInstance().path(BASE_URL)
.build().toUri().toString();
String json = ObjectMapperUtils.dtoToJsonString(postRecruitmentDto); // 여기서 발생한걸로 예상
...
}
}
에러 내용에서는 어디서 발생했는지 명시하지 않았지만 아마 BuddyDto.PostRecuirtment 객체를 역직렬화할 수 없다고 나와있기 때문에 ObjectMapperUtils.dtoToJsonString(postRecruitmentDto); 이 메서드를 수행하는 과정에서 발생한 것으로 보인다.
ObjectMapperUtils
public class ObjectMapperUtils {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static String dtoToJsonString(Object obj) throws JsonProcessingException {
return objectMapper.registerModule(new JavaTimeModule()).writeValueAsString(obj);
}
...
}
ObjectMapperUtils.dtoToJsonString() 메서드는 객체를 Json으로 역직렬화하는 작업을 수행한다. 해당 메서드에서는 별 문제가 없어 보인다.
BuddyDto.PostRecruiment 를 살펴보자
BuddyDto
public class BuddyDto {
@Getter
@Builder
@AllArgsConstructor
public static class PostRecruitment {
private final String title;
private final String content;
private final String travelNationality;
private final String travelStartDate;
private final String travelEndDate;
}
...
}
PostRecruitment 의 필드를 살펴보면 final을 사용하여 불변하도록 했다.
final 변수를 가지고 있는 클래스의 인스턴스를 생성할 때는 final 변수들의 값을 생성자에서 초기화 줘야 한다. 그래서 기본 생성자를 사용할 수 없다.
하지만 Jackson애서는 기본 생성자를 통해 JSON 데이터를 역직렬화한다. 그래서 에러가 발생했던 것이다.
해결
위 문제를 해결하기 위한 방법으로 2가지를 소개한다.
첫 번째 방법. final 제거
final을 제거하고 기본 생성자를 생성하는 방법이 있다. final을 제거하면 클래스 외부에서 변수의 값을 변경할 수 있기 때문에 @Setter을 사용하지 않고 @NoArgsConstructor(access = AccessLevel.PROTECTED)로 생성자 제한을 걸어서 외부로부터 최대한 보호하도록 했다.(생성자 제한을 Private로 둘 경우 Proxy를 생성할 수 없어서 에러가 발생한다.)
public class BuddyDto {
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public static class PostRecruitment {
private String title;
private String content;
private String travelNationality;
private String travelStartDate;
private String travelEndDate;
}
...
}
두 번째 방법. @JsonCreator, @JsonProperty 사용
만약 엄격하게 객체를 불변으로 두기 위해서 final을 유지하고 싶다면 @JsonCreator 와 @JsonProperty 애너테이션을 사용하여 final 변수에 값을 주입할 수 있다.
@JsonCreator 애너테이션은 Jackson이 역직렬화할 생성자를 지정해 준다.
@JsonProperty 애너테이션은 Jackson이 역직렬화할 JSON 데이터의 이름을 지정해 준다.
public class BuddyDto {
@Getter
@Builder
public static class PostRecruitment {
private final String title;
private final String content;
private final String travelNationality;
private final String travelStartDate;
private final String travelEndDate;
@JsonCreator
public PostRecruitment(@JsonProperty("title") String title,
@JsonProperty("content") String content,
@JsonProperty("travelNationality") String travelNationality,
@JsonProperty("travelStartDate") String travelStartDate,
@JsonProperty("travelEndDate") String travelEndDate) {
this.title = title;
this.content = content;
this.travelNationality = travelNationality;
this.travelStartDate = travelStartDate;
this.travelEndDate = travelEndDate;
}
}
...
}
하지만 @JsonProperty 애너테이션을 사용하면 코드 중복이 발생할 가능성이 있고, 코드를 더 복잡하게 만들기 때문에 첫 번째 방법으로 진행하게 되었다.
테스트
코드 수정 후 에러 없이 모든 테스트가 통과하는 것을 확인할 수 있다.
'나의 에러 일지' 카테고리의 다른 글
HTTPS - 이력서 제출했는데 내가 만든 서비스에 접속이 안되었던 건에 대하여 (0) | 2023.05.08 |
---|---|
Spring - 내 로컬에서만 java.io.FileNotFoundException이 발생할 때 원인과 해결 방법 (2) | 2023.05.07 |
Spring - Mac M1(ARM)에서 Embedded Redis를 실행하지 못하는 이유와 해결 방법 (0) | 2023.04.16 |
Java - Java 8 Local Date Time 직렬화/역직렬화 에러 원인과 해결 방법 (0) | 2023.04.11 |
Spring - non null key required 원인과 해결 방법 (0) | 2023.04.10 |