CDN 캐시 invalidation 의 함정
CDN 캐시 무효화는 UX 문제가 아니다. 결제·정산 시스템에서 배포 직후 stale 캐시는 실제 트랜잭션 실패로 이어진다. HEDVION이 직접 겪은 함정과 대응 패턴을 공유한다.
결제 현장에서 배포 직후 5분은 단순한 UX 문제가 아니다
일반적인 프론트엔드 서비스라면 배포 직후 5분간 구버전 JS가 내려오는 상황이 UX 불편으로 끝난다. 사용자가 새 기능을 잠깐 못 쓰거나, 버튼이 조금 어색하게 작동하는 수준이다. 그런데 결제 플로우가 얽혀 있으면 이야기가 완전히 달라진다. 우리가 운영하는 결제 위젯은 특정 버전의 PG SDK를 번들에 포함한다. 배포로 SDK 버전을 올렸는데 구버전 JS가 CDN 캐시에서 내려오면, 해당 시간대에 들어오는 결제 시도는 결제창 호출 자체가 실패한다. 모니터링이 촘촘하지 않으면 조용히 지나칠 수도 있는 장애다.
더 심각한 케이스는 정산 어드민에서 발생했다. 정산 담당자가 배포 직후 어드민에서 정산 승인 버튼을 눌렀을 때, 낡은 JS가 이미 deprecated된 API 엔드포인트를 호출하면 승인 액션이 404로 조용히 실패한다. 사용자 입장에서는 버튼을 눌렀는데 아무 반응이 없는 것으로 보인다. 직접 겪은 케이스에서 이 상태가 약 4분간 지속됐고, 그 사이 정산 재처리 요청이 3건 들어왔다. 자동화 배치가 그 타이밍에 맞물렸다면 배치 실패로 번졌을 상황이었다. "배포 직후 5분"은 결제·정산 자동화를 운영하는 팀에게 실제 비용이 발생하는 구간이다.
"purge = 즉시 반영"이라는 착각과 전파 지연의 실체
CDN purge API를 호출하면 전 세계 엣지에 즉시 반영된다고 생각하기 쉽다. 실제로는 그렇지 않다. Cloudflare는 purge 요청이 전체 엣지 노드에 전파되는 데 평균 약 30초, 조건에 따라 수 분이 걸릴 수 있다고 공식 문서에서 밝히고 있다. Fastly는 "typically under 150ms globally"를 마케팅하지만, 실측에서는 동남아·남미 리전처럼 트래픽 허브에서 먼 엣지에서 2040초가 넘는 경우가 나온다. 우리가 함께 쓰는 CloudFront는 경로 기반 invalidation의 "완료" 상태를 폴링으로 확인해야 하고, 실제 전 리전 전파까지 최대 35분이 걸린 사례도 있었다.
이 전파 지연이 진짜 문제가 되는 지점은 CI/CD 파이프라인과 맞물릴 때다. 배포 후 purge를 걸고 Slack에 "배포 완료" 알림을 보내도, 실제로 서울 엣지에서 새 파일이 서빙되기까지는 수십 초가 더 필요할 수 있다. 우리 팀이 초기에 "purge 완료 = 즉시 반영"으로 가정하고 배포 완료 시그널 직후 정산 배치를 트리거했다가, 배치가 구버전 JS를 바라보는 어드민 API를 호출해서 실패한 일이 있었다. 타이밍 문제처럼 보이지만, 결제·정산 자동화에서 이 타이밍은 돈과 직결된다. purge를 신뢰하기 전에 purge 자체가 보장하는 것이 무엇인지부터 짚어야 한다.
파일명 해싱과 HTML 짧은 TTL: 조합의 위력과 트레이드오프
우리가 선택한 핵심 패턴은 두 가지를 조합하는 것이었다. 첫 번째는 콘텐츠 해시 기반 파일명이다. main.js 대신 main.a3f9c2.js처럼 빌드 산출물 파일명에 해시를 붙이면, 새 배포마다 URL 자체가 달라져서 캐시 충돌이 구조적으로 불가능해진다. Cache-Control: max-age=31536000, immutable을 설정해도 안전하다. Vite, webpack, Next.js 모두 기본 설정으로 지원하며, 빌드 설정 한 줄로 적용된다. 두 번째는 HTML 파일에 짧은 TTL 적용이다. HTML은 해시 기반 URL을 쓸 수 없으므로 Cache-Control: max-age=60, stale-while-revalidate=30을 설정했다. 최악의 경우에도 60초 이내에 새 HTML이 내려오고, HTML이 새 해시 파일명을 참조하므로 JS·CSS도 자동으로 교체된다.
이 조합에는 명확한 트레이드오프가 있다. HTML TTL 60초는 캐시 히트율을 낮춘다. 우리 팀에서 측정한 수치로는 결제 위젯 페이지 HTML 요청이 피크 타임 기준 분당 800~1,200회였고, max-age=60으로 전환한 후 원서버 부하가 약 40% 상승했다. 이를 보완하기 위해 stale-while-revalidate=30을 추가해서 캐시 만료 후 백그라운드에서 갱신하는 방식을 택했다. 응답 지연은 유지하면서 히트율을 올리는 절충이다. purge API 의존을 줄이는 대신 원서버 스케일링 비용이 소폭 늘었지만, 우리는 이게 훨씬 예측 가능한 트레이드오프라고 판단했다. purge 전파 타이밍의 불확실성보다, 40% 부하 상승은 수치로 관리할 수 있기 때문이다.
정산 API 캐시와 Vary 헤더: 조용히 터지는 폭탄
정적 파일보다 훨씬 조심해야 하는 건 API 응답 캐시다. CDN은 기본적으로 응답 헤더에 private이나 no-store가 명시되지 않으면 캐싱 후보로 간주할 수 있다. 우리가 운영하는 정산 조회 API는 동일한 경로(/api/settlements)라도 로그인한 파트너사 ID에 따라 완전히 다른 데이터를 반환한다. 초기 설정에서 헤더를 제대로 세팅하지 않았다가, A사 정산 데이터가 B사 요청에 응답되는 시나리오를 내부 테스트에서 재현했다. 실서비스 전에 잡았지만, 이건 단순 버그가 아니라 데이터 유출 사고다. 인증이 필요한 엔드포인트에는 반드시 Cache-Control: private 또는 no-store를 명시해야 한다.
Vary 헤더 문제도 실전에서 자주 발생한다. Vary: Accept-Encoding이 없으면 gzip 압축 응답과 일반 응답이 동일한 캐시 키를 공유해서 압축 포맷이 맞지 않는 응답이 내려갈 수 있다. 반대로 일부 CDN은 Vary 헤더가 있으면 아예 캐싱을 포기하기도 한다. 우리는 정산 API를 다국어로 제공할 때 Vary: Accept-Language를 설정했다가, 특정 CDN 규칙에서 한국어로 캐싱된 에러 메시지가 영어 요청에 그대로 나가는 것을 확인한 적 있다. 결제·정산 맥락에서 언어가 잘못된 오류 메시지가 파트너사에 나가면 신뢰 문제로 직결된다. CDN의 Vary 지원 범위는 반드시 벤더 문서에서 직접 확인해야 한다.
HEDVION 팀이라면 배포 파이프라인을 이렇게 구성한다
우리 팀의 실제 배포 플로우를 기준으로 정리하면 이렇다. 빌드 단계에서 Vite 기준 build.rollupOptions.output.entryFileNames에 [name].[hash].js를 명시해서 모든 JS·CSS 산출물에 콘텐츠 해시가 붙도록 강제한다. CDN 규칙은 경로 패턴 기반으로 두 버킷으로 나눈다. /*.html은 max-age=60, stale-while-revalidate=30, /assets/*는 max-age=31536000, immutable이다. 이 두 규칙만으로 purge 없이도 배포 후 최대 60초 안에 전체 에셋이 갱신되는 구조가 완성된다.
purge는 "보험"으로만 사용한다. 긴급 핫픽스처럼 HTML을 즉시 교체해야 할 때만 경로 기반 purge(/*.html만 대상)를 CI에서 트리거하고, Slack 배포 알림에는 "CDN 전파 완료까지 최대 2분 소요"를 명시해서 팀원이 배포 직후 바로 확인하러 달려드는 혼선을 줄였다. 정산 배치 자동화는 배포 완료 시그널 이후 3분 딜레이를 두고 시작하도록 파이프라인을 구성했다. 3분이 과도해 보일 수 있지만, purge 전파 최대 지연을 안전하게 커버하는 가장 단순한 방법이었고, 배치 실패로 인한 재처리 비용보다 훨씬 작다.
바로 써먹는 실행 체크리스트
결제·정산·자동화 서비스를 운영하는 팀이 CDN 캐시 설정을 처음 점검하거나 재정비할 때 즉시 적용할 수 있는 항목이다.
빌드·배포 설정 점검:
- JS·CSS 파일명에 콘텐츠 해시가 붙는지 빌드 산출물 폴더에서 직접 확인한다.
main.js라는 파일이 보이면 설정이 없는 것이다. - HTML 파일의
Cache-Control이 60초 이하 TTL로 설정됐는지 CDN 규칙을 확인한다. 설정이 없으면 벤더 기본값이 적용되며, Cloudflare 기본값은 Edge TTL 4시간이다. - 인증이 필요한 모든 API 응답에
Cache-Control: private또는no-store가 명시됐는지 응답 헤더를 curl로 직접 확인한다.
자동화 파이프라인 안전장치:
- 배포 완료 시그널 이후 배치·자동화를 트리거하는 구간이 있다면, 최소 2~3분 딜레이를 삽입한다. purge 전파 지연을 코드로 가정하지 않는다.
- 긴급 패치 시 전체 purge 대신 영향 경로만 대상으로 하는 스크립트를 사전에 준비해둔다. 전체 purge는 원서버 트래픽 급증(캐시 cold start)을 유발한다.
- Slack 배포 알림에 "CDN 전파 완료 예상 시간"을 명시해서 조기 확인으로 인한 오탐 리포트를 줄인다.
정기 점검 항목:
- 정산 어드민·내부 도구도 CDN 뒤에 있다면 캐시 정책 예외 대상인지 주기적으로 확인한다. 내부 도구는 "어차피 내부에서만 쓴다"는 이유로 캐시 설정이 허술해지기 쉽다.
- CDN 벤더가 바뀌거나 업데이트됐을 때 Vary 헤더 지원 범위가 달라질 수 있다. 분기마다 벤더 릴리스 노트를 확인하는 것을 루틴으로 넣어라.
캐시 invalidation은 어렵다는 말이 밈처럼 돌아다니지만, 우리가 겪은 장애의 대부분은 파일명 해싱과 HTML 짧은 TTL 두 가지로 구조적으로 해소됐다. purge API를 신뢰하기 전에, purge가 필요 없는 구조를 만드는 데 먼저 시간을 써라.
* 위 링크는 인프런 affiliate 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
* 위 추천 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.