Cache-Control: immutable인데 왜 Cloudflare는 계속 DYNAMIC이었을까요?
CDN을 붙이고 응답 헤더에 긴 캐시 시간을 설정했는데도 CF-Cache-Status가 계속 DYNAMIC으로 표시되는 경우가 있습니다. 저희도 파일 제공 기능을 점검하면서 같은 상황을 만났습니다.
처음에는 캐시 헤더가 잘못되었거나 Cloudflare 설정이 아직 전파되지 않았다고 생각했습니다. 하지만 원인은 헤더의 강도가 아니라 URL 형태에 있었습니다.
이번 글에서는 확장자가 없는 파일 URL이 왜 기본 캐시 대상에서 제외되었는지, 어떤 실험으로 원인을 좁혔는지, 그리고 URL을 바꿀 때 놓치기 쉬운 부분까지 정리해보겠습니다.
immutable은 캐시 적격성을 보장하지 않습니다
파일 응답에는 다음과 같은 헤더가 설정되어 있었습니다.
Cache-Control: public, max-age=31536000, immutable이 헤더만 보면 충분히 강한 캐시 정책처럼 보입니다. public은 공유 캐시가 응답을 저장할 수 있다는 뜻이고, max-age는 신선도 유지 시간을 지정합니다. immutable은 해당 URL의 내용이 유효 기간 동안 바뀌지 않는다는 힌트입니다.
다만 이 지시어들은 이미 캐시 대상으로 판단된 응답을 어떻게 다룰지 설명합니다. 요청이 Cloudflare의 캐시 대상이 되는지는 그보다 앞선 단계에서 결정됩니다.
Cloudflare 공식 문서에서도 DYNAMIC을 요청 시점에 캐시 대상이 아니라고 판단된 상태로 설명합니다. 반면 BYPASS는 일단 캐시 대상으로 판단했지만, 오리진 응답 헤더 등의 이유로 저장하지 않은 상태입니다. 두 상태는 원인을 찾는 출발점부터 다릅니다. Cloudflare 캐시 상태 설명
먼저 비교 실험을 해봤습니다
당시 공개 파일 주소는 다음처럼 확장자가 없는 형태였습니다.
/f/550e8400-e29b-41d4-a716-446655440000응답의 Content-Type은 application/pdf였지만 반복해서 요청해도 CF-Cache-Status: DYNAMIC이 유지되었습니다. 그래서 같은 도메인의 여러 URL을 비교했습니다.
| 요청 형태 | 관찰한 상태 | 해석 |
|---|---|---|
/f/{식별자} |
DYNAMIC |
기본 캐시 적격성에서 제외되었습니다 |
.pdf로 끝나는 URL |
MISS, 이후 HIT 또는 REVALIDATED |
캐시 대상이 되었고 엣지 저장 또는 재검증이 이루어졌습니다 |
.ico로 끝나는 URL |
HIT |
엣지 캐시에서 바로 응답했습니다 |
이 비교로 MIME 타입보다 URL의 확장자가 결정적인 차이라는 가설을 세울 수 있었습니다.
Cloudflare의 기본 캐시 동작은 이 가설을 뒷받침합니다. 기본 설정에서는 MIME 타입이 아니라 파일 확장자를 기준으로 캐시하며, PDF·이미지·폰트·압축 파일 등 정해진 확장자를 기본 캐시 대상으로 취급합니다. 확장자가 없는 동적 경로를 캐시하려면 Cache Rule로 적격성을 명시하거나, URL 자체에 적절한 확장자를 포함해야 합니다. Cloudflare 기본 캐시 동작
공개 URL에 확장자를 포함했습니다
파일 주소를 다음 형태로 변경했습니다.
기존: /f/{slug}
변경: /f/{slug}.pdf라우트에서는 마지막 확장자를 제거한 뒤 기존 slug로 데이터를 조회하도록 했습니다. 따라서 저장 파일명과 데이터베이스 식별자는 그대로 유지하면서, 공개 URL만 브라우저와 CDN이 이해하기 쉬운 형태로 만들었습니다.
요청 URL: /f/550e8400-e29b-41d4-a716-446655440000.pdf
조회 키: 550e8400-e29b-41d4-a716-446655440000
저장 이름: 550e8400-e29b-41d4-a716-446655440000확장자를 경로 조회에 그대로 사용하지 않았기 때문에 저장 경로의 범위가 넓어지지도 않았습니다. URL은 의미가 분명해졌지만, 내부 식별과 파일 접근 규칙은 유지할 수 있었습니다.
변경 후 첫 요청에서는 MISS, 이어지는 요청에서는 HIT이 확인되었습니다. MISS는 아직 해당 엣지에 파일이 없어 오리진에서 가져왔다는 뜻이고, HIT은 저장된 응답을 엣지에서 제공했다는 뜻입니다. REVALIDATED는 조건부 요청을 통해 오리진이 파일 변경이 없음을 확인한 뒤 캐시된 응답을 사용했다는 의미입니다. Cloudflare 캐시 응답 상태
URL을 만드는 모든 지점을 함께 고쳐야 합니다
서버가 새 URL을 반환하도록 바꾼 뒤에도 한 소비 경로에서 문제가 남았습니다. 중계 서비스가 업로드 응답의 URL을 그대로 보존하지 않고, 식별자만 사용해 예전 주소를 다시 조립하고 있었기 때문입니다.
파일 서비스 응답: /f/{slug}.pdf
중계 서비스 재조립: /f/{slug}파일 서비스만 보면 수정이 끝난 것처럼 보였지만, 최종 사용자에게 전달된 URL에서는 확장자가 다시 사라졌습니다. 중계 서비스가 파일 서비스에서 받은 URL을 그대로 보존하도록 변경한 뒤에야 전체 경로에서 캐시가 안정적으로 동작했습니다.
이 경험은 URL을 API 계약으로 다뤄야 하는 이유를 보여줍니다. 생산자가 URL 규칙을 알고 있는데 소비자가 같은 규칙을 다시 구현하면, 한쪽의 변경이 다른 쪽에 조용히 누락될 수 있습니다. 가능한 경우 소비자는 완성된 URL을 전달받아 보존하는 편이 안전합니다.
체감 속도는 생각보다 조금만 줄었습니다
테스트 환경에서 같은 파일의 응답 시간은 약 0.77초에서 0.60초로 줄었습니다. 캐시가 HIT으로 바뀐 것에 비하면 차이가 작아 보일 수 있습니다.
이는 이상한 결과가 아닙니다. 가까운 네트워크에서 측정하면 Wi-Fi 왕복, TLS 연결, 브라우저 처리 같은 비용의 비중이 커집니다. 파일 크기가 작고 오리진도 가까우면 원본 파일을 읽는 시간이 전체 지연에서 차지하는 비율이 크지 않을 수 있습니다.
엣지 캐시의 더 중요한 이점은 다음과 같습니다.
- 같은 파일을 반복해서 요청할 때 오리진까지 가지 않습니다.
- 이용자와 오리진의 물리적 거리가 멀수록 전송 경로가 짧아질 수 있습니다.
- 인기 파일에 요청이 몰려도 오리진의 네트워크와 애플리케이션 부하를 줄일 수 있습니다.
- 일시적인 트래픽 증가에 대응할 여유가 커집니다.
따라서 CDN 적용 결과를 판단할 때는 개발 장비 한 대의 응답 시간만 보지 않고, CF-Cache-Status, Age, 오리진 요청 수, 지역별 응답 시간도 함께 확인하는 것이 좋습니다.
콘텐츠 주소와 캐시 정책을 함께 설계해야 합니다
긴 캐시를 안전하게 사용하려면 같은 URL의 내용이 바뀌지 않아야 합니다. 파일 내용이 바뀔 때 새 식별자와 새 URL을 발급하는 방식이라면 다음 정책을 적용하기 좋습니다.
Cache-Control: public, max-age=31536000, immutable반대로 같은 URL의 파일을 덮어쓸 수 있다면 긴 브라우저 캐시와 immutable은 이전 내용을 오래 남길 수 있습니다. 특히 공개 파일을 비공개로 전환할 수 있는 서비스라면 브라우저 TTL, 엣지 TTL, 캐시 삭제 전략을 따로 설계해야 합니다. 공개 상태만 데이터베이스에서 바꾸고 이미 저장된 엣지 응답을 삭제하지 않으면, 비공개 전환이 즉시 반영되지 않을 수 있습니다.
또한 하나의 URL이 Accept 헤더에 따라 AVIF, WebP 등 서로 다른 응답을 반환한다면 캐시 변형 규칙도 확인해야 합니다. Vary: Accept 응답 헤더를 추가하는 것만으로 모든 Cloudflare 구성이 자동 분리된다고 가정하기보다, 현재 Cache Rule과 캐시 키 설정이 해당 변형을 지원하는지 확인해야 합니다. Cloudflare의 Vary 처리
정리
이번 문제에서 가장 중요한 교훈은 Cache-Control과 캐시 적격성을 분리해서 보는 것이었습니다.
DYNAMIC인지BYPASS인지 먼저 구분합니다.- 헤더만 보지 말고 URL 형태와 Cache Rule을 함께 확인합니다.
- Cloudflare 기본 정책을 사용한다면 파일 URL에 올바른 확장자를 포함합니다.
- URL을 재조립하는 소비자가 없는지 전체 호출 경로를 추적합니다.
MISS → HIT을 반복 요청으로 검증합니다.- 긴 캐시를 사용하기 전에 URL 불변성과 공개 상태 변경 정책을 확인합니다.
immutable은 강력한 도구이지만, 캐시 대상이 된 뒤에야 의미가 있습니다. CDN 문제를 해결할 때는 헤더 하나만 고치기보다 요청 적격성, URL 설계, 소비 경로, 무효화 정책을 하나의 흐름으로 살펴보는 것이 좋습니다.
함께 읽기
- 한국에서 접속하는데 Cloudflare가 로스앤젤레스에서 응답했습니다이미지 엣지 캐시를 손보고 나서 얼마나 빨라졌는지 재고 있었습니다. 캐시에서 나오면 156ms, 오리진까지 가면 622ms. 4배 차이니까 좋아하고 있었는데, 156ms라는 숫자가 자꾸 걸렸습니다.
- 엣지 캐시를 마저 켜자 비공개 이미지 문제가 드러났습니다전에 CDN의 파일 경로에 확장자가 없어서 Cloudflare가 캐시하지 않던 문제를 고쳤습니다. 헤더를 아무리 강하게 줘도 CF-Cache-Status가 DYNAMIC으로 나오던 그 이야기입니다.
- Next.js + Postgres 프로젝트 호스팅 비용 비교: 관리형 플랫폼과 VPSNext.js와 PostgreSQL로 만든 웹 애플리케이션을 운영할 때 관리형 플랫폼과 VPS 중 어느 쪽이 더 저렴한지는 월 기본요금만으로 판단하기 어렵습니다. 관리형 플랫폼은 배포와 확장을 대신 처리하고, VPS는 낮은 인프라 비용 대신 서버 운영을 직접 맡아야 합니다.
- Cloudflare를 붙였는데 한국에서 오히려 느려진 이유한국 사용자를 대상으로 운영 중인 데모 사이트에서 응답 지연을 점검했습니다. 도메인은 Cloudflare 프록시를 거치게 해뒀고, 원본 IP와 오리진을 보호하기 위해 켜둔 상태였습니다. 그러다 사이트를 손보는 중에 "버튼이 한 박자 늦게 눌리는 것 같다"는 얘기가 나왔고, 확인해 보니 앱 코드가 아니라 이 프록시 경로가…
- 새 서브도메인이 특정 브라우저에서만 안 열릴 때: DNS 캐시 점검법새 서브도메인을 추가한 직후 “Safari에서는 열리는데 Chrome에서는 안 열린다” 같은 일이 생길 수 있습니다. 이럴 때 바로 서버 장애나 DNS 설정 오류라고 판단하기 쉽지만, 실제 원인은 로컬 또는 브라우저 DNS 캐시인 경우가 많습니다.