본문 바로가기
C.W.K.
Stream
Lesson 03 of 05 · published

CDN 계층 — 브라우저에서 원본 서버까지 이어지는 캐시

~10 min · caching-perf, cdn, edge, origin

Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"캐시된 응답은 혼자 존재하지 않아. 브라우저에서 CDN 엣지와 보호 계층을 거쳐 원본까지 이어지는 계층 안에 살아. 같은 Cache-Control도 계층에 따라 적용 범위가 다르니, 어느 캐시에 무엇을 허용하는지 알아야 빠른 캐시와 위험한 캐시를 구분할 수 있어."

위에서 아래로 이어지는 구조

일반적인 웹 요청은 다음 계층을 지날 수 있어.

  1. 브라우저 캐시 — 한 사용자의 메모리나 디스크에 있는 개인 캐시야. 같은 사용자가 자원을 다시 찾을 때 적중하면 네트워크 전송 자체를 피할 수 있어.
  2. CDN 엣지 노드 — Cloudflare PoP, AWS CloudFront 엣지, Fastly POP처럼 가까운 접속 지점에서 여러 사용자가 공유해. 인기 콘텐츠가 적중하면 원본까지 요청이 가지 않아.
  3. CDN 보호 계층이나 중간 계층 — 선택적으로 두는 중앙 캐시야. 여러 엣지에서 동시에 난 미스를 모아 원본으로 향하는 요청 수를 줄여. Cloudflare의 계층형 캐시와 Fastly의 원본 보호가 이런 역할을 해.
  4. 원본과 앱 서버 — FastAPI, Node, Rails 같은 애플리케이션이 실제 응답을 만들어. 앞단에 Nginx나 Varnish 마이크로캐시가 더 놓일 수도 있어.

각 계층은 HTTP 응답 헤더를 읽어 저장과 재사용 여부를 판단하고, 미스나 재검증이 필요할 때 다음 계층으로 요청을 넘겨. 하나의 정책이 사슬을 따라 전달되지만, 각 캐시는 자신에게 적용되는 지시어만 해석해.

어느 헤더가 어느 계층을 겨냥할까

  • Cache-Control: max-age=N — 개인 캐시와 공유 캐시에 적용되는 일반 신선도 수명이야.
  • Cache-Control: s-maxage=N — CDN과 프록시 같은 공유 캐시에만 적용돼. 공유 캐시에서는 max-age를 덮어쓰고, 브라우저는 무시해.
  • private — 개인 캐시에만 저장하도록 제한해. 브라우저는 저장할 수 있지만 CDN은 저장하면 안 돼.
  • public — 공유 캐시를 포함한 캐시가 저장할 수 있음을 명시해. 공개 콘텐츠에 흔하지만, 데이터가 정말 모든 캐시 키 범위에서 공유 가능한지 먼저 확인해야 해.
  • no-store — 브라우저와 CDN을 포함해 어떤 캐시에도 저장하지 말라는 뜻이야. 토큰이나 결제 정보처럼 남겨서는 안 되는 응답에 써.
  • CDN 전용 헤더CDN-Cache-Control, Cloudflare-CDN-Cache-Control, Surrogate-Control처럼 특정 공유 캐시가 별도 정책을 읽게 하는 헤더야. 지원 범위와 우선순위는 제공자마다 다르므로 실제 CDN 문서를 확인해야 해.

강력한 패턴: 브라우저는 짧게, CDN은 길게

모든 사용자에게 같은 내용을 주지만 자주 갱신되는 뉴스 피드나 제품 카탈로그에는 이런 조합을 쓸 수 있어.

Cache-Control: public, max-age=10, s-maxage=600

브라우저에서는 10초 동안 신선하고, 공유 캐시에서는 10분 동안 신선해. 한 엣지에 처음 들어온 요청만 미스가 나고 뒤의 요청이 모두 적중한다고 가정하면, 그 10분 동안 사용자 수가 늘어도 원본 요청은 엣지마다 한 번 수준으로 줄어들 수 있어. 엣지 50곳에서 페이지 조회 100,000건이 고르게 발생하는 단순한 예라면 원본 요청이 약 50건까지 줄 수 있지만, 실제 수치는 지역 분포와 캐시 키, 축출 정책에 따라 달라져.

Cache-Control은 캐시 사슬에 전달되는 정책이지만, 모든 캐시가 똑같이 행동한다는 보장은 없어. 브라우저, 엣지, 보호 계층, 원본 프록시가 각자 적용 가능한 지시어를 해석해. 정책을 바꿔도 이미 저장된 응답이 즉시 사라지는 것은 아니므로, 새 응답이 도착하거나 재검증·제거가 일어나기 전까지 기존 정책이 남을 수 있다는 점도 기억해.

캐시 키 — 두 요청을 무엇으로 구분할까

캐시는 보통 요청 메서드와 대상 URL을 기본 키로 삼고, 응답의 Vary에 나열된 요청 헤더 값을 보조 키로 반영해. CDN 설정에 따라 쿼리 문자열이나 쿠키를 정규화하거나 별도 키 규칙을 추가할 수도 있어. 같은 URL에 서로 다른 Authorization 값이 들어오고 응답이 Vary: Authorization를 보냈다면 두 표현을 다른 항목으로 구분해야 해. 캐시 가능한 응답인데 필요한 구분이 빠지면 이런 문제가 생겨.

  • 사용자 A의 응답이 사용자 B에게 전달되어 사용자 간 데이터가 노출돼.
  • gzip만 지원하는 클라이언트가 해제할 수 없는 Brotli 본문을 받아. 이때는 Vary: Accept-Encoding이 빠진 거야.
  • JSON을 요청한 클라이언트가 HTML 표현을 받아. 이때는 Vary: Accept 같은 표현 구분이 빠진 거야.

