Refresh-Ahead 캐시 패턴은 캐시 항목이 만료되기 전에 미리 갱신해 캐시 미스 없이 항상 신선한 데이터를 제공하는 패턴이다. 높은 가용성과 낮은 지연이 필요한 시스템에 적합하다.
동작 원리
일반 캐시:
만료 → 요청 → 캐시 미스 → DB 조회(느림) → 응답
Refresh-Ahead:
TTL의 N% 시점에 백그라운드에서 미리 갱신
요청 시 항상 캐시 히트
예: TTL = 60초, Refresh Factor = 0.8
48초(80%) 지점에서 백그라운드 갱신 시작
요청은 항상 신선한 캐시 데이터 반환
구현 예시
typescript
interface CacheEntry<T> {
value: T;
expiresAt: number;
refreshAfter: number; // 이 시간 이후 미리 갱신
}
class RefreshAheadCache<T> {
private refreshFactor: number; // 0.0 ~ 1.0
private refreshing = new Set<string>();
constructor(
private redis: Redis,
private loader: (key: string) => Promise<T>,
private ttl: number,
refreshFactor = 0.8
) {
this.refreshFactor = refreshFactor;
}
async get(key: string): Promise<T | null> {
const raw = await this.redis.get(key);
if (!raw) {
// 완전 미스: 동기 로드
return this.load(key);
}
const entry: CacheEntry<T> = JSON.parse(raw);
const now = Date.now();
// 미리 갱신 필요 여부 확인
if (now > entry.refreshAfter && !this.refreshing.has(key)) {
this.refreshing.add(key);
// 백그라운드에서 비동기 갱신 (응답 지연 없음)
this.load(key).finally(() => this.refreshing.delete(key));
}
return entry.value; // 현재 캐시 즉시 반환
}
private async load(key: string): Promise<T> {
const value = await this.loader(key);
const now = Date.now();
const entry: CacheEntry<T> = {
value,
expiresAt: now + this.ttl * 1000,
refreshAfter: now + this.ttl * this.refreshFactor * 1000,
};
await this.redis.setex(key, this.ttl, JSON.stringify(entry));
return value;
}
}
캐시 패턴 최종 비교
| 패턴 | 캐시 미스 지연 | 데이터 신선도 | 복잡도 | 적합 케이스 |
|---|
| Cache-Aside | 높음 | 중간 | 낮음 | 일반 캐싱 |
| Read-Through | 높음 | 중간 | 중간 | 단순 조회 |
| Write-Through | 없음 | 높음 | 중간 | 쓰기 일관성 |
| Write-Behind | 없음 | 중간 | 높음 | 고빈도 쓰기 |
| Refresh-Ahead | 없음 | 높음 | 높음 | 고가용성 조회 |
주의사항
Refresh Factor 튜닝:
너무 낮음 (0.5): DB 부하 높음 (자주 갱신)
너무 높음 (0.99): 만료 직전까지 갱신 안 함
변동성 높은 데이터:
실시간 가격, 재고 → Refresh-Ahead 부적합
(미리 갱신해도 금방 무효화)
안정적 데이터:
상품 정보, 사용자 프로필 → 적합
관련 개념
- •Read-Through 캐시 패턴
- •Write-Behind 캐시 패턴
- •셀 기반 아키텍처