TTL — 시간에 맡긴다

가장 단순하다. 저장할 때 만료를 걸고 지나면 사라진다.

SET product:42 {...} EX 300
  • 보장 — 낡은 데이터는 최대 TTL만큼만 유지된다. 무효화 로직이 필요 없고, 코드가 늘지 않는다
  • 포기 — 실시간성. 값이 바뀌어도 TTL이 남아 있으면 낡은 값이 나간다

바뀌는 빈도가 낮거나 잠깐 낡아도 되는 데이터에 잘 맞는다. 가장 먼저 시도할 방법이고, 이걸로 충분한 경우가 많다.

명시적 삭제 — 바뀔 때 지운다

데이터를 갱신하면서 캐시도 함께 지운다.

productRepository.save(product);
cache.delete("product:" + product.getId());
  • 보장 — 다음 조회부터 새 값이 나간다. 실시간성이 좋다
  • 포기 — 단순함. 지워야 할 곳을 빠짐없이 찾아야 한다

문제는 대개 파생 캐시에서 생긴다. 상품 하나를 고쳤을 때 product:42만이 아니라 목록 캐시, 카테고리 캐시, 검색 결과 캐시가 모두 낡는다. 새 화면을 추가하면서 캐시를 하나 더 만들었는데 무효화를 빠뜨리는 일이 흔하다.

DB 갱신과 캐시 삭제 사이에 장애가 나면 낡은 값이 남는 것도 감안해야 한다. 그래서 명시적 삭제를 쓰더라도 TTL을 함께 걸어두는 편이 안전하다.

버전 키 — 지우지 않고 무효화한다

키에 버전을 넣고, 무효화가 필요하면 버전을 올린다.

키 :  product:42:v7
갱신:  version:product:42 → 8
조회:  product:42:v8   (없음 → 새로 채운다)
  • 보장 — 지울 대상을 찾을 필요가 없다. 버전 하나만 올리면 그 아래 파생 키가 전부 함께 무효화된다
  • 포기 — 메모리. 낡은 키가 즉시 사라지지 않고 TTL이나 LRU로 정리될 때까지 남는다

카테고리 버전을 올려 그 카테고리의 모든 캐시를 한 번에 무효화하는 식으로, 묶어서 무효화할 때 특히 강하다.

스탬피드

셋 중 무엇을 쓰든 만나는 문제다. 인기 있는 키가 만료되는 순간 요청 수천 개가 동시에 빈 캐시를 보고 한꺼번에 DB로 몰린다.

  • 잠금 — 하나만 갱신하게 하고 나머지는 기다리거나 낡은 값을 받는다
  • 만료 분산 — TTL에 약간의 무작위를 더해 동시에 만료되지 않게 한다
  • 조기 갱신 — 만료 직전에 미리 비동기로 채운다

정리

  실시간성 구현 난이도 메모리
TTL 낮음 낮음 좋음
명시적 삭제 높음 높음 좋음
버전 키 높음 중간 나쁨

TTL로 시작해서 부족한 지점만 다른 전략으로 보강하는 순서를 권한다. 처음부터 정교하게 만들면 무효화 누락을 찾느라 시간을 더 쓰게 된다.