[업무일지] 기본값이 JDK 직렬화였던 레디스 캐시 설정
거래내역 통계 조회에 레디스 캐시를 붙였는데, 응답 DTO 에
Serializable을 달아야 컴파일이 됐습니다. 그게 이상해서 파봤더니RedisCacheConfiguration.defaultCacheConfig()가 JDK 직렬화를 쓰고 있었습니다. 직렬화 방식과 TTL 을 명시하는 쪽으로 바꿨습니다.
거래내역 통계 API 가 무거웠습니다. 사용자별로 집계 쿼리가 몇 개 돌고, 값이 자주 바뀌지도 않아서 캐시를 붙였습니다. Java 21, Spring Boot 3.2.x 에 spring-boot-starter-data-redis 를 쓰는 환경입니다.
1
2
3
4
5
// Java 21, Spring Boot 3.2.x, spring-boot-starter-data-redis
@Cacheable(cacheNames = "paymentStatistics", key = "'userId:' + #userId")
public PaymentStatisticsResponse getStatistics(Long userId) {
...
}
여기까진 금방 됐습니다. 그런데 응답 DTO 에서 막혔습니다.
Serializable 을 달아야 돌아갔다
캐시에 넣는 순간 예외가 났고, implements Serializable 을 붙이니 됐습니다.
1
public class PaymentStatisticsResponse implements Serializable {
붙이고 넘어갔는데 이게 계속 걸렸습니다. JSON 으로 나가는 응답 DTO 인데 왜 자바 직렬화 인터페이스가 필요한가 싶었거든요.
defaultCacheConfig 가 뭘 하고 있었나
설정은 이 한 줄이었습니다.
1
2
3
4
@Bean
public RedisCacheConfiguration defaultCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig();
}
이 기본값이 값 직렬화에 RedisSerializer.java() 를 씁니다. 자바 기본 직렬화입니다. 그래서 Serializable 을 요구했던 거고요. TTL 은 Duration.ZERO 라 만료가 없습니다. 한 번 들어간 값은 지우지 않으면 계속 남습니다.
레디스에 직접 붙어서 값을 봤더니 읽을 수 없는 바이너리였습니다. 캐시가 이상할 때 눈으로 확인할 방법이 없다는 뜻입니다.
필드를 하나 추가하면 어떻게 되나
이게 제일 신경 쓰였던 부분입니다.
자바 기본 직렬화는 클래스 구조를 같이 저장합니다. serialVersionUID 를 직접 선언하지 않으면 필드 구성으로 계산되는데, 필드를 하나 추가하면 그 값이 바뀝니다. 그러면 기존에 캐시에 들어 있던 값은 새 클래스로 못 읽습니다.
이 DTO 는 실제로 필드가 늘어난 적이 있었습니다. 계좌 정보 항목을 하나 추가했었거든요. 그리고 TTL 이 없으니 문제가 되는 값이 시간이 지나서 사라지지도 않습니다. 캐시를 비우지 않으면 계속 그 상태입니다.
배포하면 응답 형태가 바뀌는 건 흔한 일인데, 그때마다 캐시를 비워야 하는 구조였습니다. 그걸 아무 데도 안 적어놨고요.
JSON 으로 바꾸고 TTL 을 넣었다
1
2
3
4
5
6
7
8
9
10
11
@Bean
public RedisCacheConfiguration defaultCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(30));
}
값을 JSON 으로 바꿨습니다. 이러면 Serializable 이 필요 없고, 필드를 추가해도 없는 필드는 그냥 안 채워집니다. 레디스에서 값도 그대로 읽힙니다.
키 직렬화는 사실 기본값도 문자열입니다. 그래도 같이 명시했습니다. 기본값에 한 번 물린 뒤라 어느 게 기본이고 어느 게 우리 결정인지 코드에 보이는 편이 낫다고 봤습니다.
TTL 은 30분으로 잡았습니다. 통계값이 실시간이어야 하는 화면이 아니고, 30분이면 그 사이에 뭐가 잘못돼도 알아서 사라지거든요. 숫자에 대단한 근거는 없습니다. 만료가 아예 없는 것보다 낫다는 정도입니다.
남은 생각
GenericJackson2JsonRedisSerializer 도 완전히 자유롭진 않습니다. 값에 @class 로 타입 정보를 같이 넣기 때문에 클래스를 옮기거나 이름을 바꾸면 기존 캐시를 못 읽습니다. JDK 직렬화보다 덜 깨지는 거지 안 깨지는 게 아닙니다. 그래서 TTL 이 짝으로 필요합니다. 어차피 깨질 수 있으면 짧게 살다 가게 하는 쪽이 낫습니다.
제가 배운 건 기본값을 그냥 쓸 때는 그 기본값이 뭔지 확인해야 한다는 겁니다. defaultCacheConfig() 라는 이름은 “적당한 기본”처럼 보이는데, 실제로는 자바 직렬화와 무한 TTL 이라는 꽤 강한 선택이 들어 있습니다. 그리고 그 선택을 저한테 알려준 건 문서가 아니라 Serializable 을 붙이라는 컴파일 요구였습니다. 뭔가 예상 밖의 걸 요구하면 그게 신호입니다.
한계도 남았습니다. 캐시 무효화는 여전히 TTL 에 맡기고 있습니다. 통계에 반영돼야 하는 이벤트가 생겼을 때 캐시를 직접 비우는 처리는 넣지 않았고, 지금도 30분을 기다립니다. 그게 문제가 될 화면이 아니라고 판단했지만 캐시를 더 쓰게 되면 다시 봐야 할 부분입니다.