TL;DR
- 컴퓨터 과학의 2가지 어려운 문제: cache invalidation, naming things, off-by-one errors (Phil Karlton)
- HTTP 캐시 헤더 마스터:
Cache-Control,ETag,Vary,If-None-Match - CDN 캐시 = 전 세계 분산 캐시: 사용자에게 가까운 PoP에서 응답 → 50ms 이하 latency
- Stale-While-Revalidate (SWR): 오래된 응답 즉시 반환 + 백그라운드 갱신 = 100% 캐시 히트율 체감
- Edge Computing: 캐시를 넘어 코드 실행 → Cloudflare Workers, Fastly Compute@Edge
1. 캐시는 왜 어려운가?
1.1 Phil Karlton의 명언
"There are only two hard things in Computer Science: cache invalidation and naming things."
이 농담은 반은 진실입니다. 캐시 무효화는 정말 어렵습니다.
1.2 두 가지 본질적 어려움
1. 언제 무효화할 것인가?
- 너무 빨리 → 캐시 효과 없음
- 너무 늦게 → stale data 문제
2. 어떻게 무효화할 것인가?
- 단일 키? 패턴? 전체?
- 동기? 비동기?
- 실패 시 어떻게?
1.3 캐시 일관성의 트레이드오프
강한 일관성 ←─────────────────→ 높은 성능
(실시간) (캐시 적중)
모든 시스템은 이 스펙트럼의 어딘가에 있습니다. 100% 둘 다 가질 수 없습니다.
2. HTTP 캐시 헤더 마스터하기
2.1 Cache-Control — 캐시의 핵심
Cache-Control: public, max-age=3600, s-maxage=86400
디렉티브:
| 값 | 의미 |
|---|---|
public | CDN, 브라우저 모두 캐시 가능 |
private | 브라우저만 (사용자별) |
no-cache | 캐시는 가능, 매번 검증 필요 |
no-store | 절대 캐시 안 됨 (민감 데이터) |
max-age=N | 브라우저 캐시 유효 시간 (초) |
s-maxage=N | CDN 캐시 유효 시간 (브라우저보다 우선) |
must-revalidate | 만료 후 반드시 검증 |
immutable | 절대 변하지 않음 (CSS, JS with hash) |
stale-while-revalidate=N | 만료 후 N초 동안 stale 사용 가능 |
stale-if-error=N | 에러 시 N초 동안 stale 사용 |
2.2 일반적 패턴
HTML (자주 변함):
Cache-Control: public, max-age=0, must-revalidate
API JSON (변할 수 있음):
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400
정적 파일 (해시 포함):
Cache-Control: public, max-age=31536000, immutable
민감 데이터 (사용자 정보):
Cache-Control: private, no-store
2.3 ETag — 효율적 검증
서버가 콘텐츠의 고유 식별자(해시)를 응답:
HTTP/1.1 200 OK
ETag: "abc123"
Cache-Control: max-age=3600
캐시 만료 후 클라이언트:
GET /api/users/123 HTTP/1.1
If-None-Match: "abc123"
서버가 콘텐츠가 같으면:
HTTP/1.1 304 Not Modified
ETag: "abc123"
304 Not Modified = body 없음. 대역폭 절약.
2.4 Last-Modified — 시간 기반
HTTP/1.1 200 OK
Last-Modified: Tue, 15 Apr 2025 10:00:00 GMT
GET /api/users/123 HTTP/1.1
If-Modified-Since: Tue, 15 Apr 2025 10:00:00 GMT
ETag보다 약함: 1초 이내 변경 감지 못 함, 시계 동기화 의존.
2.5 Vary — 캐시의 동일성 정의
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Vary: Accept-Encoding, Accept-Language
의미: "같은 URL이라도 Accept-Encoding(gzip vs br)과 Accept-Language(en vs ko)가 다르면 다른 캐시".
주의: Vary: User-Agent는 거의 무한 변형 → 캐시 무용지물. 절대 하지 마세요.
2.6 일반적 실수
1. no-cache ≠ "캐시 안 됨"
no-cache: 캐시는 가능, 사용 전 검증 필요no-store: 캐시 절대 X
2. Pragma: no-cache는 옛날 헤더
- HTTP/1.0 잔재
- 거의 무시되지만 호환성 위해 유지하는 곳 있음
3. Vary: *는 캐시 비활성화
- 모든 헤더가 다르면 다른 캐시 = 절대 hit 안 함
3. CDN 동작 원리
3.1 CDN이란?
Content Delivery Network — 전 세계에 분산된 캐시 서버 네트워크.
사용자 (서울) → [CDN PoP 서울] (캐시 hit!) → 응답
↓ miss
[Origin 서버 (미국)]
효과:
- 서울 사용자: 5ms (CDN PoP)
- 미국 origin 직접: 200ms (왕복 + 처리)
3.2 CDN의 구성 요소
| 컴포넌트 | 역할 |
|---|---|
| PoP (Point of Presence) | 도시별 캐시 서버 (Cloudflare는 300+) |
| Origin | 원본 서버 (당신의 서버) |
| Edge Cache | PoP의 캐시 저장소 |
| DNS | 가장 가까운 PoP로 라우팅 |
| Anycast | 같은 IP가 여러 PoP로 라우팅 |
3.3 캐시 키
CDN은 무엇으로 캐시를 식별하는가?
기본: URL만.
GET /api/users/123 → 캐시
GET /api/users/123?lang=en → 다른 캐시 (query string 포함)
Cloudflare Workers:
const cacheKey = new Request(url, { method: 'GET' })
const cache = caches.default
const cached = await cache.match(cacheKey)
3.4 cache hit ratio
가장 중요한 메트릭.
hit_ratio = cache_hits / (cache_hits + cache_misses)
| Hit Ratio | 의미 |
|---|---|
| 95%+ | 우수 |
| 90-95% | 좋음 |
| 80-90% | 개선 여지 |
| 80% 미만 | 캐시 전략 재검토 필요 |
1% 향상이 큰 의미: 96% → 97% = origin 트래픽 25% 감소.
3.5 CDN 비교
| Cloudflare | Fastly | CloudFront | Akamai | Vercel/Netlify | |
|---|---|---|---|---|---|
| PoP 수 | 300+ | 80+ | 600+ | 4000+ | 100+ |
| Edge 컴퓨팅 | Workers (V8) | Compute@Edge (Wasm) | Lambda@Edge | EdgeWorkers | Edge Functions |
| 무료 티어 | ✅ 관대 | ❌ | ❌ | ❌ | ✅ |
| 가격 | 매우 저렴 | 비쌈 | 보통 | 비쌈 | 보통 |
| 캐시 무효화 | 즉시 | 150ms (Instant Purge) | 분 단위 | 분 단위 | 분 단위 |
| DDoS 보호 | 우수 | 좋음 | 좋음 | 우수 | 보통 |
Cloudflare: 가성비 챔피언. 90% 사용 사례에 적합. Fastly: 빠른 무효화, 미디어/뉴스에 강함. CloudFront: AWS 통합이 강점. Akamai: 엔터프라이즈, 가장 많은 PoP.
4. Cache Invalidation — 가장 어려운 부분
4.1 4가지 무효화 전략
1. TTL 기반: 시간이 지나면 자동 만료
Cache-Control: max-age=3600 // 1시간 후 만료
장점: 단순. 단점: 1시간 동안 stale 가능.
2. Push (즉시 무효화): 콘텐츠 변경 시 CDN에 알림
curl -X POST https://api.cloudflare.com/zones/.../purge_cache \
-H "Authorization: Bearer ..." \
-d '{"files":["https://example.com/api/users/123"]}'
장점: 즉각적. 단점: 복잡, 비용.
3. ETag 기반: 매 요청마다 검증 (304 응답)
- 대역폭은 절약, 지연은 여전
4. Versioned URLs: 변경 시 새 URL
/static/main.abc123.js → /static/main.def456.js
장점: 무효화 자체가 불필요. CSS/JS에 표준.
4.2 Cloudflare의 무효화 API
# 단일 URL
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
-H "Authorization: Bearer {api_token}" \
-d '{"files":["https://example.com/page1"]}'
# 태그 기반 (Enterprise)
curl -X POST ... -d '{"tags":["user-123"]}'
# 전체
curl -X POST ... -d '{"purge_everything":true}'
4.3 Cache Tags — 우아한 무효화
Fastly와 Cloudflare Enterprise가 지원:
HTTP/1.1 200 OK
Cache-Tag: user-123, post-456, blog-list
무효화:
curl -X POST https://api.fastly.com/service/{id}/purge/user-123
→ user-123 태그가 붙은 모든 캐시 즉시 무효화.
사용 예시: 사용자가 프로필 업데이트 → user-123 태그가 붙은 모든 페이지 무효화.
4.4 무효화의 어려운 시나리오
시나리오 1: 사용자가 댓글 작성
영향받는 페이지:
- /post/123 (해당 글)
- /post/123/comments
- /user/abc/comments (작성자 페이지)
- /sidebar/recent-comments
- ... 그리고 더 많은
→ 모든 페이지를 일일이 무효화? 태그 기반이 답.
시나리오 2: 데이터베이스 일관성
DB 트랜잭션 commit → 캐시 무효화
↓ 만약 캐시 무효화가 실패하면?
stale data!
→ 재시도 + dead letter queue. 또는 eventual consistency를 받아들이기.
시나리오 3: 무효화 폭증
블랙 프라이데이 = 가격 자주 변경 = 무효화 폭증 → CDN API rate limit.
→ 배치 무효화, 태그 사용, TTL 줄이기.
5. Stale-While-Revalidate — 게임 체인저
5.1 문제
전통적 캐시:
[캐시 만료] → [origin 요청] → [기다림] → [응답]
↑ 사용자가 기다림 (slow!)
5.2 Stale-While-Revalidate
Cache-Control: max-age=60, stale-while-revalidate=86400
의미:
- 60초간: 신선한 캐시 사용
- 60초 ~ 86460초: stale 캐시 즉시 반환 + 백그라운드에서 갱신
- 86460초 후: 만료 (origin 요청)
5.3 사용자 경험
사용자 1 (60초): [캐시 hit, fresh] instant ✅
사용자 2 (61초): [캐시 hit, stale] instant ✅ (백그라운드 갱신 시작)
사용자 3 (62초): [캐시 hit, fresh] instant ✅ (방금 갱신됨)
모든 사용자가 instant 응답! origin은 가끔만 호출됨.
5.4 Next.js의 ISR (Incremental Static Regeneration)
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 60 // 60초 후 백그라운드 재생성
}
}
내부적으로 SWR 패턴 사용. 정적 속도 + 동적 데이터.
5.5 stale-if-error
Cache-Control: max-age=60, stale-if-error=86400
의미: origin 에러 시 24시간 동안 stale 데이터 반환.
효과: origin 다운에도 사이트 살아있음. 장애 회복력 핵심.
6. Cache 패턴 — 코드 레벨
6.1 Cache-Aside (Lazy Loading)
def get_user(user_id):
user = cache.get(f"user:{user_id}")
if user is None:
user = db.query(f"SELECT * FROM users WHERE id={user_id}")
cache.set(f"user:{user_id}", user, ttl=3600)
return user
가장 흔함. 단순, 안정적. 단점: cache miss 시 첫 요청은 느림.
6.2 Read-Through
캐시가 직접 origin을 호출 (애플리케이션은 캐시만 호출).
# 캐시 라이브러리가 자동 처리
user = cache.get(f"user:{user_id}", loader=lambda: db.query(...))
장점: 코드 단순. 단점: 캐시 라이브러리 의존.
6.3 Write-Through
쓰기 시 캐시도 동시 업데이트.
def update_user(user_id, data):
db.execute(f"UPDATE users SET ... WHERE id={user_id}")
cache.set(f"user:{user_id}", data, ttl=3600)
장점: 캐시 항상 최신. 단점: 쓰기 느림, 캐시 활용 안 될 데이터도 캐싱.
6.4 Write-Behind (Write-Back)
쓰기를 캐시에만 → 비동기로 DB에 반영.
def update_user(user_id, data):
cache.set(f"user:{user_id}", data, ttl=3600)
queue.push({"action": "update", "id": user_id, "data": data})
장점: 매우 빠른 쓰기. 단점: 캐시 장애 시 데이터 손실 위험.
6.5 Cache Stampede 방지
문제: 인기 키가 만료되면 수천 요청이 동시에 origin을 호출.
해결:
1. Mutex (분산 락)
def get_user(user_id):
user = cache.get(f"user:{user_id}")
if user is None:
with cache.lock(f"lock:user:{user_id}", timeout=10):
user = cache.get(f"user:{user_id}") # double-check
if user is None:
user = db.query(...)
cache.set(f"user:{user_id}", user, ttl=3600)
return user
2. Probabilistic Early Expiration TTL 직전부터 일부 요청은 미리 갱신:
def get_with_recompute(key):
value, ttl = cache.get_with_ttl(key)
if random() < ttl_factor(ttl):
# 일부 요청만 미리 재계산
recompute_async(key)
return value
3. Stale-While-Revalidate (이미 본 것)
7. Edge Computing — 캐시를 넘어서
7.1 Edge에서 코드 실행
CDN은 더 이상 단순 캐시가 아닙니다. 코드도 실행:
- Cloudflare Workers (V8 isolate)
- Fastly Compute@Edge (WebAssembly)
- AWS Lambda@Edge (Node.js, Python)
- Vercel Edge Functions (V8)
- Deno Deploy (V8)
7.2 사용 사례
1. A/B 테스팅
export default {
async fetch(request) {
const variant = Math.random() < 0.5 ? 'a' : 'b'
return fetch(`https://origin.com/page-${variant}`)
}
}
2. Geo 라우팅
const country = request.cf.country
const origin = country === 'KR' ? 'asia.api.com' : 'us.api.com'
return fetch(origin + new URL(request.url).pathname)
3. 인증/인가
const token = request.headers.get('Authorization')
const user = await verifyJWT(token)
if (!user) return new Response('Unauthorized', { status: 401 })
return fetch(origin)
4. HTML 변환
const response = await fetch(origin)
return new HTMLRewriter()
.on('h1', { element(el) { el.setInnerContent('Modified!') } })
.transform(response)
5. API 집계
const [user, posts] = await Promise.all([
fetch('https://api.com/user/123').then(r => r.json()),
fetch('https://api.com/posts?user=123').then(r => r.json())
])
return new Response(JSON.stringify({ user, posts }))
7.3 Edge 컴퓨팅의 한계
- CPU 시간 제한: 보통 10-50ms (Cloudflare Workers는 50ms)
- 메모리 제한: 128MB (Cloudflare), 50MB (Fastly)
- 콜드 스타트: V8 isolate는 거의 없음 (5ms 이하)
- 데이터베이스 접근: edge에서 origin DB는 느림 → Edge DB 필요 (Turso, D1, Neon)
7.4 Edge 데이터 저장소
| Cloudflare | Fastly | Vercel | |
|---|---|---|---|
| KV | Workers KV | Object Store | KV |
| DB | D1 (SQLite) | - | Neon, Turso |
| Object | R2 | - | Blob |
| Cache | Cache API | Cache | - |
| Vector | Vectorize | - | - |
8. CDN 캐시 베스트 프랙티스
8.1 정적 자산
# CSS, JS (해시 파일명)
Cache-Control: public, max-age=31536000, immutable
# 이미지 (잘 안 변함)
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
8.2 HTML
# 자주 변하지만 캐시 가능
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400
max-age=0: 브라우저는 매번 검증s-maxage=300: CDN은 5분 캐시stale-while-revalidate=86400: 24시간 동안 stale 사용
8.3 API JSON
# 사용자 별도 데이터 (private)
Cache-Control: private, max-age=60, must-revalidate
# 공개 API (public)
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400
8.4 사용자 별 콘텐츠
# 절대 캐시 안 됨
Cache-Control: private, no-store
또는 cache key에 Cookie 포함 (위험 — 카디널리티 폭증).
8.5 압축 + 캐시
HTTP/1.1 200 OK
Content-Encoding: gzip
Cache-Control: public, max-age=3600
Vary: Accept-Encoding
Vary: Accept-Encoding 필수 — gzip vs br vs none이 다른 캐시.
9. 모니터링과 디버깅
9.1 응답 헤더 확인
curl -I https://example.com/api/users/123
HTTP/2 200
cf-cache-status: HIT
age: 234
cache-control: public, max-age=3600
주요 헤더:
cf-cache-status: HIT, MISS, EXPIRED, BYPASS, REVALIDATEDage: 캐시된 지 N초x-cache: AWS CloudFront 헤더
9.2 캐시 상태
HIT ← 캐시에서 반환
MISS ← origin 요청
EXPIRED ← 만료, 갱신 중
REVALIDATED ← 304로 검증됨
BYPASS ← 캐시 우회 (no-cache 등)
DYNAMIC ← 캐시 못 함
9.3 캐시 히트율 추적
대부분 CDN 대시보드에 표시:
- Cloudflare Analytics
- Fastly Insights
- CloudFront Reports
9.4 디버깅 흐름
캐시 hit 안 됨 → 다음을 확인:
- Cache-Control 헤더:
no-cache,private있나? - Set-Cookie: 쿠키 있으면 캐시 안 됨
- Vary: 너무 많은 변형
- Method: POST는 캐시 안 됨 (대부분)
- Status code: 200, 301, 302, 404, 410만 캐시
- Query string: CDN이 query string 무시 설정?
10. 실전 — 블로그 사이트 캐싱
10.1 시나리오
- WordPress 또는 Next.js 블로그
- 일일 100만 페이지뷰
- 게시물이 자주 업데이트됨
- 댓글 시스템
10.2 캐시 전략
HTML 페이지:
Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400
정적 자산:
Cache-Control: public, max-age=31536000, immutable
API JSON:
Cache-Control: public, max-age=300, stale-while-revalidate=86400
10.3 무효화
게시물 업데이트 시:
- 태그 무효화:
post-{id},category-{slug},homepage - 새 URL: 정적 자산은 해시 변경
// Next.js + Cloudflare
async function updatePost(id, data) {
await db.update(...)
await cf.purgeByTags([`post-${id}`, 'homepage'])
}
10.4 결과
- Cache hit ratio: 95%+
- Origin requests: 5만/일 (95% 절감)
- 응답 시간: 50ms (CDN) vs 500ms (origin)
- 비용: 95% 절감
퀴즈
1. no-cache와 no-store의 차이는?
답: no-cache: 캐시 가능, 단 사용 전 반드시 origin에 검증 요청 (If-None-Match). 304 받으면 캐시 사용. 의외로 캐시 활용 가능. no-store: 캐시 절대 불가능. 요청, 응답을 어디에도 저장하지 않음 (메모리, 디스크). 민감 데이터에 사용. 흔한 실수: "캐시 안 됨"을 원할 때 no-cache를 사용 → 실제로는 캐시되고 검증만 함.
2. Stale-While-Revalidate가 게임 체인저인 이유는?
답: 사용자에게 항상 instant 응답을 줍니다. 캐시 만료 후에도 stale 데이터를 즉시 반환하고, 백그라운드에서 갱신. 사용자는 기다리지 않고, 다음 요청부터 새 데이터. 결과: 체감 cache hit ratio 100%, origin 트래픽은 여전히 적음. Next.js ISR이 이 패턴 사용. SWR은 캐시 만료의 본질적 트레이드오프(신선함 vs 속도)를 거의 해결합니다.
3. Cache Stampede 방지 방법 3가지는?
답: (1) Mutex/분산 락 — 만료된 키에 대해 한 요청만 origin을 호출, 나머지는 결과를 기다림 (또는 stale 사용), (2) Probabilistic Early Expiration — TTL 직전부터 일부 요청을 미리 재계산, (3) Stale-While-Revalidate — 만료된 캐시도 사용하면서 백그라운드 갱신. 인기 키일수록 이러한 보호가 중요. Reddit, Facebook이 비슷한 패턴 사용.
4. CDN 캐시 hit ratio가 1% 향상되면?
답: origin 트래픽이 25%+ 감소합니다. 95% → 96%면 origin 요청은 5% → 4%로 감소 = 20% 절감. 96% → 97%면 4% → 3% = 25% 절감. 캐시 히트율이 높아질수록 1%의 가치가 커집니다. 이는 (1) 서버 비용, (2) 대역폭 비용, (3) 응답 시간, (4) 신뢰성 모두에 영향. 0.5% 향상도 큰 비즈니스 가치.
5. Edge Computing이 캐시를 넘어선 의미는?
답: CDN은 더 이상 정적 파일 캐싱만 하지 않습니다. 사용자에게 가까운 노드에서 코드를 실행합니다. 사용 사례: A/B 테스팅, geo 라우팅, 인증, HTML 변환, API 집계. 결과: 동적 콘텐츠도 50ms 이하. Cloudflare Workers는 V8 isolate로 콜드 스타트 5ms 이하. Fastly Compute@Edge는 Wasm 기반. Origin 서버의 역할이 줄어들고, edge가 점점 더 많은 일을 합니다.
참고 자료
- MDN HTTP Caching
- RFC 7234 — HTTP Caching
- RFC 5861 — Stale Content — SWR 정의
- Cloudflare Cache Docs
- Fastly Caching Best Practices
- HTTP Caching Best Practices — Google
- Cache Stampede
- SWR React Library — Vercel
- Cloudflare Workers
- Fastly Compute@Edge
- Vercel Edge Functions
- HTTP Caching Decision Tree — Harry Roberts
현재 단락 (1/353)
- **컴퓨터 과학의 2가지 어려운 문제**: cache invalidation, naming things, off-by-one errors (Phil Karlton)