이종관
글 목록으로

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

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

2025년 1월 8일·15 min read·
infra
caching
redis
performance
architecture

개요

캐싱(Caching)은 자주 접근하는 데이터를 원본보다 빠른 저장소에 임시 보관하여 응답 시간을 단축하고 원본 데이터 소스의 부하를 줄이는 기법이다. 잘 설계된 캐싱 전략은 데이터베이스 쿼리를 90% 이상 줄이고, 응답 시간을 수십 배 개선할 수 있다.

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

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

캐시 계층 구조

plaintext
사용자 요청


L0: CDN / Edge Cache          ← 글로벌 엣지 (지연 시간 < 50ms)
    │ MISS

L1: Reverse Proxy Cache        ← NGINX, Varnish
    │ MISS

L2: API Gateway Cache          ← Kong, AWS API Gateway
    │ MISS

L3: Application In-Process     ← Node.js Map, Caffeine (JVM)
    │ MISS

L4: Distributed Cache          ← Redis, Memcached
    │ MISS

L5: Primary Datastore          ← PostgreSQL, MongoDB

각 계층의 특성

계층저장소지연 시간용량범위
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보다 길게 유지한다.

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

캐싱 패턴 (Caching Patterns)

1. Cache-Aside (Lazy Loading)

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

plaintext
읽기:
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 접근을 관리한다. 애플리케이션은 캐시만 바라본다.

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

3. Write-Through

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

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

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

4. Write-Back (Write-Behind)

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

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

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

패턴 비교

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

캐시 무효화 (Cache Invalidation)

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

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"라고도 한다.

plaintext
캐시 만료 → 동시 요청 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);
  }
}

단순 잠금 키로 스탬피드를 방지하면 피크 시 DB 부하를 30-70% 절감할 수 있다.

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

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개의 해시 슬롯에 분배하여 수평 확장한다.

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

Redis Sentinel

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

plaintext
[Sentinel 1] [Sentinel 2] [Sentinel 3]
     │            │            │
     └────────────┼────────────┘

     ┌────────────┼────────────┐
     ▼            ▼            ▼
 [Master]    [Replica 1]  [Replica 2]
 
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. 이벤트 기반 워밍: 데이터 변경 이벤트(CDC) 발생 시 즉시 캐싱
  4. 점진적 전환: 신규 캐시 노드에 트래픽을 서서히 증가
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): 관련 캐시 항목을 태그로 묶어 일괄 무효화
plaintext
User Service가 사용자 정보 변경

Kafka 이벤트: user.updated (userId: 123)

├── Order Service: redis.del("user-orders:123")
├── Profile Service: redis.del("user-profile:123")
└── Recommendation Service: redis.del("user-recs:123")

엣지 캐싱 (Edge Caching)

Cloudflare Workers와 엣지 캐싱

사용자에게 물리적으로 가장 가까운 엣지 서버에서 캐시를 제공하여 글로벌 지연 시간을 최소화한다. 엣지 캐싱을 활용하면 전 세계적으로 50ms 미만의 지연 시간을 달성할 수 있다.

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

엣지 캐시 전략:

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

참고 자료