CDN의 캐시 적중률은 사이트 성능과 오리진 비용을 동시에 좌우합니다. 그런데 캐시는 단순히 max-age=3600 한 줄로 끝나지 않습니다. 진짜 효과를 보려면 변경 시 즉시 반영만료 직후 사용자 대기 시간을 동시에 해결해야 합니다.

CDN 캐시 정책 — Stale-While-Revalidate 흐름


캐시 헤더의 기본

가장 중요한 두 헤더는 Cache-ControlETag입니다.

Cache-Control: public, max-age=300, s-maxage=86400, stale-while-revalidate=3600
ETag: "abc123def"
  • public — 누구나 캐시 가능 (사용자별 데이터 없음)
  • max-age=300 — 브라우저 캐시 5분
  • s-maxage=86400 — CDN(공유 캐시) 1일
  • stale-while-revalidate=3600 — 만료 후 1시간 stale 허용
  • ETag — 콘텐츠 식별자, 변경 시 다른 값

Stale-While-Revalidate (SWR)

전통적 캐시 동작:

요청 → Fresh? → 캐시 응답
           ↓ no
        오리진 호출 → 사용자 대기 (느림)

SWR 적용:

요청 → Fresh? → 캐시 응답
           ↓ no (stale 윈도우 내)
        Stale 응답 즉시 (사용자 빠름)
        + 백그라운드로 재검증 → 다음 요청부터 Fresh

사용자는 만료 직후에도 즉시 응답을 받고, 캐시는 백그라운드에서 갱신됩니다. 평균 응답 시간이 극적으로 개선됩니다.

예시 설정

# 오리진(Nginx) 응답 헤더
add_header Cache-Control "public, max-age=60, stale-while-revalidate=86400" always;
브라우저 입장:
- 60초간 캐시 사용 (즉시 응답)
- 60~86400초 사이면 stale 응답 + 백그라운드 갱신
- 86400초 이후엔 오리진 호출 강제

CDN 측 동작은 Cloudflare·Fastly·Akamai 모두 SWR을 지원합니다(설정 명칭은 약간 다름).


ETag와 Last-Modified — 조건부 요청

캐시가 만료됐을 때 항상 전체 콘텐츠를 다시 받을 필요는 없습니다. 변경되지 않았으면 헤더만 받고 끝낼 수 있습니다.

요청:
GET /style.css HTTP/2
If-None-Match: "abc123def"

응답 (변경 없음):
HTTP/2 304 Not Modified
ETag: "abc123def"
(본문 없음)

ETag는 콘텐츠 해시(파일이 같으면 같은 값)가 일반적입니다. Last-Modified는 파일 변경 시간 기준이며 정확도가 떨어집니다.

Nginx는 정적 파일에 ETag를 자동 생성합니다. 동적 페이지라면 애플리케이션에서 직접 계산해 헤더로 내야 합니다.


Vary 헤더 — 캐시 분기

같은 URL이라도 사용자에 따라 다른 콘텐츠가 나가야 할 수 있습니다.

Vary: Accept-Encoding
Vary: User-Agent
Vary: Accept-Language

Vary로 명시한 헤더가 다르면 CDN은 별도로 캐시합니다.

  • Accept-Encoding — gzip·brotli 별 분리 (필수)
  • Accept-Language — 다국어 페이지 분리
  • User-Agent — 모바일·데스크톱 분리 (캐시 적중률 떨어뜨림, 신중)

너무 많은 Vary는 캐시 적중률을 망칩니다. 꼭 필요한 것만 추가하세요.


Cache-Key 커스터마이징

기본 Cache-Key는 URL이지만, CDN별로 키를 조정할 수 있습니다.

Cloudflare 예시:

Cache Key:
  - Hostname: include
  - Path + Query string: include
  - Cookies: exclude (또는 화이트리스트만)
  - User-Agent: exclude

쿠키를 무시하면 로그인 사용자도 동일 캐시 적중. 반대로 일부 쿠키(예: 다크모드)는 키에 포함하고 싶을 수도 있습니다.


Surrogate-Key / Cache-Tag — 빠른 무효화

수많은 URL을 한 번에 무효화하려면 URL 패턴 매칭이 비효율적입니다. 태그 기반 무효화가 답입니다.

응답 헤더:
Surrogate-Key: blog post-123 author-jane category-tech
또는 (Cloudflare/Fastly):
Cache-Tag: blog post-123 author-jane category-tech

