[Redis] cannot deserialize from Object value (SerializationException)
Springboot Cache를 적용하고자 할 때 발생가능한 오류입니다.
- 저는 redis cache 사용했습니다.
- 또한 캐시 value에 대해서 GenericJackson2JsonRedisSerializer를 적용했습니다.
서버에 캐시를 적용했는데 만약 이런 에러를 만났다면, jackson의 역직렬화에 대해 생각해봅시다.
캐시된 응답을 올바른 객체로 내보내야 하는데 역직렬화할때 쓸 생성자가 없다면
cannot deserialize from Object value를 로그에서 만날 수 있습니다.
*생성자의 존재 문제가 아닌, 역직렬화하기 적절한 생성자를 두는 것이 핵심입니다.
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `org.springframework.http.ResponseEntity` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
일단 응답 DTO를 직렬화해서 캐시를 최초 생성해둘 때는 문제가 되지 않았을 것입니다.
(redis-cli에서 보면, 캐싱 데이터가 담긴 것을 확인할 수 있었습니다.)
하지만 캐싱된 값을 역직렬화하기 위한 장치 또한 잘 해뒀어야 합니다.
■ 해법1
아주 기본 세팅!
Dto에 기본생성자(@NoArgsConstructor)를 둬야 합니다.
■ 해법2
"생성자를 잘 처리하면" 됩니다.
다만 jackson이 직렬화/역직렬화 과정에서 모두 이해할 수 있는 형태여야 합니다.
만약 생성자에 특별한 로직이 없고, 단지 기본타입의 필드들이 파라미터를 이루고 있다면 별 문제 없을 것입니다. 하지만 만일 파라미터에 Entity를 그대로 넣었거나 어떤 복잡한 타입이 들어가 있다면 문제가 될 수 있습니다.
jackson 입장이 되어봅시다. Dto에 복잡한 타입이 있다면 직렬화/역직렬화 정책을 예측하지 못할 수 있습니다. 예를들어 LocalDateTime이 DTO 필드에 있다면, 직렬화할 때 예상치 못한 형태로 저장됩니다. 역직렬화하는 방법이 항상 직렬화 방법과 동일하지는 않기 때문에, 직렬화한 것을 역직렬화해내지 못할 수도 있는 것이죠. 솔루션으로 @JsonFormat 을 사용하거나 커스텀 직렬화기를 둘 수도 있고, springboot 2.x에서는 @JsonDeserialize(using = LocalDateTimeDeserializer.class), @JsonSerialize(using = LocalDateTimeSerializer.calss)로 해결할 수 있다고 합니다. (LocalDateTime 역직렬화/직렬화 문제해결 참고)
또한 Dto 생성자 파라미터가 '복잡한 타입'이면 역직렬화할 때 매핑이 안 됩니다.
파라미터에 Entity를 넣은 경우를 생각해봅시다. jackson은 Entity를 이해할 수 없습니다. 제가 만약 Entity를 파라미터에 넣어서 entity.getName() 이런식으로 값들을 뽑아 응답 Dto를 만들어내고 그 Dto를 캐싱했다고 가정합시다.
캐싱된 값을 역직렬화할 때 또다시 이 생성자를 사용하게 될 것입니다. 그런데 파라미터가 Entity네요? jackson은 DTO 필드의 나열을 어떤 Entity 객체로 자동 매핑하는 기능이 없습니다. 매핑 가능하도록 도와줘야 합니다.
단순한 해결책은 파라미터에 Entity를 받는게 아니라, 매핑이 쉬운 필드타입 파라미터로 풀어주는 것입니다.
■ 추가설명
해결방안 중 @JsonCreator와 @JsonParameter가 있습니다.
생성자에 @JsonCreator를 붙여주고, 파라미터에 @JsonParameter("keyName") 을 붙여줘서 jackson의 json 매핑을 확실하게 돕는 것이죠. 만약 키 이름이 잘못될 경우 null이 들어갑니다.
어떤 파라미터에 @JsonParameter("keyName")을 지정했는데 실제로 그 값이 없어서 null이 들어온다면, 단순히는 해당 Dto 필드에 null값이 들어가겠습니다. 만약 해당 필드가 객체였고 생성자 내에서 객체를 참조하고 있다면, Dto가 만들어지는 과정에서 NPE가 날 것입니다.
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `com.example.demo.XxxxxDto`, problem: `java.lang.NullPointerException`
아무튼 @JsonCreator와 @JsonParameter의 핵심도 역시 Jackson이 그 생성자를 json으로 잘 매핑할 수 있게 명시하는 것입니다. 모두 같은 맥락인 것이죠.