캐시 정합성 전략 영상을 보고
개요
- 아티클 주소 :
뜨끈한 캐시를 위한 캐시 정합성 심화 전략에 대하여
캐시에 대해 알아야 하는 기본적인 내용들이 있어서 정리한다.
캐시 정합성이 어려운 이유
상품 가격이 10,000 -> 12,000 원으로 변경되었다면?
- DB 에는 12,000 원으로 업데이트 완료
- 캐시에는 10,000 원으로 남아있을 수 있다.
-> 사용자는 10,000 원을 봤는데, 결제는 신 가격으로 처리될 수 있다!!
원인 분석
- 캐시와 DB 는 별도의 저장소 - 두 곳을 원자적 업데이트는 사실상 불가능하다.
- 네트워크 지연, 서버 장애, 동시 요청 등등 갱신 순서가 보장되지 않을 수 있다.
두 곳을 일관되게 유지 라는건 근본적으로 어렵다.
=> 그렇기에, 패턴별 트레이드오프 + 비즈니스 요구에 맞게 전략을 선택해야 한다.
Cache-Aside

가장 널리 사용되는 캐시 패턴 (Lazy Loading)
- 읽기 : 캐시에서 먼저 조회, 없으면 DB 에서 읽고 캐시 저장 후 반환
- 쓰기 : DB 를 먼저 업데이트하고, 캐시 삭제
- TTL 을 설정해 캐시가 영원히 남는 걸 방지
캐시를 삭제하는 이유 : 업데이트시, DB 쓰기 & 캐시 쓰기 사이 정합성이 깨질 수 있다.
왜지..?
다음 읽기는 자연스럽게 최신 데이터로 캐시가 재생성된다.
나머지 캐시 전략
-
Read-Through : 읽기 동작, 캐시 Miss 시 자동으로 DB 조회
-
Write-Through : 쓰기 동작, 캐시에 쓰고, 동기적으로 DB에 쓰기
정합성은 높지만, 매번 쓰기마다 캐시 + DB 두 번 기록해 느리다.
(어차피 정합성 문제가 발생할 수 있다.)
- Write-Behind : 쓰기 동작, 캐시에 쓰고, 비동기로 나중에 DB 반영
쓰기가 매우 빠르지만 캐시 장애 시 미반영 데이터가 유실될 수 있다.
유실되어도 괜찮은 데이터에만 적용해야 한다.
캐시 무효화 순서 역전 문제
일종의 캐시간 Race Condition
1. Thread A: DB 업데이트
2. Thread B: DB 조회, 구 데이터
3. Thread A: 캐시 삭제
4. Thread B: 캐시에 구 데이터 저장
=> 캐시에 구 데이터 잔존! (DB는 최신, 캐시는 구 데이터가 TTL 만료까지 유지)DB 업데이트, 캐시 삭제 순서에 상관없이 가능성은 존재한다.
그래도, DB 먼저 업데이트 -> 캐시 삭제가 상대적으로 안전하다. - Source of Truth
Double Delete
public void updateProduct(Long id, ProductUpdateDto dto) {
// 1. 캐시 선 삭제 -- 구 데이터 즉시 제거
cache.deleteProduct(id);
repo.updateProduct(id, dto);
CompletableFuture.delayedExecutor(500, TimeUnit.MILLISECONDS)
.execute(() -> cache.deleteProduct(id));
}- 1차 삭제 : 구 데이터 즉시 제거해 대부분 요청이 최신 데이터를 받게 유도
- 2차 삭제 : 500ms 뒤 한 번 더 삭제해 레이스 컨디션으로 저장된 구 데이터 제거
지연 시간은 DB 복제 지연 + 캐시 쓰기 시간등을 고려하여 결정 (보통 300ms ~ 1s)
-> 지연 시간 사이 여전히 구 데이터 노출 가능 및 완벽하지 않다.
비교적 간단하게 구현가능한게 큰 장점
Cache Stampede

