← 모든 글

CDN 캐시 invalidation 의 함정

Cloudflare 앞단에 캐시를 뒀을 때 invalidation 타이밍이 왜 생각대로 동작하지 않는지, 우리가 겪은 사례와 해결책을 정리한다.

문제는 배포 직후 5분이었다

정적 파일을 CDN에 올리는 것 자체는 간단하다. 문제는 배포 직후다. 새 버전을 배포하고 브라우저를 새로고침해도 이전 JS 번들이 그대로 내려오는 경험을 우리 팀은 꽤 여러 번 반복했다.

원인은 단순했다. CDN 엣지 노드는 Cache-Control: max-age=31536000 같은 헤더를 충실하게 따른다. purge API를 호출해도 모든 엣지 노드에 전파되는 데 수십 초에서 수 분이 걸린다. 그 사이에 사용자가 요청을 보내면 낡은 캐시가 응답된다.

우리가 초기에 저지른 실수는 “purge 호출 = 즉시 반영”이라고 가정한 것이었다. 실제로는 전파 지연이 항상 존재한다.

우리가 선택한 해결 패턴

두 가지를 조합했다.

첫 번째는 파일명에 콘텐츠 해시를 붙이는 것이다. main.js 대신 main.a3f9c2.js처럼 빌드 도구가 자동으로 삽입하게 했다. 해시가 바뀌면 URL 자체가 달라지므로 캐시 충돌이 원천 차단된다. 단, HTML 파일은 이 방식을 적용할 수 없다.

두 번째는 HTML 파일에 짧은 TTL을 설정하는 것이다. Cache-Control: max-age=60, stale-while-revalidate=30으로 설정해 두면, 최악의 경우에도 60초 이내에 새 HTML이 내려온다. HTML이 새 해시 파일명을 참조하므로 연쇄적으로 모든 에셋이 갱신된다.

이 둘을 쓰면 purge API를 거의 호출할 필요가 없다.

주의해야 할 추가 함정들

Vary 헤더 무시 문제: Accept-Encoding이나 Accept-Language에 따라 다른 응답을 내려야 한다면, Vary 헤더를 정확히 명시해야 한다. CDN이 Vary를 무시하거나 부분적으로 지원하는 경우, 엉뚱한 압축 포맷이나 언어로 캐시된 응답이 나갈 수 있다.

API 응답 캐시: 정적 파일이 아닌 API 엔드포인트를 캐시할 때는 더 조심해야 한다. 사용자별로 달라지는 데이터인데 CDN이 공용 캐시에 넣어버리는 사고가 발생할 수 있다. Cache-Control: private 또는 no-store를 명시하지 않으면 CDN은 캐싱 후보로 간주할 수 있다.

purge 범위: 경로 기반 purge와 태그 기반 purge는 동작 방식이 다르다. 전체 purge는 원하지 않는 엣지까지 비워버려 원서버 트래픽이 일시적으로 급증한다. 가능하면 범위를 좁혀서 호출하는 것이 낫다.

정리

캐시 invalidation 은 어렵다고 많이들 이야기하지만, 우리가 겪은 문제의 대부분은 “파일명 해싱 + HTML 짧은 TTL” 두 가지만으로 해소됐다. purge API에 지나치게 의존하기 전에 이 조합을 먼저 시도해 보길 권한다.

— by slecs