개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

Redis 캐시 전략 — Cache Aside, Write Through, Write Back

2026년 1월 23일
RedisCacheCache Aside캐시성능 최적화

Redis를 캐시로 도입하기로 했습니다. 그런데 어떤 방식으로 캐시를 채우고, DB와 동기화할지 결정해야 합니다. 잘못된 전략을 선택하면 오래된 데이터가 서비스되거나, 캐시가 항상 비어 있는 상태가 됩니다.

용어 정리

Cache Hit:  요청한 데이터가 캐시에 있음 → 캐시에서 바로 반환
Cache Miss: 요청한 데이터가 캐시에 없음 → DB 조회 필요

Cache Hit Rate: 전체 요청 중 캐시에서 처리된 비율. 80~90% 이상이 목표입니다. 낮으면 캐시가 효과를 발휘하지 못하고 있다는 신호입니다.

Cache Aside (Lazy Loading)

가장 일반적인 방식입니다. 애플리케이션이 직접 캐시와 DB를 관리합니다.

읽기:
1. 캐시에서 조회
2. Cache Hit → 반환
3. Cache Miss → DB 조회 → 캐시에 저장 → 반환

쓰기:
1. DB에 저장
2. 캐시 무효화(삭제)
async function getUser(id: string) {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);

  const user = await db.users.findById(id);
  await redis.set(`user:${id}`, JSON.stringify(user), "EX", 300);
  return user;
}

async function updateUser(id: string, data: any) {
  await db.users.update(id, data);
  await redis.del(`user:${id}`);  // 캐시 무효화
}

장점:

  • 실제로 요청된 데이터만 캐시됨 (메모리 효율적)
  • DB 장애 시 캐시에서 일부 요청 처리 가능

단점:

  • 첫 요청은 항상 Cache Miss (Cold Start)
  • DB 업데이트 후 캐시 삭제 사이에 짧은 데이터 불일치 가능

Cold Start: 서버 재시작 등으로 캐시가 비어 있는 상태. 이 시점에는 모든 요청이 DB로 가므로 일시적으로 부하가 증가합니다.

Write Through

데이터를 쓸 때 캐시와 DB에 동시에 저장합니다.

쓰기:
1. 캐시에 저장
2. DB에 저장

읽기:
1. 캐시에서 조회 (항상 최신 데이터)
2. Cache Hit → 반환 (Miss 거의 없음)
async function updateUser(id: string, data: any) {
  await redis.set(`user:${id}`, JSON.stringify(data), "EX", 300);
  await db.users.update(id, data);
}

장점:

  • 캐시와 DB가 항상 동기화됨
  • Cache Miss가 거의 없음

단점:

  • 쓰기 지연이 두 배 (캐시 + DB 모두 기다림)
  • 읽히지 않는 데이터도 캐시에 저장되어 메모리 낭비

Write Back (Write Behind)

데이터를 먼저 캐시에 쓰고, 나중에 비동기로 DB에 동기화합니다.

쓰기:
1. 캐시에 즉시 저장 (응답 반환)
2. 비동기로 DB에 저장

읽기:
1. 캐시에서 조회

장점:

  • 쓰기 응답 속도가 매우 빠름
  • DB 부하를 줄이고 쓰기를 묶어서 처리 가능

단점:

  • 캐시 장애 시 DB에 반영 안 된 데이터 유실 위험
  • 구현이 복잡함

세 방식 비교

항목Cache AsideWrite ThroughWrite Back
읽기 속도Cache Hit 시 빠름빠름빠름
쓰기 속도빠름느림 (동기)매우 빠름
데이터 일관성약간 불일치 가능강함약함
구현 복잡도낮음낮음높음
메모리 효율좋음나쁨좋음

대부분의 경우 Cache Aside로 시작합니다. 단순하고 효과적이며, 문제가 생겨도 캐시를 비우면 원상복구됩니다.

캐시 무효화 전략

캐시된 데이터가 낡으면 잘못된 데이터가 서비스됩니다. 무효화 방법을 명확히 정해야 합니다.

TTL 기반

SET cache:product:500 "..." EX 300  # 5분 뒤 자동 삭제

데이터가 5분 이내로 낡은 것을 허용할 수 있다면 가장 간단합니다.

이벤트 기반

// 상품 업데이트 시
await db.products.update(id, data);
await redis.del(`cache:product:${id}`);  // 즉시 무효화

데이터 변경 시 정확히 해당 캐시를 삭제합니다.

태그 기반

// "category:electronics" 태그에 속한 모든 캐시 무효화
const keys = await redis.smembers("tag:electronics:keys");
await redis.del(...keys);

관련된 여러 캐시를 묶어서 무효화해야 할 때 씁니다.

Cache Stampede 방어

캐시가 만료되는 순간 다수의 요청이 동시에 DB로 몰리는 현상으로, DB에 급격한 부하가 발생합니다.

// Mutex Lock으로 방어
async function getWithLock(key: string) {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  // 락 획득 시도
  const lock = await redis.set(`lock:${key}`, "1", "NX", "EX", 5);
  if (!lock) {
    // 락 못 얻으면 잠시 대기 후 캐시 재시도
    await sleep(50);
    return getWithLock(key);
  }

  // 락 획득 → DB 조회 → 캐시 저장
  const data = await db.query(key);
  await redis.set(key, JSON.stringify(data), "EX", 300);
  await redis.del(`lock:${key}`);
  return data;
}
← 이전 글Redis Sorted Set — 점수 기반 정렬 집합과 랭킹
다음 글 →Redis TTL — 키 만료와 자동 삭제