캐시 키 오류는 조용히 잘못된 응답을 내보내서 더 위험해. 표준 콘텐츠 협상에는 Vary를 정확히 쓰고, CDN에서 키를 별도로 조정했다면 그 설정까지 함께 검토해야 해. 사용자별 데이터는 private이나 no-store로 공유 저장 자체를 막는 편이 더 안전할 때가 많아.

캐시 제거 — 저장된 응답을 즉시 무효화해야 할 때

콘텐츠를 고쳤는데 CDN의 기존 응답이 TTL이 끝날 때까지 남아 있을 수 있어. 즉시 무효화하는 방법은 크게 두 가지야.

  • URL로 제거 — 특정 URL의 캐시 항목을 지우라고 CDN API에 요청해. Cloudflare API나 Fastly API처럼 대상이 분명할 때 쓰는 방식이야.
  • 캐시 태그로 제거 — 응답에 Surrogate-Key: article-42 author-pippa처럼 태그를 붙이고 나중에 같은 태그를 가진 항목을 한꺼번에 무효화해. URL을 전부 나열하지 않고 관련 콘텐츠 묶음을 지울 수 있지만, 지원 방식은 CDN 상품과 설정에 따라 달라.

제거는 유용하지만 정상적인 신선도 설계를 대신하지는 못해. 변경 허용 시간을 반영한 TTL을 정하고, 잠시 오래된 응답을 보여도 되는 콘텐츠에는 stale-while-revalidate를 조합해 긴급 제거에 의존하는 상황을 줄여.

cwkPippa의 CDN 현실

cwkPippa WebUI는 localhost나 Tailscale에서 사용하므로 일반적인 공개 CDN을 거치지 않아. 반면 cwk-site는 Vercel 엣지 네트워크에서 제공되고, 공개 콘텐츠와 인증이 필요한 API가 서로 다른 캐시 정책을 가져야 해. 인증 관련 응답에는 no-store처럼 공유 저장을 막는 정책이 중요해. 같은 URL이 Accept-Language에 따라 한국어와 영어를 반환한다면 Vary: Accept-Language를 보내거나 로캘을 URL에 포함해 캐시 키가 표현을 확실히 구분하도록 해야 해. 핵심은 Vary 하나를 습관처럼 붙이는 게 아니라, 실제 표현 선택 기준과 캐시 키를 일치시키는 거야.

Code

짧은 브라우저 TTL과 긴 CDN TTL 조합·http
# 짧은 브라우저 TTL + 긴 CDN TTL — canonical 인기-content 패턴
GET /api/news HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=10, s-maxage=600
Vary: Accept-Encoding
ETag: "v17-abc"

# 번역:
# - 브라우저: 10s 후 refetch (혹은 304)
# - CDN edge: 600s 후 refetch (혹은 304)
# - 10분 100k 페이지 뷰면, origin 이 ~50 request 봄 (윈도우 당 edge 당 1)
CDN-Cache-Control과 Surrogate-Key로 계층별 정책 나누기·http
# CDN-specific override — CDN vs 브라우저 다른 정책
GET /api/article/42 HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60
CDN-Cache-Control: public, max-age=86400, stale-while-revalidate=604800
Surrogate-Key: article-42 author-pippa
Vary: Accept-Encoding, Accept-Language
ETag: "v17-abc"

# 브라우저가 Cache-Control: 60s 신선도.
# CDN 이 CDN-Cache-Control: 1일 신선도 + 1주 stale-while-revalidate.
# Surrogate-Key 가 tag 로 purge 가능: 'author-pippa tag 된 모든 거 evict'.
URL 기반 제거와 캐시 태그 기반 제거·bash
# CDN cache purge (Cloudflare 예)
curl -X POST 'https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache' \
  -H 'Authorization: Bearer CF_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"files":["https://example.com/api/article/42"]}'

# Tag 기반 purge (CDN 이 지원하면; Fastly 가 함)
curl -X POST 'https://api.fastly.com/service/SERVICE_ID/purge' \
  -H 'Fastly-Key: TOKEN' \
  -H 'Surrogate-Key: author-pippa'
# 전체 CDN footprint 에서 author-pippa tag 된 모든 cached 항목 evict.

External links

Exercise

Vercel이나 Cloudflare처럼 CDN 앞에 놓인 엔드포인트가 있다면 curl -i로 실제 응답을 검사해. (1) CDN이 해석하는 Cache-Control 지시어, (2) cf-cache-status, x-vercel-cache, age 같은 캐시 상태 헤더, (3) Vary 헤더를 찾아 의미를 설명해. 그다음 원본의 Cache-Control을 바꾸고 재배포한 뒤, 기존 캐시 항목에 새 정책이 언제 반영되는지 관찰해. 보너스로 안전한 연습 환경에서만 Accept-Language에 따라 공개 문구가 달라지는 응답의 Vary를 잠시 빼고, 서로 다른 언어 요청이 같은 캐시 항목으로 섞이는지 확인해.
Hint
Cloudflare는 cf-cache-status에 HIT, MISS, EXPIRED 같은 상태를 표시하고, Vercel은 x-vercel-cache에 HIT, MISS, STALE, REVALIDATED 같은 상태를 표시할 수 있어. age는 응답이 공유 캐시에 머문 시간을 초 단위로 보여 줘. 제공자와 경로에 따라 헤더 이름이나 동작이 다를 수 있으니 한 번의 요청으로 결론 내리지 말고 같은 URL을 여러 번 호출해. Vary 실험은 인증 정보나 개인 데이터가 전혀 없는 공개 테스트 응답에서만 진행해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.