Redis를 활용한 캐시 사용시 주의점
<개요>
- 기본생성자 유무
- Serializer / Deserializer 적용
- 클래스 패키지 위치
<내용>
Q1. 기본생성자 사용
Page<DataDto> findDataAll (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
Pageable pageable) {
if(loginUserDetails.isAdmin()){
return dataService.findAllDatas();
}
return new PageImpl(new ArrayList(),Pageable.unpaged(),0);
}
- Paging처리를 위해서 JPA에서 제공되고 있는 Pageable를 사용하고 있었다.
- 여기에 Redis를 저장소로 하여 캐시를 적용하면 다음과 같은 형태로 사용이 가능하다.
@Cacheable(value="findDataAll", key="#pageable.pageSize.toString().concat('-').concat(#pageable.pageNumber)")
- 당연히 잘 될것으로 예상하고 수행하였으나 다음과 같은 Exception을 만나게 되었다.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot
construct instance of `org.springframework.data.domain.PageImpl` (no
Creators, like default construct, exist): cannot deserialize from Object
value (no delegate- or property-based Creator)
at [Source: (byte[])"
- 기본 생성자가 존재하지 않기 때문에 발생하는 오류로
A1. 기본 생성자를 가진 wrapper class를 활용하여 문제를 해결할 수 있다.
@JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"})
public class RestPage<T> extends PageImpl<T> {
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public RestPage(@JsonProperty("content") List<T> content,
@JsonProperty("number") int page,
@JsonProperty("size") int size,
@JsonProperty("totalElements") long total) {
super(content, PageRequest.of(page, size), total);
}
public RestPage(Page<T> page) {
super(page.getContent(), page.getPageable(), page.getTotalElements());
}
}
Q2. Page클래스는 다음과 같은 형태로 해결하였으나 Java 8 부터 제공하는 LocalDateTime에 대해서도 비슷한 오류가 발생한다.
기본적으로 LocalDataTime을 저장하면 "yyyy-MM-dd'T'HH:mm:ss" 와 같은 형태로 처리되기를 원하지만 아래와 같은 형태로 저장이 된다. 그래서 json 역직렬화를 할때 문제가 발생하게 된다.
{
"lastPartDate":{
"year":2020,
"month":"JANUARY",
"dayOfMonth":1,
"dayOfWeek":"WEDNESDAY",
"dayOfYear":1,
"monthValue":1,
"hour":0,
"minute":0,
"second":0,
"nano":0,
"chronology":{
"id":"ISO",
"calendarType":"iso8601"
}
}
}
A2. 역직렬화 방안
- Spring boot 1.x 대에서는 JSR-310을 지원하지 않기 때문에 dependency 에 모듈 추가가 필요하다.
- 참조사이트에 나와있는 것처럼 Custom Serializer를 구현하는 방법도 있으며,
- Spring boot 2.x 대를 사용중이면 다음과 같이 쉽게 처리를 할 수도 있다.
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime creationDateTime;
Q3. 혹시라도 Redis에 저장하는 Class의 패키지명등을 수정하면 아래 Exception 을 만나게 된다.
com.fasterxml.jackson.databind.exc.InvalidTypeIdException
이유는 우리가 Redis에 실제 저장된 값을 조회해보면 알 수 있는데 다음과 같다.
역직렬화에 필요한 클래스 정보가 같이 들어가있는데 기존 클래스와 다르기 때문에 에러가 발생한다.
A3. 해당 Cache를 evict해주도록 하자. 일반적으로 데이터가 변경되는 경우에 대해서는 Refresh를 해주지만 리팩토링작업중 클래스가 변경되었을 경우에 까먹는 케이스가 많다.
<정리>
- 정리를 해보면 결국 Serializer / Deserializer 에 대한 이야기이다. RedisCacheConfiguration설정을 보면 대부분 다음과 같다.
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
- 특히 Json내부의 상세타입을 다시 객체화 하기 위해서는
a. 저장되어 있는 클래스 정보가 일치하고,
b. 기본 생성자 (혹은 그 역할을 수행할 수 있는 무언가) 가 존재해야 하며,
c. Serialize한것과 같은 방법으로 Deserialize를 수행해야 한다.
- Redis외의 기타 저장소를 사용할때에도 동일한