로컬 캐시를 사용하던 프로젝트에서 캐시 동기화 이슈와 캐시 히트율을 높히기 위해 캐시 매니저를 Caffeine에서 Redis 로 전환해야했다.
전환하면서 겪은 역직렬화시 클래스 데이터 타입으로 인한 이슈와 캐시되어있던 데이터에 필드가 추가되면서 겪었던 ObjectMapper 설정 이슈에 대해 정리해보자.
장점
단점
장점
단점
장점
단점:
RedisTemplate<String, Object>
를 사용하는 경우, A 스레드에서는 A 데이터 타입으로 Serializer 를 등록했지만, B 스레드에서 B 데이터 타입으로 등록해버려 역직렬화에 실패할 수 있다.)유연성을 고려하여 Jackson2JsonRedisSerializer 을 선택했다.
private RedisCacheConfiguration createCache(CacheType cacheType) {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.of(cacheType.getExpireAfterWrite(), cacheType.getExpireTimeUnit()))
.serializeKeysWith(fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(fromSerializer(new Jackson2JsonRedisSerializer<>(cacheType.getClazz())))
.disableCachingNullValues();
}
}
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
커스텀 ObjectMapper 에 LocalDateTime 을 역직렬화 할 수 있도록 설정을 추가해주었다.
private RedisCacheConfiguration createCache(CacheType cacheType) {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.of(cacheType.getExpireAfterWrite(), cacheType.getExpireTimeUnit()))
.serializeKeysWith(fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(fromSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, cacheType.getClazz())))
.disableCachingNullValues();
}
해치웠나...
요구사항으로 인해 캐시되어있는 데이터에 필드가 추가되자 에러 알림이 발생했다.
ObjectMapper 의 FAIL_ON_UNKNOWN_PROPERTIES
기본 설정은 true 이므로 데이터 타입에 정의되지 않은 필드가 조회한 데이터에 있는 경우 위의 에러가 발생한다.
새로운 필드가 추가된 체로 데이터가 저장된거면 배포된 서버에서도 새로운 필드가 추가되어있을텐데, 왜 에러가 발생했을까?! 이유는 우리 배포 방법이 블루 / 그린 배포 방식이면서 25프로씩 트래픽을 전환하고 있기 때문이다. 새버전에서 추가된 job 필드로 인해 해당 필드가 추가된 데이터가 캐싱되지만, 구버전에서는 job 필드가 정의되지 않은 필드이기 때문에 역직렬화시에 에러가 발생한다.
배포 전에 FAIL_ON_UNKNOWN_PROPERTIES
를 false 로 설정 후 적용했다면 에러가 발생하지 않았을 것이다. 추후에 이 히스토리를 모르고 필드를 추가할 누군가를 위해 위 설정은 적용하자!
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(createCaches());
return cacheManager;
}
private List<CaffeineCache> createCaches() {
return Arrays.stream(CacheType.values())
.map(this::createCache)
.collect(Collectors.toList());
}
private CaffeineCache createCache(CacheType cacheType) {
return new CaffeineCache(cacheType.getCacheName(),
Caffeine.newBuilder()
.recordStats()
.expireAfterWrite(cacheType.getExpireAfterWrite(), cacheType.getExpireTimeUnit())
.maximumSize(cacheType.getMaximumSize())
.build()
);
}
}
@EnableCaching
@Configuration
@RequiredArgsConstructor
public class CacheConfig {
private final ObjectMapper objectMapper;
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.withInitialCacheConfigurations(createCaches())
.enableStatistics()
.build();
}
private Map<String, RedisCacheConfiguration> createCaches() {
return Arrays.stream(CacheType.values())
.collect(Collectors.toMap(
CacheType::getCacheName,
this::createCache
));
}
private RedisCacheConfiguration createCache(CacheType cacheType) {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.of(cacheType.getExpireAfterWrite(), cacheType.getExpireTimeUnit()))
.serializeKeysWith(fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(fromSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, cacheType.getClazz())))
.disableCachingNullValues();
}
}
@CacheConfig(cacheNames={"user"})
public class UserService {
클래스 단위로 캐시 키가 같다면, @CacheConfig
으로 간단하게 설정할 수 있다. 캐시 키가 개발 중간에 변경되는 경우, @Cacheable
, @CacheEvict
등등 모두 찾아다니면서 변경해줘야 하는데 @CacheConfig
를 사용한다면 클래스 단위로 변경만 하면 된다.
KEYS 명령어로 조회 후에 DEL 로 데이터 삭제를 하게된다. KEYS 는 블로킹 명령어로 운영환경에서 피해야하는데 @CacheEvict
시에 논블로킹 명령어인 SCAN 을 사용하도록 변경할 수 있다.
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.RedisCacheManagerBuilder
.fromCacheWriter(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory, BatchStrategies.scan(1000))) // 🔥🔥🔥 BatchStrategies 기본값인 KEYS 를 SCAN 으로 변경
.withInitialCacheConfigurations(cacheConfigs())
.enableStatistics()
.build();
}
@CacheConfig
로 키를 클래스 단위에서 관리할 수 있다.