[업무일지] JSON 으로 바꿨다가 같은 날 되돌린 레디스 캐시 직렬화
거래내역 통계 조회에 레디스 캐시를 붙였는데, 캐시에 넣는 순간 응답 DTO 에
Serializable이 없다는 예외가 났습니다. 파봤더니RedisCacheConfiguration.defaultCacheConfig()가 JDK 직렬화를 쓰고 있었고, 한 달 남짓 뒤 기본 설정을 JSON 직렬화로 바꿨습니다. 그날 오후 새로 붙은 캐시가 이 기본값을 그대로 물려받았고, 저녁에 역직렬화 에러로 팀에서 되돌렸습니다. 지금은 JDK 직렬화에 캐시별 TTL 로 둡니다.
거래내역 통계 API 가 무거웠습니다. 사용자별로 집계 쿼리가 몇 개 돌고, 값이 자주 바뀌지도 않아서 캐시를 붙였습니다. Java 17, Spring Boot 3.3.x 에 spring-boot-starter-data-redis 를 쓰는 환경입니다.
1
2
3
4
5
// Java 17, Spring Boot 3.3.x, spring-boot-starter-data-redis
@Cacheable(cacheNames = "paymentStatistics", key = "'userId:' + #userId")
public PaymentStatisticsResponse getStatistics(Long userId) {
...
}
통계 캐시에는 처음부터 5분 TTL 을 따로 줬습니다. 여기까진 금방 됐습니다. 그런데 응답 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 라 만료가 없습니다. 통계 캐시처럼 따로 TTL 을 준 캐시가 아니면, 한 번 들어간 값은 지우지 않는 한 계속 남습니다.
레디스에 직접 붙어서 값을 봤더니 읽을 수 없는 바이너리였습니다. 캐시가 이상할 때 눈으로 확인할 방법이 없다는 뜻입니다.
필드를 하나 추가하면 어떻게 되나
이게 제일 신경 쓰였던 부분입니다.
자바 기본 직렬화는 클래스 구조를 같이 저장합니다. 통계 DTO 같은 일반 클래스는 serialVersionUID 를 직접 선언하지 않으면 필드 구성으로 계산되는데, 필드를 하나 추가하면 그 값이 바뀝니다(record 는 이 검사를 면제받습니다). 그러면 기존에 캐시에 들어 있던 값은 새 클래스로 못 읽습니다.
통계 캐시는 5분이면 옛 값이 사라지니 버틸 만합니다. 문제는 기본값 그대로 새 캐시를 붙일 때입니다. TTL 을 따로 안 주면 만료가 없어서 응답 형태가 바뀌는 배포 때마다 누가 비워 줘야 하는데, 그걸 아무 데도 안 적어놨고요.
기본값을 JSON 으로 바꿨다
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분으로 잡았습니다. 숫자에 대단한 근거는 없고, 만료가 아예 없는 것보다 낫다는 정도였습니다.
같은 날 되돌렸다
그날 오후 동료가 캐시를 하나 새로 붙였습니다. 날짜 필드가 든 record 를 담는 캐시였고, 직렬화 설정은 제가 바꾼 기본값을 그대로 물려받았습니다. 저녁에 같은 동료가 역직렬화 에러를 고치는 커밋으로 기본 설정을 JDK 직렬화로 되돌렸습니다. 그 안에는 자바 날짜 타입 모듈을 넣었다가 다시 빼는 커밋도 있었고, 결국 캐시에 들어가는 DTO 들에 Serializable 을 붙이는 쪽으로 정리됐습니다. 레디스를 띄우는 테스트 컨테이너도 그때 같이 들어왔고요.
제 변경은 통계 캐시 하나만 보고 기본값을 바꾼 거였습니다. 그때 캐시는 그것 하나뿐이었지만, 기본값은 뒤에 붙는 캐시까지 그대로 물려받습니다. GenericJackson2JsonRedisSerializer 는 값에 타입 정보를 같이 적어 두고 그걸로 되살리는데, 날짜 타입이나 record 를 넣고 꺼내 보는 테스트 없이 바꿨죠.
지금은 JDK 직렬화에 캐시별 TTL 로 둔다
그래서 기본 설정은 다시 JDK 직렬화입니다. 제가 넣었던 기본 TTL 30분도 그때 같이 빠져서 기본값은 다시 만료가 없습니다. 통계 캐시는 처음부터 준 5분, 오후에 붙은 캐시는 하루 TTL 을 따로 받아 돌고, 캐시되는 DTO 는 Serializable 을 달고 있습니다.
defaultCacheConfig() 라는 이름은 “적당한 기본”처럼 보이는데, 실제로는 자바 직렬화와 무한 TTL 이라는 꽤 강한 선택이 들어 있습니다.
JSON 으로 다시 간다면 기본값을 한 번에 바꾸지 않고 캐시마다 직렬화를 따로 줄 생각입니다. 그 전에 날짜 타입과 record 를 실제로 넣고 꺼내 보는 테스트부터 있어야 하고요. 그때까지는 기본값 그대로 붙인 캐시가 TTL 까지 빠뜨리면, 필드 추가 배포 때 옛 값을 못 읽을 수 있다는 위험을 안고 갑니다.