jongkwan.dev
개발 · Essay №004

캐싱 전략: 다층 캐시 아키텍처와 Redis 고급 활용

Cache-Aside, Write-Through, 스탬피드 방지, Redis Cluster 등 캐싱 전략 총정리

이종관2025년 1월 8일18 min read
Contents

캐싱은 빠른 저장소에 데이터를 임시 보관해 응답 시간과 원본 부하를 줄이는 기법이다. 패턴 선택과 스탬피드·일관성 관리가 설계의 핵심이다.

캐싱이 푸는 문제

캐싱(Caching)은 자주 접근하는 데이터를 원본보다 빠른 저장소에 임시 보관하는 기법이다. 응답 시간을 단축하고 원본 데이터 소스의 부하를 줄인다. 잘 설계된 캐싱 전략은 워크로드에 따라 데이터베이스 쿼리를 크게 줄이고 응답 시간을 단축할 수 있다.

Redis는 인메모리 캐시의 표준으로 널리 쓰이며, 다층 캐시 아키텍처(Multi-Level Caching)와 엣지 캐싱(Edge Caching)을 결합한 전략이 흔하다. 캐시 스탬피드(Cache Stampede) 방지, 캐시 일관성(Cache Consistency) 유지가 대규모 시스템 설계의 핵심 과제로 남아 있다.

다층 캐시 아키텍처 (Multi-Level Caching)

캐시 계층 구조

각 계층의 특성

계층저장소지연 시간용량범위
L0 (CDN)Cloudflare, CloudFront< 50ms (글로벌)대규모전 세계
L1 (Reverse Proxy)NGINX, Varnish< 1ms중간서버 단위
L3 (In-Process)로컬 메모리< 0.1ms소규모프로세스 단위
L4 (Distributed)Redis Cluster< 5ms대규모서비스 전체
L5 (DB)PostgreSQL10-100ms무제한-

다층 캐시의 과제

스탬피드 이중 발생: In-Process 캐시(L3)와 Redis(L4)가 동시에 만료되면, 모든 인스턴스가 동시에 Redis를 거쳐 DB에 접근하는 이중 스탬피드가 발생할 수 있다.

해결: L3과 L4의 TTL을 다르게 설정하고, L4의 TTL을 L3보다 길게 유지한다.

text
L3 TTL: 30초 (빠른 만료, 자주 갱신)
L4 TTL: 300초 (L3 미스 시 안전망)

캐싱 패턴 (Caching Patterns)

1. Cache-Aside (Lazy Loading)

가장 널리 사용되는 패턴. 애플리케이션이 캐시와 데이터베이스를 직접 관리한다.

text
읽기:
1. 캐시에서 조회 (GET key)
2. HIT → 캐시 데이터 반환
3. MISS → DB에서 조회 → 캐시에 저장 (SET key value EX ttl) → 데이터 반환
 
쓰기:
1. DB에 쓰기
2. 캐시 무효화 (DEL key)
typescript
async function getUser(userId: string): Promise<User> {
  // 1. 캐시 조회
  const cached = await redis.get(`user:${userId}`);
  if (cached) {
    return JSON.parse(cached);
  }
 
  // 2. DB 조회
  const user = await db.users.findById(userId);
  if (user) {
    // 3. 캐시에 저장 (TTL 5분)
    await redis.set(`user:${userId}`, JSON.stringify(user), 'EX', 300);
  }
 
  return user;
}

장점: 필요한 데이터만 캐싱, 캐시 장애 시 DB로 폴백 가능 단점: 첫 요청은 항상 느림(Cold Start), 캐시와 DB 간 불일치 가능

2. Read-Through

캐시 자체가 DB 접근을 관리한다. 애플리케이션은 캐시만 바라본다.

text
읽기:
1. 캐시에 요청
2. HIT → 반환
3. MISS → 캐시가 자체적으로 DB 조회 → 저장 → 반환
  • 장점: 애플리케이션 로직 단순화
  • 단점: 캐시 라이브러리/미들웨어가 DB 접근 로직을 포함해야 함

3. Write-Through

