Redis를 캐시로 도입하기로 했습니다. 그런데 어떤 방식으로 캐시를 채우고, DB와 동기화할지 결정해야 합니다. 잘못된 전략을 선택하면 오래된 데이터가 서비스되거나, 캐시가 항상 비어 있는 상태가 됩니다.
Cache Hit: 요청한 데이터가 캐시에 있음 → 캐시에서 바로 반환
Cache Miss: 요청한 데이터가 캐시에 없음 → DB 조회 필요
Cache Hit Rate: 전체 요청 중 캐시에서 처리된 비율. 80~90% 이상이 목표입니다. 낮으면 캐시가 효과를 발휘하지 못하고 있다는 신호입니다.
가장 일반적인 방식입니다. 애플리케이션이 직접 캐시와 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}`); // 캐시 무효화
}
장점:
단점:
Cold Start: 서버 재시작 등으로 캐시가 비어 있는 상태. 이 시점에는 모든 요청이 DB로 가므로 일시적으로 부하가 증가합니다.
데이터를 쓸 때 캐시와 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에 동기화합니다.
쓰기:
1. 캐시에 즉시 저장 (응답 반환)
2. 비동기로 DB에 저장
읽기:
1. 캐시에서 조회
장점:
단점:
| 항목 | Cache Aside | Write Through | Write Back |
|---|---|---|---|
| 읽기 속도 | Cache Hit 시 빠름 | 빠름 | 빠름 |
| 쓰기 속도 | 빠름 | 느림 (동기) | 매우 빠름 |
| 데이터 일관성 | 약간 불일치 가능 | 강함 | 약함 |
| 구현 복잡도 | 낮음 | 낮음 | 높음 |
| 메모리 효율 | 좋음 | 나쁨 | 좋음 |
대부분의 경우 Cache Aside로 시작합니다. 단순하고 효과적이며, 문제가 생겨도 캐시를 비우면 원상복구됩니다.
캐시된 데이터가 낡으면 잘못된 데이터가 서비스됩니다. 무효화 방법을 명확히 정해야 합니다.
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);
관련된 여러 캐시를 묶어서 무효화해야 할 때 씁니다.
캐시가 만료되는 순간 다수의 요청이 동시에 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;
}