Skip to content
Published on

CDN & Edge 캐싱 전략 완전 가이드 2025: Cache Invalidation, ETag, Stale-While-Revalidate

Authors

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

디렉티브:

의미
publicCDN, 브라우저 모두 캐시 가능
private브라우저만 (사용자별)
no-cache캐시는 가능, 매번 검증 필요
no-store절대 캐시 안 됨 (민감 데이터)
max-age=N브라우저 캐시 유효 시간 (초)
s-maxage=NCDN 캐시 유효 시간 (브라우저보다 우선)
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 CachePoP의 캐시 저장소
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 비교

CloudflareFastlyCloudFrontAkamaiVercel/Netlify
PoP 수300+80+600+4000+100+
Edge 컴퓨팅Workers (V8)Compute@Edge (Wasm)Lambda@EdgeEdgeWorkersEdge 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 데이터 저장소

CloudflareFastlyVercel
KVWorkers KVObject StoreKV
DBD1 (SQLite)-Neon, Turso
ObjectR2-Blob
CacheCache APICache-
VectorVectorize--

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, REVALIDATED
  • age: 캐시된 지 N초
  • x-cache: AWS CloudFront 헤더

9.2 캐시 상태

HIT           ← 캐시에서 반환
MISS          ← origin 요청
EXPIRED       ← 만료, 갱신 중
REVALIDATED304로 검증됨
BYPASS        ← 캐시 우회 (no-cache 등)
DYNAMIC       ← 캐시 못 함

9.3 캐시 히트율 추적

대부분 CDN 대시보드에 표시:

  • Cloudflare Analytics
  • Fastly Insights
  • CloudFront Reports

9.4 디버깅 흐름

캐시 hit 안 됨 → 다음을 확인:

  1. Cache-Control 헤더: no-cache, private 있나?
  2. Set-Cookie: 쿠키 있으면 캐시 안 됨
  3. Vary: 너무 많은 변형
  4. Method: POST는 캐시 안 됨 (대부분)
  5. Status code: 200, 301, 302, 404, 410만 캐시
  6. 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 무효화

게시물 업데이트 시:

  1. 태그 무효화: post-{id}, category-{slug}, homepage
  2. 새 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-cacheno-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가 점점 더 많은 일을 합니다.


참고 자료