Spring Cache를 적용하던 중
Error occurs java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.evertrip.post.dto.response.PostResponseDto
에러가 발생했습니다.
위의 블로그 작성자분이 친절하게 해당 에러에 대한 풀이를 해주셨습니다. 참고하여 정리해보도록 하겠습니다.
Redis에 객체(DTO)를 저장할 때 Serializer를 통해 직렬화를 해주어야 합니다. (직렬화 = 자바 -> json 같은 타입)
이 때 여러가지 직렬화 방법을 설정해줄 수 있습니다.
해당 Serializer는 Class Type을 지정해야 하며, Redis에 객체를 저장할 때 class 값 대신 Classy Type 값을 JSON 형태로 저장한다고 합니다.
Class Type을 지정해야 하기 때문에 특정 클래스에 종속적이게 되고, RedisTemplate을 여러 쓰레드에서 접근하게 될 시 serializer 타입의 문제가 발생하는 경우가 있다고 합니다.
해당 Serializer는 String 값을 그대로 저장합니다. JSON 형태로 직접 encoding, decoding 해줘야한다는 단점이 있지만 Class Type을 지정할 필요가 없고 쓰레드 간의 문제가 발생하지 않습니다.
해당 Serializer는 객체의 클래스 지정 없이 모든 Class Type을 JSON 형태로 저장할 수 있는 Serializer입니다.
Class Type에 상관 없이 모든 객체를 직렬화해준다는 장점이 있지만, Object의 class 및 package까지 전부 함께 저장하게 되어 다른 프로젝트에서 redis에 저장되어 있는 값을 사용하려면 package까지 일치시켜줘야합니다.
따라서 MSA 구조의 프로젝트 같은 경우 문제가 생길 수 있습니다.
현재 에러가 난 상황에서의 cacheManager 설정입니다. Jackson2JsonRedisSerializer
에 Class type이 Object로 설정되어 있는 것을 확인하실 수 있습니다.
이는 Redis에 값을 저장하고 이후 요청에서 캐싱 데이터를 조회할 때 값에 저장된 JSON 타입을 PostResponseDto 형태로 역직렬화하여야 하는데 해당 Jackson2JsonRedisSerializer
에서 Object 타입으로 역직렬화하면서 발생되는 에러라 추측이 됩니다.
그래서 Object 타입이 아닌 PostResponseDto 타입으로 설정해주었습니다.
위와 같이 설정한 후 로직을 실행해보았습니다.
첫 번째 조회 요청 시 실제 DB에 해당 데이터를 조회한 후 캐싱 데이터를 Redis에 저장하는 로직이 정상적으로 동작하였고 두 번째 조회 요청 시 Redis에 해당 데이터가 존재할 시 읽어오는 작업 또한 정상적으로 동작이 되었습니다.
두 번째 시도 방법은 Custom ObjectMapper와 GenericJackson2JsonRedisSerializer를 이용해보았습니다.
ObjectMapper의 설정 중 enableDefaultTyping()
는 직렬화 시 type 정보를 저장할 scope를 지정합니다.
여기서는 non-final 클래스들에 대해 타입 정보를 저장할 수 있도록 했습니다.(GenericJackson2JsonRedisSerializer의 기본 동작 방식)
enableDefaultTyping()
는 2.10 버전 이후로 Deprecated 됐으며, activateDefaultTyping()
을 사용하여 코드를 수정하였습니다.
해당 설정을 이용하여 조회 로직을 실행해보았습니다.
첫 번째 조회 로직을 실행한 후 Redis에 저장된 캐시 데이터 입니다.
Jackson2JsonRedisSerializer
과의 차이를 보시면 캐싱이 된 데이터의 정보에 Class Type이 저장되어 있는 것을 볼 수 있습니다.
두 번째 조회 로직을 실행해보면
실제 DB에 조회 쿼리가 날라가지 않고 Redis에 저장된 캐시 데이터를 정상적으로 조회하는 것을 확인할 수 있습니다.
두 가지 해결 방안 중 본인의 상황에 알맞는 방법을 택하여서 해결하면 될 것 같습니다.
저는 여러가지 캐싱 데이터 형태가 존재하기도 하고 Object의 class 및 package까지 전부 함께 저장하게 되어도 상관이 없는 아키텍처 구조이기 때문에 GenericJackson2JsonRedisSerializer
를 사용하기로 결정하였습니다.
참고 자료
bagt13님의 Velog