이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- TTL, eviction, invalidation은 각각 어떤 문제를 해결하는가?
- TTL을 짧게 잡으면 왜 DB 부하와 stampede 위험이 커질 수 있는가?
- 데이터 변경을 캐시에 반영할 때 삭제, 갱신, 이벤트 중 무엇을 선택해야 하는가?
개요
TTL, eviction, invalidation은 모두 캐시에서 값이 사라지는 방식과 관련이 있지만 목적이 다르다.
TTL은 key가 일정 시간 뒤 만료되도록 하는 정책이다. Eviction은 Redis 메모리가 부족할 때 어떤 key를 밀어낼지 정하는 정책이다. Invalidation은 원본 데이터가 바뀌었을 때 관련 캐시를 명시적으로 지우거나 갱신하는 정책이다.
세 가지를 구분하지 않으면 “TTL 걸었으니 정합성 해결” 같은 잘못된 판단을 하기 쉽다.
TTL은 정합성 계약이다
TTL은 기술적으로 만료 시간이다. 설계 관점에서는 사용자가 오래된 값을 볼 수 있는 최대 시간에 가깝다.
product:detail:1001 TTL = 5분상품 설명이라면 5분 stale이 큰 문제가 아닐 수 있다. 그러나 가격이나 재고라면 5분 동안 틀린 값이 사용자 손해로 이어질 수 있다.
TTL은 데이터 성격마다 달라야 한다.
| 데이터 | TTL 후보 | 이유 |
|---|---|---|
| 상품 설명 | 5분에서 1시간 | 자주 바뀌지 않고 피해가 작다. |
| 상품 가격 | 수 초에서 수 분, 또는 invalidation | 잘못 표시되면 사용자 신뢰 문제가 된다. |
| 재고 수량 | 매우 짧게 또는 캐시 제외 | 구매 가능 여부와 직접 연결된다. |
| 권한 정보 | 매우 짧게 또는 강한 무효화 | 보안 문제로 이어질 수 있다. |
| 추천 목록 | 수 분 이상 | 개인화 품질 저하는 허용 가능할 수 있다. |
TTL을 정할 때는 “데이터가 얼마나 자주 바뀌는가?”보다 “틀렸을 때 피해가 얼마인가?”를 먼저 본다.
TTL이 너무 짧을 때
짧은 TTL은 stale 시간을 줄이지만 miss를 늘린다.
peak read = 5,000 QPS
cache hit ratio 95% -> DB read 250 QPS
cache hit ratio 80% -> DB read 1,000 QPSTTL을 줄여 hit ratio가 떨어지면 DB 부하는 빠르게 늘어난다. 특히 많은 key가 비슷한 시간에 만료되면 cache stampede가 생길 수 있다.
TTL에는 jitter를 넣는 것이 좋다.
Duration baseTtl = Duration.ofMinutes(5);
Duration jitter = Duration.ofSeconds(ThreadLocalRandom.current().nextInt(0, 60));
redisTemplate.opsForValue().set(key, value, baseTtl.plus(jitter));Jitter는 key들이 같은 순간에 만료되는 일을 줄인다.
Eviction은 메모리 압박 대응이다
Eviction은 TTL과 다르다. TTL이 남아 있어도 Redis 메모리가 부족하면 eviction policy에 따라 key가 제거될 수 있다.
Redis 운영에서는 maxmemory-policy가 중요하다.
allkeys-lru
volatile-lru
allkeys-lfu
noevictionnoeviction은 메모리가 꽉 차면 쓰기 요청이 실패할 수 있다. allkeys-lru는 모든 key 중 덜 사용된 key를 밀어낸다. 세션, lock, cache를 같은 Redis에 섞으면 eviction이 더 위험해진다.
캐시 key는 사라져도 다시 만들 수 있어야 한다. 반대로 session이나 distributed lock key는 eviction되면 사용자 로그아웃이나 동시성 문제가 생길 수 있다. 그래서 용도가 다른 데이터는 Redis logical DB만 나누는 것보다 클러스터나 인스턴스 분리를 검토한다.
Invalidation 방식
원본 데이터 변경 후 캐시를 맞추는 방식은 크게 세 가지다.
| 방식 | 장점 | 위험 |
|---|---|---|
| 삭제 | 다음 read에서 DB를 다시 읽어 채운다 | 삭제 실패 시 stale data가 남는다. |
| 갱신 | 쓰기 직후 캐시도 최신으로 만든다 | DB 성공, 캐시 실패 같은 부분 실패가 생긴다. |
| 이벤트 무효화 | 쓰기 경로와 캐시 삭제를 분리한다 | 이벤트 유실, 지연, 순서 역전이 생길 수 있다. |
많은 서비스에서는 쓰기 성공 후 캐시 삭제를 선택한다.
@Transactional
public void changePrice(Long productId, Money newPrice) {
productRepository.updatePrice(productId, newPrice);
cacheInvalidationPublisher.publish(new ProductPriceChanged(productId));
}트랜잭션 안에서 Redis를 직접 지우면 DB rollback과 Redis 삭제의 순서 문제가 생길 수 있다. 이벤트를 쓴다면 outbox 같은 방식으로 DB 변경과 이벤트 발행의 일관성을 보강할 수 있다.
삭제와 갱신 중 선택
캐시 갱신은 좋아 보이지만 더 어렵다.
예를 들어 상품 상세 캐시가 상품, 판매자, 할인, 리뷰 요약을 합친 값이라면 가격 변경 시 전체 캐시 값을 정확히 다시 만들기 어렵다. 이때는 삭제 후 다음 조회에서 재구성하는 편이 안전하다.
반대로 단순한 코드성 데이터나 feature flag처럼 값이 작고 생성 비용이 낮으면 갱신도 가능하다.
기준은 다음이다.
- 캐시 값이 여러 원천 데이터를 조합하면 삭제가 안전하다.
- 캐시 값 생성 비용이 매우 크면 갱신이나 refresh ahead를 검토한다.
- 잘못된 값 피해가 크면 삭제 실패를 감지하고 재시도해야 한다.
운영 지표
TTL, eviction, invalidation은 지표로 봐야 한다.
- cache hit ratio
- key count와 memory usage
- eviction count
- expired key count
- invalidation event count
- invalidation failure count
- stale read 신고나 보정 횟수
- DB fallback QPS
Eviction count가 증가하면 단순히 Redis 메모리만 늘릴 문제가 아닐 수 있다. TTL이 너무 길거나 key cardinality가 예상보다 크거나, 다른 용도의 key가 같은 Redis를 압박하고 있을 수 있다.
위험 신호!
- 모든 데이터에 같은 TTL을 적용한다.
- TTL만 믿고 원본 변경 시 invalidation을 하지 않는다.
- Redis eviction policy를 모른 채 세션과 캐시를 섞어 쓴다.
- 캐시 삭제 실패를 로그만 남기고 끝낸다.
- 이벤트 기반 invalidation을 쓰면서 중복, 지연, 유실 처리가 없다.
실전 팁
- TTL은 데이터 종류별로 문서화한다.
- TTL에는 jitter를 넣어 동시 만료를 줄인다.
- 캐시 key 수와 value 크기로 Redis 메모리를 추정한다.
- 중요한 invalidation은 retry 가능한 메시지로 처리한다.
- 캐시 삭제 실패가 사용자 피해로 이어지는 데이터는 알림을 둔다.
확인 질문
확인 질문
- 확인 질문: TTL과 eviction의 차이는 무엇인가?
- 답변: TTL은 key의 시간 기반 만료 정책이고, eviction은 메모리 부족 시 Redis가 key를 제거하는 정책이다.
- 확인 질문: TTL을 짧게 잡으면 어떤 비용이 생기는가?
- 답변: stale 시간은 줄지만 cache miss와 DB read가 증가하고 동시 만료 시 stampede가 생길 수 있다.
- 확인 질문: 복잡한 조합 캐시는 갱신보다 삭제가 안전한 이유는 무엇인가?
- 답변: 변경된 일부 데이터만으로 전체 캐시 값을 정확히 재구성하기 어렵고 부분 갱신 오류가 stale data를 만들 수 있기 때문이다.