데이터가 캐시와 DB에 동시에 기록된다.

text
쓰기:
1. 캐시에 쓰기
2. 캐시가 DB에 쓰기 (동기)
3. 양쪽 모두 완료 후 응답

장점: 캐시와 DB가 항상 일관성 유지 단점: 쓰기 지연 시간 증가 (두 번 쓰기), 불필요한 데이터도 캐싱

4. Write-Back (Write-Behind)

데이터를 캐시에만 즉시 기록하고, DB에는 비동기로 나중에 기록한다.

text
쓰기:
1. 캐시에 쓰기 → 즉시 응답
2. 백그라운드에서 DB에 쓰기 (배치 또는 지연)

장점: 매우 빠른 쓰기 성능, DB 부하 분산 단점: 데이터 손실 위험 (캐시 장애 시 미동기화 데이터 유실)

패턴 비교

패턴읽기 성능쓰기 성능일관성데이터 손실 위험복잡도
Cache-Aside좋음보통보통낮음낮음
Read-Through좋음보통보통낮음보통
Write-Through좋음느림높음매우 낮음보통
Write-Back좋음매우 빠름낮음높음높음

캐시 무효화 (Cache Invalidation)

"컴퓨터 과학에서 어려운 것은 딱 두 가지: 캐시 무효화와 이름 짓기." - Phil Karlton

무효화가 어려운 이유는 언제·무엇을 지울지 판단이 워크로드마다 다르기 때문이다. 그래서 무효화 방식을 TTL 기반과 이벤트 기반으로 나눠 본다.

TTL (Time-To-Live) 기반

캐시 항목에 만료 시간을 설정하여 자동 무효화한다.

redis
SET user:123 '{"name":"홍길동"}' EX 300  # 5분 후 자동 만료

TTL 설정 가이드:

  • 자주 변경되는 데이터: 짧은 TTL (30초 ~ 5분)
  • 거의 변경되지 않는 데이터: 긴 TTL (1시간 ~ 24시간)
  • 변경 시 즉시 무효화 필요: TTL + 이벤트 기반 무효화

이벤트 기반 무효화

데이터 변경 시 명시적으로 캐시를 삭제하는 방식이다.

typescript
async function updateUser(userId: string, data: UpdateUserDto) {
  // 1. DB 업데이트
  const updatedUser = await db.users.update(userId, data);
 
  // 2. 캐시 무효화
  await redis.del(`user:${userId}`);
 
  // 3. 관련 캐시도 무효화
  await redis.del(`user-profile:${userId}`);
  await redis.del(`user-orders:${userId}`);
 
  return updatedUser;
}

삭제 vs 갱신

전략동작장점단점
삭제 (Invalidate)캐시 키 삭제, 다음 조회 시 재캐싱단순, 안전다음 요청 시 DB 히트
갱신 (Update)캐시 값을 새 데이터로 직접 교체DB 히트 없음경합 상태(Race Condition) 위험

권장: 대부분의 경우 삭제 전략이 더 안전하다.

캐시 스탬피드 방지 (Cache Stampede Prevention)

문제

인기 있는 캐시 키가 만료되는 순간, 수백~수천 개의 동시 요청이 모두 DB를 직접 조회하여 DB가 과부하에 빠지는 현상이다. "Thundering Herd" 또는 "Dog-Pile Effect"라고도 한다.

text
캐시 만료 → 동시 요청 1000개 → DB에 1000개 동일 쿼리 → DB 과부하

방지 전략

1. 분산 잠금 (Distributed Locking)

하나의 요청만 DB를 조회하고, 나머지는 잠금이 해제될 때까지 대기한다.

typescript
async function getWithLock(key: string): Promise<any> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
 
  const lockKey = `lock:${key}`;
  // SET NX (존재하지 않을 때만 설정) - 원자적 잠금
  const acquired = await redis.set(lockKey, '1', 'EX', 10, 'NX');
 
  if (acquired) {
    try {
      const data = await fetchFromDatabase(key);
      await redis.set(key, JSON.stringify(data), 'EX', 300);
      return data;
    } finally {
      await redis.del(lockKey);
    }
  } else {
    // 잠금 대기 후 재시도
    await sleep(50);
    return getWithLock(key);
  }
}