CMS에서 글을 수정하면 post-123 태그가 붙은 모든 캐시(목록 페이지·상세 페이지·작가 페이지)를 한 번에 무효화:

# Fastly API 예시
curl -X POST "https://api.fastly.com/service/SERVICE_ID/purge/post-123" \
  -H "Fastly-Key: $TOKEN"

URL 일일이 명시하지 않아도 됩니다. 대규모 사이트에서 핵심 기능.


캐시 가능성 분석

페이지를 캐시 가능한 것과 불가능한 것으로 분류해야 정책이 깔끔해집니다.

캐시 가능 (긴 TTL)

  • 정적 자원: CSS·JS·이미지·폰트 (파일명에 해시 → 영구 캐시)
  • 공개 페이지: 블로그 글, 상품 페이지
  • 자주 안 바뀌는 API 응답

캐시 가능 (짧은 TTL + SWR)

  • 홈페이지, 카테고리 페이지
  • 뉴스·실시간성 콘텐츠
  • 검색 결과 (인기 키워드)

캐시 불가

  • 로그인 사용자 페이지
  • 결제·주문 페이지
  • 사용자별 맞춤 콘텐츠
  • 실시간 데이터 (재고·가격)

명시적으로 캐시 안 하려면:

Cache-Control: no-store
또는
Cache-Control: private, no-cache

파일명 해시 — 영구 캐시의 핵심

정적 자원에 콘텐츠 해시를 파일명에 포함하면 영구 캐시 가능합니다.

<!-- 옛 방식 (캐시 문제) -->
<link rel="stylesheet" href="/style.css">

<!-- 해시 포함 (영구 캐시 가능) -->
<link rel="stylesheet" href="/style.4f8e1b.css">
Cache-Control: public, max-age=31536000, immutable

immutable 키워드는 “이 자원은 절대 안 바뀐다” — 브라우저가 새로고침 시에도 재검증 안 함. webpack·vite·rollup 같은 빌드 도구가 자동으로 해시를 생성합니다.


동적 페이지의 캐싱 — Edge Includes

페이지 대부분은 정적이지만 일부 영역(예: 로그인 메뉴)만 사용자별이라면, ESI(Edge Side Includes)를 활용합니다.

<!-- 캐시 가능한 본문 -->
<header>
  <esi:include src="/api/user-menu" />
</header>
<article>...본문...</article>

CDN이 본문은 캐시하고, <esi:include> 부분만 매번 가져옵니다. Varnish·Fastly가 강력하게 지원합니다.

비슷한 패턴으로 클라이언트 측 페치(JS로 로그인 메뉴만 fetch)도 자주 쓰입니다.


캐시 적중률 측정

캐시 정책의 효과는 적중률(Hit Ratio) 로 측정합니다.

적중률 = (Edge에서 응답한 요청) / (전체 요청)
  • 정적 사이트 + 좋은 정책: 95%+
  • 일반 동적 사이트: 60~80%
  • 사용자별 페이지 위주: 20~40%

CDN 대시보드에서 확인 가능합니다. 적중률이 낮으면 다음을 점검:

  • Cache-Control 헤더 누락
  • 잘못된 Vary
  • 쿠키가 Cache-Key에 포함됨
  • TTL이 너무 짧음

점진적 도입 전략

처음부터 완벽한 정책을 짤 필요는 없습니다.

1단계 — 정적 자원만

이미지·CSS·JS에만 긴 TTL.

Cache-Control: public, max-age=31536000, immutable

2단계 — 공개 HTML에 짧은 TTL + SWR

Cache-Control: public, s-maxage=60, stale-while-revalidate=86400

3단계 — Cache-Tag 도입

CMS 변경 시 자동 무효화. 적중률 90%+ 목표.

4단계 — ESI 또는 Client Fetch

동적 영역만 분리해 더 많은 페이지를 캐시 가능하게.


마무리

좋은 캐시 정책은 한 줄로 끝나지 않지만, 효과는 즉각적입니다. SWR + Cache-Tag + 파일명 해시 세 가지만 잘 잡으면 오리진 부하의 90%를 줄이고, 사용자 응답 시간을 절반 이하로 만들 수 있습니다.

TCP-80.NET의 전용서버 + Cloudflare·Fastly 조합으로 글로벌 사용자에게 빠른 응답을 제공하실 수 있으며, 캐시 헤더 설계 가이드는 @tcp80net으로 문의해 주세요.