--- id: cs-cache-eviction title: Cache Eviction — LRU / LFU / ARC / TinyLFU category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [cs, cache, eviction, vibe-coding] tech_stack: { language: "TS", applicable_to: ["Backend"] } applied_in: [] aliases: [LRU, LFU, ARC, TinyLFU, W-TinyLFU, cache eviction, hit rate] --- # Cache Eviction > Cache 가득 = 무엇을 빼지? **LRU (가장 오래 안 본), LFU (가장 적게 본), ARC (Adaptive), W-TinyLFU (Caffeine)**. Workload 따라 hit rate 큰 차이. ## 📖 핵심 개념 - LRU: Least Recently Used. - LFU: Least Frequently Used. - ARC: Adaptive — 둘 다 + auto. - TinyLFU / W-TinyLFU: 빈도 + 최근성 + admission. ## 💻 코드 패턴 ### LRU (가장 일반) ```ts class LRUCache { private cache = new Map(); // Map 가 insertion order 유지 constructor(private max: number) {} get(key: K): V | undefined { if (!this.cache.has(key)) return undefined; const v = this.cache.get(key)!; this.cache.delete(key); this.cache.set(key, v); // 최근 접근으로 return v; } set(key: K, value: V) { if (this.cache.has(key)) this.cache.delete(key); this.cache.set(key, value); if (this.cache.size > this.max) { const first = this.cache.keys().next().value; this.cache.delete(first!); // 가장 오래된 } } } ``` ### lru-cache (Node) ```ts import LRU from 'lru-cache'; const cache = new LRU({ max: 1000, ttl: 60_000, // 60초 TTL updateAgeOnGet: true, // get 시 age reset fetchMethod: async (key) => { return await db.users.find(key); }, }); const user = await cache.fetch('u1'); ``` ### LFU (빈도) ```ts class LFUCache { private cache = new Map(); constructor(private max: number) {} get(key: K): V | undefined { const entry = this.cache.get(key); if (!entry) return undefined; entry.freq++; return entry.value; } set(key: K, value: V) { if (this.cache.size >= this.max) { // 가장 적게 본 것 evict let minFreq = Infinity; let minKey: K | undefined; for (const [k, e] of this.cache) { if (e.freq < minFreq) { minFreq = e.freq; minKey = k; } } this.cache.delete(minKey!); } this.cache.set(key, { value, freq: 0 }); } } ``` ⚠️ Pure LFU 의 문제: cache pollution (오래된 popular 가 영원 남음). ### ARC (Adaptive Replacement Cache) ``` 4개 list 관리: - T1: recent (한 번 본) - T2: frequent (여러 번 본) - B1: T1 evict ghost - B2: T2 evict ghost Hit on B1 → T1 가중치 ↑ Hit on B2 → T2 가중치 ↑ Adaptive ``` → IBM 특허 — 사용 제한 가능. ### W-TinyLFU (Caffeine, Java) ``` Window LRU (1%) + Main (LFU + LRU). Admission policy: 새 entry 가 window 통과 → 빈도 비교 → 통과 시 main 으로. 매우 좋은 hit rate. ``` → Java Caffeine 가 이 알고리즘. ### Node — better-cache ```ts import { Cache } from 'cache-manager'; const cache = new Cache({ store: 'memory', max: 1000, ttl: 60_000, }); await cache.set('key', value); const v = await cache.get('key'); ``` ### Two-tier cache ``` L1: in-process (LRU, 작은) — ns access L2: Redis (큰) — ms access Get: L1 → miss → L2 → miss → DB ``` ```ts async function getUser(id: string): Promise { const l1 = lru.get(id); if (l1) return l1; const l2 = await redis.get(`user:${id}`); if (l2) { const user = JSON.parse(l2); lru.set(id, user); return user; } const user = await db.users.find(id); lru.set(id, user); await redis.setex(`user:${id}`, 60, JSON.stringify(user)); return user; } ``` ### Cache stampede (위 redis 문서 참조) ``` 1000 concurrent miss → 모두 DB 호출. → Singleflight / lock. ``` ```ts const inflight = new Map>(); async function getUser(id: string): Promise { const cached = lru.get(id); if (cached) return cached; if (inflight.has(id)) return inflight.get(id)!; const promise = db.users.find(id).then(user => { lru.set(id, user); inflight.delete(id); return user; }); inflight.set(id, promise); return promise; } ``` → 같은 key 동시 fetch = 1번만. ### TTL + jitter ```ts const ttl = 60_000 + Math.random() * 10_000; // 60-70s cache.set(key, value, { ttl }); ``` → 동시 expire 방지 (thundering herd). ### Cache key design ```ts // ❌ 너무 specific `user:${id}:${page}:${filter}:${sort}` // 매번 다른 key // ✅ 분리 `user:${id}` + 그 user 의 page list 별도 ``` ### Negative cache (없는 것도 cache) ```ts async function getUser(id: string): Promise { const cached = cache.get(id); if (cached === '__NULL__') return null; if (cached) return cached as User; const user = await db.users.find(id); if (!user) { cache.set(id, '__NULL__', { ttl: 30_000 }); // 짧게 return null; } cache.set(id, user); return user; } ``` → "없는 user" 반복 query 방지. ### Hit rate 측정 ```ts class InstrumentedCache { private hits = 0; private misses = 0; get(key: K) { const v = this.cache.get(key); if (v) { this.hits++; return v; } this.misses++; return undefined; } hitRate() { return this.hits / (this.hits + this.misses); } } ``` → 80%+ 가 일반 목표. ### Sized cache (memory budget) ```ts const cache = new LRU({ maxSize: 100 * 1024 * 1024, // 100 MB sizeCalculation: (value) => value.length, }); ``` ### Cache 종류 비교 ``` Code-level: Map / LRU In-memory shared: Memcached / Redis CDN: Cloudflare / CloudFront Browser: HTTP cache + Service Worker DB: shared_buffers / buffer pool ``` ### 패턴 ``` Cache-aside (look-aside): App 가 read miss 시 cache.set Read-through: Cache 가 자동 fetch (lru-cache fetchMethod) Write-through: Write 가 DB + cache 같이 Write-behind: Write cache → 비동기 DB Refresh-ahead: TTL 임박 시 미리 refresh ``` ## 🤔 의사결정 기준 | 워크로드 | 추천 | |---|---| | 일반 web | LRU | | Skewed (popular minority) | LFU / W-TinyLFU | | Adaptive | ARC (legal 시) / W-TinyLFU | | 큰 throughput | Caffeine (Java) / 자체 | | 분산 | Redis + 클라 LRU L1 | | Workload 다양 | W-TinyLFU 가 안정 | ## ❌ 안티패턴 - **TTL 없음**: stale. - **무한 size**: OOM. - **Hit rate 모니터링 X**: cache 효과 모름. - **Cache stampede 무시**: 동시 1000 miss = DB 다운. - **Negative cache 없음**: "없는 거" 반복 query. - **Key namespace 충돌**: prefix 명시. - **Cache pollution (모든 거 cache)**: 빈도 낮은 거 evict 자주. - **Pure LFU 사용 + 패턴 변화**: 옛 popular 가 영원. ## 🤖 LLM 활용 힌트 - LRU + TTL + jitter 가 안전 default. - Hot/cold 가 strict 면 W-TinyLFU. - Stampede = singleflight 또는 lock. - Hit rate 모니터링 + alarm. ## 🔗 관련 문서 - [[DB_Redis_Patterns]] - [[Web_HTTP_Cache_Headers]] - [[Backend_Rate_Limiting]]