캐시에서 HIT이 나오면 좋은 일 같죠? 사실은 잘못된 사본을 빠르게 나눠주는 중일 수도 있어요.
CDN, Cache, 그리고 Edge Delivery에서는 캐시가 사용자 가까이에 사본을 두고 빠르게 응답하는 큰 그림을 봤어요. 그리고 Cache Key와 Vary, Cookie와 캐시 가능성에서는 어떤 요청에 어떤 사본을 써도 되는지를 더 조심스럽게 봤죠.
이번 글은 그 지식이 실제 장애 장면에서 어떻게 보이는지 읽어볼게요.
장면은 이래요.
- 어떤 사용자는 한국어 페이지를 기대했는데 영어 페이지를 봐요.
- 로그인하지 않은 사용자가 로그인 사용자용 조각을 봤다고 제보해요.
- API 본문은 맞는 것 같은데 CORS 헤더가 다른 사이트용으로 보여요.
- 오리진에서는 이미 고쳤는데 CDN에서는 계속 예전 HTML이 나와요.
- 응답 헤더에는
HIT과 꽤 큰Age가 보여요.
HIT이고, 오리진 부하도 줄었으니까요.
근데요, 캐시에서 중요한 건 빠른 것보다 먼저 맞는 응답 사본을 맞는 요청에 주는 것이에요. 이 응답 사본에는 본문과 응답 헤더가 함께 들어 있어요. 캐시 오염은 캐시가 이번 요청에 맞지 않는 저장된 응답을 선택해 재사용할 때 의심하는 증상이에요.
여기서는 특정 공격 기법을 따라 하는 방법이 아니라, 운영자가 잘못된 캐시 사본이 사용자에게 나가는 증상을 어떻게 읽고 줄일지에 집중해요. 실제 보안 취약점이 의심되면 CDN 설정, 애플리케이션 응답 생성 로직, 보안 로그를 함께 보고 필요한 경우 보안 담당자와 같이 다뤄야 해요.
먼저 같은 도시락이 맞는지 확인해요
탕비실에 도시락이 쌓여 있다고 해볼게요. 겉포장에는 모두 “샌드위치”라고 적혀 있어요. 그런데 안쪽은 다를 수 있죠.- 누구나 먹어도 되는 공용 샌드위치
- 알레르기 때문에 특정 사람에게만 맞는 샌드위치
- 한국어 이름표가 붙은 도시락
- 영어 이름표가 붙은 도시락
- 어제 만든 샌드위치인데 오늘도 남아 있는 것
HIT이 나왔더라도, 그 사본이 이번 요청의 조건에 맞는지는 따로 봐야 해요.
여기서 오늘 볼 핵심 질문은 이거예요.
“캐시가 찾은 사본은 정말 이 요청에 맞는 사본일까요?”이 그림에서 캐시 오염은 “캐시가 있다”가 문제가 아니에요. 캐시가 같은 것으로 묶으면 안 되는 요청을 같은 칸에 묶었거나, 이미 잘못 저장된 사본을 계속 꺼내는 장면이 문제예요.
먼저 읽을 신호 여섯 가지
캐시 오염이 의심될 때는 재배포나 purge부터 누르기 전에, 증거를 짧게 잡아야 해요. 특히 잘못된 사본은 purge하면 사라질 수 있어서, 무엇이 잘못 묶였는지 놓치기 쉬워요.
이 여섯 가지를 한 화면에 모으면, “캐시가 이상해요”라는 말이 조금 더 구체적인 질문으로 바뀌어요.
- 한국어 요청인데 왜
Content-Language: en일까요? Cookie: session=user-b가 있는 요청에public사본을 줘도 될까요?Set-Cookie가 있는 응답이 공유 캐시에 저장돼도 되는 장면일까요?
캐시 오염은 보통 네 갈래로 보여요
운영에서 보이는 증상은 다양하지만, 처음에는 네 갈래로 나눠 읽으면 좋아요.
여기서 중요한 반전이 있어요.
캐시 오염의 핵심은 본문만 틀리는 게 아니라, 이번 요청에 맞지 않는 응답 사본(본문과 응답 헤더)을 캐시가 선택했다는 데 있어요.
본문은 같은 JSON이어도
Access-Control-Allow-Origin 같은 응답 헤더가 요청의 Origin에 따라 달라진다면, 헤더만 섞여도 문제가 돼요. 반대로 HTML 본문은 맞아 보이는데 안쪽에 참조하는 JavaScript 파일 경로가 예전 것이라 장애가 나는 경우도 있어요.
이 흐름은 “캐시가 원인이다”라고 확정하는 표가 아니에요. 캐시를 의심할 때 어떤 종류의 틀림인지를 먼저 나누는 표예요.
예시 1. 언어가 섞이는 장면
가장 이해하기 쉬운 장면부터 볼게요.Vary를 봐야 해요.
문제가 되는 흔한 모양은 이래요.
Vary: Accept-Language가 없거나, CDN의 커스텀 cache key에 언어 조건이 반영되지 않으면 같은 URL의 사본이 섞일 수 있어요.
이때 해결 방향은 단순히 “캐시 끄기”만은 아니에요. 공용 페이지를 캐시하고 싶다면 언어가 실제로 응답을 바꾸는 신호인지, 그렇다면 Vary나 CDN key에 어떻게 좁게 반영할지 봐야 해요.
예시 2. 사용자별 응답이 공유 캐시에 섞이는 장면
더 조심해야 하는 건 사용자별 응답이에요.
민감한 사용자별 페이지라면 보통 공유 캐시에 두지 않는 쪽으로 설계해야 해요. HTTP 캐시 관점에서는 RFC 9111: HTTP Caching의
private, no-store, 인증된 응답 재사용 조건 같은 기본 규칙을 바닥에 두고, CDN 제품의 우회 정책을 함께 봐야 해요.
예시 3. CORS 헤더만 섞이는 장면
이번에는 본문이 공용 JSON이라고 해볼게요.Origin에 따라 달라진다면, 캐시는 그 차이를 알아야 해요. 이런 경우 Vary: Origin이 필요할 수 있어요.
여기서 헷갈리기 쉬운 점은, 캐시가 비교해야 하는 것이 본문만은 아니라는 거예요. 본문이 맞아도 저장된 응답의 CORS 헤더가 이번 요청과 다르면 그 응답 사본을 안전하게 재사용할 수 없어요.
예시 4. 오래된 HTML이 계속 남는 장면
배포 직후에도 캐시 오염처럼 보이는 일이 있어요.
여기서도
HIT만 보고 끝내면 안 돼요. HIT은 캐시가 사본을 찾았다는 뜻에 가깝고, 그 사본이 지금 배포 의도와 맞는지는 Age, freshness, purge 조건까지 같이 봐야 해요.
복구할 때는 영향 차단과 원인 보존을 나눠요
캐시 오염 의심 장애는 빨리 멈춰야 해요. 잘못된 사용자별 응답이나 잘못된 CORS 헤더가 나간다면 영향이 계속 퍼질 수 있으니까요. 다만 복구는 두 줄로 나눠 생각하면 좋아요. 사용자 정보가 섞일 가능성이 있으면, 원인 분석보다 영향 차단이 먼저예요. 대신 그 순간에도 최소한의 헤더 증거는 남겨두는 게 좋아요. 복구 후에는 같은 조건으로 다시 확인해야 해요.Origin, 쿠키, query, 호스트 이름까지 같은 조건으로 재현해야 해요.
잘못 읽기 쉬운 함정 여섯 가지
하나,HIT이면 좋은 상태라고만 보기.HIT은 빠른 신호일 수 있지만, 잘못된 사본을 빠르게 주는 신호일 수도 있어요. 사본이 맞는지까지 봐야 해요.
둘, URL만 같으면 같은 응답이라고 보기.언어, 압축,
Origin, 쿠키, 인증 상태가 응답을 바꾼다면 URL만으로 부족할 수 있어요.
셋, Vary: Cookie를 안전한 해결책으로 과신하기.사용자별 민감 응답은 공유 캐시에 저장하지 않는 쪽이 먼저예요.
Vary: Cookie는 히트율도 크게 떨어뜨릴 수 있어요.
넷, purge가 원인 수정이라고 생각하기.purge는 잘못 저장된 사본을 지우는 조치예요. 같은 조건으로 다시 잘못 저장될 수 있다면 cache key,
Vary, Cache-Control, CDN 규칙을 고쳐야 해요.
다섯, 브라우저 캐시와 CDN 캐시를 섞어 읽기.Network 탭의
(memory cache)와 CDN의 HIT은 위치가 달라요. 어느 캐시가 응답했는지 먼저 나눠야 해요.
여섯, 보안 이슈인데 성능 이슈처럼만 다루기.다른 사용자 정보, 권한별 응답, CORS 허용 헤더가 섞였다면 단순 캐시 튜닝이 아니에요. 영향 범위와 노출 가능성을 별도로 확인해야 해요.
재발 방지는 캐시 키를 좁고 명확하게 만드는 일이에요
캐시 오염을 막으려면 “캐시를 다 끄자”로 끝내기보다, 어떤 응답을 어떤 조건으로 재사용할지 명확히 해야 해요.
좋은 캐시 설정은 무조건 오래 저장하는 설정이 아니에요. 공유해도 되는 사본은 적극적으로 재사용하고, 사용자별이거나 조건이 다른 사본은 섞이지 않게 나누는 설정이에요.
자, 정리해볼까요?
- 캐시 오염은 캐시가 있다는 문제가 아니라, 이번 요청에 맞지 않는 응답 사본(본문과 응답 헤더)을 맞는 것처럼 선택해 재사용하는 장면이에요.
-
HIT은 캐시가 사본을 찾았다는 신호이지, 그 사본이 이번 요청에 맞다는 보증은 아니에요. -
처음에는 URL,
Cache-Control,Age, CDN 상태,Vary, 실제 요청 헤더, 쿠키와 인증 신호를 같이 봐야 해요. - 언어, 사용자별 응답, CORS 헤더, 오래된 HTML은 서로 다른 갈래라서 같은 방식으로 고치면 안 돼요.
-
purge는 영향 차단에는 도움이 되지만, cache key,
Vary, 저장 정책을 고치지 않으면 같은 문제가 다시 생길 수 있어요. - 재발 방지는 공유 가능한 사본과 사용자별 사본을 명확히 나누는 일에서 시작해요.
이어서 보면 좋은 글
- Cache Key와 Vary는 왜 같이 읽어야 할까요? — 같은 URL의 사본이 왜 나뉘어야 하는지 원리부터 다시 보고 싶을 때 좋아요.
- CDN Cache Status 헤더는 어떻게 읽어야 할까요? —
HIT,MISS,BYPASS,STALE같은 상태값을 주변 헤더와 함께 읽고 싶을 때 이어서 보면 좋아요. - Cookie와 캐시 가능성은 왜 같이 봐야 할까요? — 사용자별 응답이 공유 캐시에 섞이지 않게 판단하는 기준을 더 자세히 볼 수 있어요.
- stale-while-revalidate와 soft purge는 왜 같이 볼까요? — 오래된 사본이 바로 사라지지 않는 운영 장면을 이어서 볼 수 있어요.