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