잠금이나 리스(lease)로 동시 재계산을 한 요청으로 묶으면 피크 시 DB 부하가 크게 줄어든다. Facebook은 'Scaling Memcache at Facebook'(USENIX NSDI 2013)에서 리스 적용 후 피크 DB 쿼리율이 17K/s에서 1.3K/s로 약 13배 떨어졌다고 보고했다. 절감 폭은 키 만료 패턴과 동시성에 따라 달라진다.

2. 확률적 조기 만료 (Probabilistic Early Expiration)

TTL이 만료되기 전에 확률적으로 미리 갱신을 수행한다.

XFetch 알고리즘은 캐시 만료 전에 확률적으로 갱신을 시도하여 Stampede를 예방한다. delta는 캐시 재계산에 걸리는 시간이고, beta는 조기 갱신의 적극성을 조절하는 파라미터이다. 남은 TTL이 작아질수록 우변 난수보다 작아질 확률이 커지므로, 만료 직전 일부 요청만 미리 갱신하게 된다.

typescript
function shouldRefresh(ttlRemaining: number, delta: number, beta: number = 1.0): boolean {
  // XFetch 알고리즘
  const random = -delta * beta * Math.log(Math.random());
  return ttlRemaining < random;
}
 
// ttlRemaining이 적을수록 갱신 확률이 높아짐
// delta: 캐시 재계산에 소요되는 시간
// beta: 조기 만료 정도 조절 (기본 1.0)

3. Singleflight / Request Coalescing

동일한 키에 대한 동시 요청을 하나로 합쳐서 DB에 한 번만 조회한다.

go
// Go의 singleflight 패턴
group := &singleflight.Group{}
 
result, err, shared := group.Do("user:123", func() (interface{}, error) {
    return fetchFromDatabase("user:123")
})
// shared == true이면 다른 호출자와 결과를 공유한 것

4. 스탬피드 방지 전략 비교

전략구현 복잡도효과적합한 상황
분산 잠금보통높음대부분의 경우
확률적 조기 만료보통높음TTL이 긴 캐시
Singleflight낮음높음같은 프로세스 내 동시 요청
캐시 워밍낮음보통예측 가능한 패턴

Redis 고급 활용

Redis Cluster

데이터를 16,384개의 해시 슬롯에 분배하여 수평 확장한다.

