같은 URL이 사용자마다 다른 언어로 캐시돼 섞인 이유
문제 발생
Accept-Language에 따라 한국어/영어 응답을 내려주는 API를 만들었습니다. CDN을 붙이자 영어 사용자에게 한국어 응답이, 그 반대도 나타났습니다. 서버 코드에는 문제가 없었습니다.
원인 분석
캐시는 URL을 키로 씁니다. 같은 URL에 대해 서버가 요청 헤더를 보고 다른 표현을 돌려주고 있는데, 캐시가 그 사실을 모르면 먼저 저장된 응답을 모두에게 줍니다.
MDN은 콘텐츠 협상을 같은 URI에 대해 여러 표현을 제공하고, 사용자 에이전트가 어떤 표현이 가장 적합한지 지정하게 하는 메커니즘으로 설명합니다. 클라이언트는 Accept(MIME 타입), Accept-Language(언어), Accept-Encoding(압축)으로 선호를 표현하고, 각 항목에 **q-value(품질 계수)**로 우선순위를 붙입니다(기본 q=1.0).
그리고 캐시를 위한 헤더가 따로 있습니다 — Vary 응답 헤더는 서버가 콘텐츠 협상에 어떤 요청 헤더를 사용했는지 캐시에 알려줍니다. 이것이 결정적인데, 없으면 캐시는 어떤 기준이 응답을 결정했는지 알 수 없어 잘못 캐시된 콘텐츠를 서빙할 위험이 있습니다.
맞는 표현이 없을 때의 응답도 정해져 있습니다 — 클라이언트의 Accept* 헤더에 맞는 표현을 제공할 수 없으면 406 Not Acceptable로 응답합니다.
해결 방안
- 협상에 쓴 헤더를
Vary에 적습니다.
Content-Language: ko
Vary: Accept-Language-
협상 축을 최소로 유지합니다.
Vary에 헤더가 늘수록 캐시 적중률이 급격히 떨어집니다 —Vary: User-Agent처럼 값이 사실상 무한한 헤더는 캐시를 무력화합니다. -
언어는 URL로 분리하는 방법도 검토합니다.
/ko/posts,/en/posts처럼 경로에 넣으면 캐시·공유·SEO가 모두 단순해집니다 — 헤더 협상은 링크로 공유했을 때 다른 것이 보인다는 문제가 있습니다. -
형식 협상은 명시적으로 처리합니다.
Accept: application/jsonvsapplication/problem+json처럼 우리가 실제로 지원하는 타입만 다루고, 그 외에는 406으로 정직하게 거절합니다. -
압축은 대개 프록시가 담당합니다. 앱이
Accept-Encoding을 직접 다루기 시작하면 nginx·CDN과 이중으로 처리되어 이상한 조합이 생깁니다. -
캐시 계층에서 실제로 확인합니다. 두 언어로 연속 요청해 서로 다른 응답이 오는지, CDN 로그에서 캐시 키가 나뉘는지 봅니다 — 코드만 봐서는 드러나지 않습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.