인기 데이터의 캐시가 만료되는 순간, 수많은 요청이 동시에 캐시 Miss 발생
모든 요청이 DB 로 몰려, 동일한 데이터 중복 조회 - DB 과부하
꼭 수많은 요청이 아니더라도 무거운 쿼리가 실행되는 것도 문제가 될 수 있다.
- 일종의 Thundering Herd, 트래픽 높은 서비스에서 빈번히 발생 가능
- 심하면 DB 에 큰 부하를 주고, 연쇄적으로 더 많은 캐시 Miss 를 발생시키는 악순환 발생 가능
분산 락
public Product getProductWithLock(Long id) {
String key = "product:" + id;
Product cached = cache.opsForValue().get(key);
if (cached != null) return cached;
// 분산 락으로 한 스레드만 DB 조회
RLock lock = redisson.getLock("lock:" + key);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
cached = cache.opsForValue().get(key); // Double Check
if (cached != null) return cached;
Product product = repo.findById(id).orElseThrow();
cache.opsForValue().set(key, product, Duration.ofMinutes(30));
return product;
}
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
throw new RuntimeException("캐시 갱신 대기 초과");
}분산 락을 통해 하나의 스레드만 DB 를 조회하는걸 보장한다.
나머지는 락 대기 후 캐시에서 읽는다.
- 락 획득 후 Double Check 를 한다. - 다른 스레드가 이미 캐시 채웠을 수 있으므로 재확인
트레이드 오프가 확실하다. 캐시를 보장하기 위해
그 캐시를 저장하는 (일반적으로) 레디스에 추가적인 연산을 하는 것이기 때문이다.
PER 알고리즘
public Product getProductWithPER(Long id) {
String key = "product:" + id;
CacheEntry entry = getFromCache(key);
if (entry == null) return fetchAndCache(id, key);
long ttl = entry.getExpireAt() - System.currentTimeMillis();
// beta * log(random) * computeTime 으로 조기 갱신 확률 계산
double gap = entry.getComputeTime()
* Math.log(Math.random());
if (ttl + gap <= 0) {
return fetchAndCache(id, key); // TTL 전에 확률적으로 갱신
}
return entry.getValue();
}- PER : Probabilistic Early Recomputation
TTL 만료 전 확률적으로 데이터를 미리 갱신하는 알고리즘이다.
만료 시점에 가까워질수록 갱신 확률이 높아져 자연스럽게 1~2개의 요청만 DB 를 조회하는걸 기대할 수 있다.
즉, 모든 요청이 보내는걸 확률적으로 피한것이다.
- 락 없이 동작하므로 분산 락 방식보다 성능 오버헤드가 적다
TTL 전략, 트레이드오프
TTL 이 캐시 정합성과 성능의 균형을 결정하는 핵심적 파라미터이다.
(일종의 DB 인덱스 느낌)
정합성: 캐시 데이터와 DB 원본 데이터가 얼마나 일치하는지
캐시 히트율: 요청이 들어왔을 때 캐시에서 바로 응답할 수 있는 비율
DB 부하: 캐시 미스 시마다 DB 조회, ↔ 히트율
- 1분 이하: 정합성 높음 / 캐시 히트율 낮음 / DB 부하 높음
실시간 데이터(재고, 좌석) 등에 적합
- 5분~30분: 정합성 중간 / 캐시 히트율 높음 / DB 부하 중간
일반적 상품 정보, 사용자 프로필에 적합
- 1~24시간 : 정합성 낮음 / 캐시 히트율 매우 높음 / DB 부하 낮음
변경 빈도가 낮은 데이터, 카테고리 & 설정등
- TTL X : 정합성 높음 / 캐시 히트율 최고 / DB 부하 최저
변경 시 명시적 삭제, 정적 데이터들
- 추가로, TTL 은 Jitter 를 적용하면 좋다. 모든 키가 동시 만료되는 걸 방지하는 랜덤 오프셋
캐시 워밍과 프리로딩
서버 기동시 캐시가 비어있다.
-> 모든 요청이 DB 로 직행!!! (Cold Start)
- Warming : 서버 시작 시 인기 데이터 미리 캐시에 적재
헬스체크를 워밍 완료 후 정상으로 전환해야한다.
- Lazy Loading : 최초 요청 시 캐시 적재, 이후 Hit
- Schedule Warming : 주기적으로 인기 데이터 갱신
- Shadow 배포 : 신규 서버에 기존 서버 트래픽 복제해 캐시 활성화
실전 체크리스트
캐시를 잘 적용하고 싶다면 해야할 것