text
슬롯 0-5460     → Node A (+ Replica A')
슬롯 5461-10922 → Node B (+ Replica B')
슬롯 10923-16383 → Node C (+ Replica C')

Redis Sentinel

**고가용성(HA)**을 위한 모니터링 및 자동 장애 조치(Failover) 시스템이다.

장애 복구 흐름: Master 장애 감지 → Sentinel 투표 → Replica 1 승격 → 클라이언트 리다이렉트

Redis 고급 데이터 구조

데이터 구조활용 사례예시
Sorted Set실시간 리더보드, 랭킹ZADD leaderboard 1000 "player1"
HyperLogLog고유 방문자 수 추정 (UV)0.81% 오차로 2^64개까지 카운트, 12KB 메모리
Stream이벤트 스트리밍, 메시지 큐Kafka 대안 (경량)
Bitmap사용자 활동 추적 (일별 로그인)SETBIT daily:login:2026-02-10 userId 1
Geospatial위치 기반 검색 (반경 내 가게)GEORADIUS stores 126.97 37.56 5 km

Redis 활용 예시: 실시간 리더보드

redis
# 점수 추가/갱신
ZADD game:leaderboard 1500 "player_A"
ZADD game:leaderboard 2300 "player_B"
ZADD game:leaderboard 1800 "player_C"
 
# 상위 10명 조회 (점수 내림차순)
ZREVRANGE game:leaderboard 0 9 WITHSCORES
 
# 특정 플레이어 순위 조회
ZREVRANK game:leaderboard "player_A"
 
# 점수 범위 조회
ZRANGEBYSCORE game:leaderboard 1000 2000

캐시 워밍 전략 (Cache Warming)

개념

서비스 시작 시 또는 배포 후 캐시가 비어있는 콜드 스타트(Cold Start) 상태에서 DB에 부하가 집중되는 것을 방지하기 위해, 사전에 캐시를 채워놓는 전략이다.

워밍 방법

  1. 스타트업 워밍: 서비스 부팅 시 인기 데이터를 미리 캐싱
  2. 스케줄 워밍: 크론 잡으로 주기적 캐시 갱신
  3. 이벤트 기반 워밍: 데이터 변경 이벤트(Change Data Capture, CDC) 발생 시 즉시 캐싱
  4. 점진적 전환: 신규 캐시 노드에 트래픽을 서서히 증가

아래 예시는 인기 상품 1000개를 한 번에 캐싱한다. redis.pipeline()으로 SET 명령을 모아 한 번의 왕복으로 보내고, 'EX', 3600으로 각 키에 1시간 TTL을 건다.

typescript
// 스타트업 워밍 예시
async function warmCache() {
  const popularProducts = await db.products.findPopular({ limit: 1000 });
 
  const pipeline = redis.pipeline();
  for (const product of popularProducts) {
    pipeline.set(
      `product:${product.id}`,
      JSON.stringify(product),
      'EX', 3600
    );
  }
  await pipeline.exec();
 
  console.log(`캐시 워밍 완료: ${popularProducts.length}개 상품`);
}

마이크로서비스에서의 캐시 일관성

과제

마이크로서비스 환경에서는 여러 서비스가 동일 데이터를 캐싱할 수 있으며, 데이터 변경 시 모든 서비스의 캐시를 동기화해야 한다.

해결 방안

  1. 이벤트 기반 무효화: Kafka/Redis Pub/Sub으로 캐시 무효화 이벤트 브로드캐스트
  2. CDC 기반 자동 무효화: Debezium으로 DB 변경 감지 → 캐시 자동 갱신
  3. 짧은 TTL: 강한 일관성이 불필요하면 짧은 TTL로 최종 일관성 확보
  4. 캐시 태그 (Cache Tags): 관련 캐시 항목을 태그로 묶어 일괄 무효화

엣지 캐싱 (Edge Caching)

Cloudflare Workers와 엣지 캐싱

사용자에게 물리적으로 가장 가까운 엣지 서버에서 캐시를 제공하여 글로벌 지연 시간을 최소화한다. Cloudflare는 자사 네트워크 페이지에서 전 세계 인터넷 사용자의 95%가 데이터센터로부터 50ms 이내에 있다고 밝힌다(Cloudflare Network, 2026 기준). 사용자와 엣지 PoP가 가까울수록 캐시 HIT 응답은 그만큼 빨라진다.

text
사용자 (한국) → 서울 엣지 PoP → 캐시 HIT → 즉시 응답 (< 50ms)
사용자 (한국) → 서울 엣지 PoP → 캐시 MISS → 오리진 서버 → 캐싱 → 응답

엣지 캐시 전략:

  • 정적 콘텐츠: 이미지, JS/CSS → 긴 TTL (1일~1년)
  • 동적 API 응답: Cache-Control 헤더 기반 → 짧은 TTL (1초~5분)
  • 개인화 콘텐츠: 캐시 불가 또는 Vary 헤더 활용

정리

캐싱 패턴은 읽기·쓰기 성능과 일관성, 데이터 손실 위험의 트레이드오프로 고른다. 대부분은 Cache-Aside로 시작하고, 강한 일관성이 필요하면 Write-Through, 쓰기 처리량이 우선이면 Write-Back을 검토한다. 인기 키 만료가 DB를 덮치는 스탬피드는 분산 잠금·확률적 조기 만료·Singleflight로 막고, 다층 캐시에서는 L3보다 L4의 TTL을 길게 둬 이중 스탬피드를 피한다. 무효화는 삭제 전략을 기본으로 두고, 마이크로서비스에서는 이벤트·CDC 기반으로 캐시를 동기화한다.

다음 글에서는 같은 시리즈의 운영 주제를 이어서 다룬다.

참고 자료