URL이 같으면 캐시도 무조건 같은 사본을 꺼낼 것 같죠? 사실은 요청 헤더 몇 개가 달라졌다는 이유만으로 다른 사본을 꺼내야 할 때가 있어요.CDN, Cache, 그리고 Edge Delivery에서는 캐시가 무엇을 같은 응답으로 볼지 정하는 기준을 캐시 키라고 가볍게 봤어요. 그리고 Cache-Control과 Age 헤더에서는 저장된 사본이 아직 fresh인지, 이미 얼마나 나이를 먹었는지 읽었죠. 이번에는 그보다 한 칸 앞의 질문을 볼게요.
“캐시가 지금 꺼내려는 사본은 정말 이 요청에 맞는 사본일까요?”실제 운영에서는 이런 장면을 자주 만나요.
/docs라는 같은 URL이에요. 그런데 영어를 원하는 사용자, 한국어를 원하는 사용자, br 압축을 받을 수 있는 브라우저, gzip만 받을 수 있는 클라이언트가 모두 같은 사본을 받아도 될까요?
오늘은 Cache Key가 무엇을 같은 요청으로 묶는지, Vary가 어떤 요청 헤더를 추가로 비교하라고 말하는지, 그리고 캐시가 잘 안 맞거나 너무 많이 쪼개질 때 어디를 봐야 하는지 같이 읽어볼게요. HTTP 캐시의 기본 캐시 키와 Vary 계산 규칙은 RFC 9111: HTTP Caching과 RFC 9111의 Vary 절을 바닥에 두고, Vary 헤더 자체의 의미는 RFC 9110의 Vary 절을 기준으로 잡을게요.
여기서는 특정 CDN의 설정 화면보다 표준 HTTP 캐시가 같은 응답을 재사용해도 되는지 판단하는 감각에 집중해요. CDN마다 쿼리 문자열 정규화, 커스텀 캐시 키, 쿠키 포함 규칙은 다를 수 있으니, 제품별 설정은 별도 문서와 함께 확인해야 해요.
같은 상품도 옵션이 다르면 다른 칸에 둬야 해요
카페 픽업대를 떠올려볼게요. 겉으로는 모두 같은 “라떼” 주문처럼 보여요. 그런데 실제로는 옵션이 달라질 수 있죠.- 아이스 라떼인지, 따뜻한 라떼인지
- 일반 우유인지, 오트 우유인지
- 작은 컵인지, 큰 컵인지
- 한국어 이름표인지, 영어 이름표인지
핵심은 캐시 키가 사본을 꺼내는 이름표라는 점이에요. 그리고
Vary는 그 이름표에 “이 요청 헤더들도 같이 맞는지 보세요”라고 덧붙이는 신호예요.
이 그림에서 URL은 출발점이에요. 하지만 Vary가 있으면 캐시는 URL만 보고 끝내지 않고, 응답을 만들 때 영향을 준 요청 헤더까지 다시 비교해야 해요.
Cache Key는 캐시가 사본을 찾는 주소예요
아주 단순하게 말하면 cache key는 이 질문의 답이에요.“어떤 요청들이 같은 저장 칸을 공유해도 될까요?”HTTP 표준에서는 캐시 키가 최소한 요청 method와 target URI를 바탕으로 만들어진다고 설명해요. 현실의 일반적인 웹 캐시는 대부분
GET 응답을 주로 저장하기 때문에, 운영자가 처음 볼 때는 URL이 가장 큰 축으로 보일 때가 많아요.
여기서 “보통”이라고 말한 이유가 있어요. 표준은 큰 원칙을 주지만, CDN과 프록시는 운영자가 설정한 cache key 정책을 추가로 가질 수 있거든요.
예를 들어 어떤 CDN은 이런 선택지를 제공할 수 있어요.
- 쿼리 문자열 전체를 cache key에 포함
- 특정 쿼리만 포함
- 쿼리 순서를 정규화
- 특정 쿠키나 헤더를 cache key에 포함
- 모바일/데스크톱 같은 장치 구분을 별도 키에 포함
Vary는 “이 요청 헤더까지 맞아야 해요”라는 표시예요
이제Vary를 볼게요.
“이 응답은 요청의 Accept-Encoding 값에 따라 달라질 수 있으니, 나중에 재사용할 때도 그 요청 헤더가 맞는지 비교하세요.”
왜 필요할까요?
브라우저 A는 Brotli 압축을 받을 수 있어요.
br로 압축한 응답을 보냈다면, 그 사본을 B에게 그대로 주면 안 돼요. B가 Brotli를 이해하지 못할 수 있으니까요.
이 흐름에서 Vary: Accept-Encoding이 없으면 캐시는 같은 URL이라는 이유로 br 사본을 엉뚱한 클라이언트에게 줄 위험이 생겨요.
Vary는 캐시를 더 안전하게 만들 수 있어요. 하지만 동시에 캐시 키를 더 잘게 쪼개기 때문에 히트율을 떨어뜨릴 수도 있어요. 그래서 Vary는 “많이 넣을수록 좋은 헤더”가 아니라, 응답 내용이나 응답 헤더가 실제로 달라지는 요청 헤더만 정확히 넣어야 하는 헤더예요.
Vary가 빠지면 잘못된 사본이 섞일 수 있어요
가장 위험한 장면은 오리진이 요청 헤더에 따라 다른 응답을 만들면서도Vary를 빠뜨리는 경우예요.
예를 들어 /home이 언어에 따라 달라진다고 해볼게요.
Vary: Accept-Language가 없어요. 그러면 캐시는 /home의 public 응답 하나를 저장했다가, 영어를 원하는 사용자에게도 한국어 사본을 줄 수 있어요.
이런 문제는 눈에 띄면 금방 찾을 것 같지만, 실제로는 지역, 언어, 실험 그룹, 로그인 상태가 얽히면 꽤 교묘하게 보여요.
Vary가 너무 넓으면 캐시가 거의 안 맞을 수 있어요
반대 문제도 있어요. 이번에는 응답에 이런 헤더가 붙었다고 해볼게요.User-Agent 값은 생각보다 길고 다양해요.
Cookie는 더 조심해야 해요.
Vary: Cookie가 붙으면, 캐시를 거의 못 쓰는 상태가 될 수 있어요.
Vary: *는 나중 요청에 그냥 맞춰볼 수 없다는 신호예요
가끔 이런 값을 볼 수도 있어요.
Vary: *가 있으면 나중 요청에 이 응답이 적절한지 origin server에 보내지 않고는 판단할 수 없다고 설명해요.
운영 감각으로는 이렇게 보면 돼요.
그래서
Vary: *는 일반적인 CDN 히트율을 높이는 도구라기보다, 저장된 응답을 나중에 그냥 재사용하기 어렵게 만드는 신호로 읽는 편이 좋아요.
실제 디버깅에서는 다섯 가지를 같이 봐요
캐시 키와Vary가 의심될 때는 응답 헤더 하나만 보지 말고, 요청과 응답을 한 화면에서 같이 봐야 해요.
여기서 중요한 건
Vary가 응답 헤더라는 점이에요. 하지만 비교 대상은 요청 헤더예요. 그래서 디버깅할 때 응답의 Vary만 캡처하고 끝내면 반쪽이에요. 반드시 그 요청이 보낸 Accept-Encoding, Accept-Language, Origin, Cookie 같은 값을 같이 봐야 해요.
어떤 CDN은 표준
Vary 외에도 설정 화면에서 host, query, header, cookie를 cache key에 넣거나 뺄 수 있게 해요. 응답에 Vary가 안 보여도 CDN 설정이 별도 key 분리를 하고 있을 수 있고, 반대로 Vary가 있어도 제품 정책에 따라 저장 방식이 다르게 보일 수 있어요.잘못 읽기 쉬운 함정
1. “URL이 같으니 무조건 같은 캐시다”
URL은 가장 중요한 축이지만 전부는 아니에요.Vary: Accept-Encoding이 있으면 같은 URL에도 압축 방식별 사본이 있을 수 있고, Vary: Accept-Language가 있으면 언어별 사본이 있을 수 있어요.
2. “Vary가 많을수록 더 안전하다”
무조건 그렇지는 않아요. 필요한 차이를 반영하지 않으면 사본이 섞이고, 너무 많은 차이를 반영하면 히트율이 떨어져요. 안전과 재사용 사이에서 정말 응답을 바꾸는 신호만 골라야 해요.3. “Vary: Cookie면 로그인 응답도 공유 캐시에 안전하다”
위험한 단정이에요. 사용자별 민감 응답은 보통private, no-store, 인증 헤더, CDN 우회 정책까지 함께 봐야 해요. Vary: Cookie는 요청 쿠키가 다르면 사본을 나누라는 신호이지, 민감 응답을 공유 캐시에 저장해도 된다는 허가증이 아니에요.
4. “Age가 있으면 cache key가 맞았다는 뜻이다”
Age는 캐시를 거쳐 온 힌트예요. 하지만 어떤 key로 저장됐고, Vary 비교가 제대로 됐는지는 별도의 문제예요. Age, CDN 상태 헤더, Vary, 실제 요청 헤더를 같이 봐야 해요.
예시로 같이 읽어볼게요
1. 압축 방식 때문에 사본이 나뉘는 정적 파일
Content-Encoding: br이므로, 나중 요청이 Brotli를 받을 수 있는지 확인해야 해요. 여기서 Vary: Accept-Encoding은 자연스러운 신호예요.
2. 언어별 페이지인데 Vary가 있는 경우
/guide라도 언어별 사본이 나뉠 수 있어요. 이 경우에는 Content-Language와 Vary: Accept-Language가 서로 말이 맞는지 봐요.
3. 쿠키 때문에 히트율이 떨어지는 경우
Vary: Cookie 때문에 사용자의 쿠키 조합마다 사본이 갈라져 히트율이 낮아질 수 있어요. 이때는 쿠키 전체가 아니라 실험 그룹처럼 실제로 응답을 바꾸는 작은 신호만 key에 넣을 수 있는지 검토해야 해요.
4. CORS 응답에서 Origin을 빼먹은 경우
Access-Control-Allow-Origin 값이 달라진다면 Vary: Origin이 필요할 수 있어요. 없으면 다른 origin 요청에 앞선 origin의 CORS 헤더가 섞여 보일 수 있거든요.
자, 정리해볼까요?
Cache Key는 캐시가 저장된 응답을 찾을 때 쓰는 이름표예요.HTTP 캐시의 기본 key는 최소한 요청 method와 target URI에서 출발하지만, 실제 CDN은 query, header, cookie 같은 추가 정책을 가질 수 있어요.Vary는 응답을 나중에 재사용할 때 어떤 요청 헤더까지 맞는지 비교하라고 알려주는 응답 헤더예요.
Vary가 빠지면 언어, 압축, CORS, 실험 응답이 섞일 수 있어요.Vary가 너무 넓으면 사본이 과하게 쪼개져 캐시 히트율이 떨어질 수 있어요.캐시 문제를 볼 때는 URL, method, Vary, 실제 요청 헤더, Age, CDN 상태 헤더를 같이 봐야 해요.Cache-Control은 얼마나 오래 쓸 수 있는지를 말하고, cache key와 Vary는 어떤 요청에 그 사본을 써도 되는지를 말해요. 이 둘을 같이 봐야 “캐시가 됐다”와 “맞는 사본이 나왔다”를 구분할 수 있어요.
이어서 보면 좋은 글
- CDN, Cache, 그리고 Edge Delivery — 캐시와 엣지 전달의 큰 그림으로 돌아가고 싶을 때 좋아요.
- Cache-Control과 Age 헤더는 어떻게 같이 읽어야 할까요? — 지금 사본이 fresh인지, 이미 얼마나 오래됐는지 같이 읽어봐요.
- ETag와 조건부 요청은 어떻게 304를 만들까요? — 캐시된 사본을 전체 다운로드 없이 다시 확인하는 흐름을 이어서 볼 수 있어요.
- 브라우저 waterfall은 어디부터 읽어야 할까요? — 캐시된 요청과 네트워크 요청이 브라우저 도구에서 어떻게 다르게 보이는지 이어서 볼 수 있어요.