Cache-Control 헤더를 알아보자 (no-store, no-cache, max-age, must-revalidate)
Cache-Control
HTTP 완벽 가이드를 캐시 챕터를 읽으면서 header에 대해 처음으로 고민하면서 읽게 되었는데, 스터디를 하면서 재미있는 질문과 논의가 나온 거 같아 정리해보면 좋을 거 같아 글을 작성하게 되었습니다. E-tag
등도 캐시에서는 빼놓을 수 없는 헤더지만, 이번 글에서는 Cache-Control
필드에 해당하는 네 가지 디렉티브를 중심으로 정리해보려고 합니다.
no-store
no-cache
max-age
must-revalidate
no-store
no-store
디렉티브는 캐시에 응답을 저장하는 것을 금지합니다. 말 그대로 아예 저장(store)을 하지 않는 걸 의미합니다.
캐시를 하지 않는다, 라고 하면 당연히 이런 옵션만 있을 거 같은데 no-cache
라는 디렉티브가 따로 있습니다.
no-cache
no-cache
디렉티브는 캐시의 데이터를 사용하기 전 항상 원 서버에 요청을 보내 revalidate를 하게 합니다. 한마디로, 캐시의 데이터가 신선하든 아니든 일단 무조건 가장 최신의 데이터를 받아오게 됩니다. no-store
와 다르게 no-cache
는 캐시에 저장은 하되, 클라이언트가 매 요청마다 revalidate를 시도합니다.
이 부분에서 처음에 의문이었던 부분은, 늘 revalidate를 요청하는데 왜 캐시에 저장을 하는가였습니다. 이는 revalidation이 항상 데이터의 갱신을 보장하는 개념이 아니기 때문입니다. 예를 들어, no-cache
헤더 때문에 revalidation을 요청했는데, 서버가 E-tag
를 기준으로 판별하여 데이터가 아직 fresh 하다는 304
응답을 돌려주는 경우가 있습니다. 이 때는 no-cache
지만 브라우저는 캐시에 저장된 데이터를 사용자에게 보여주게 됩니다.
요청 헤더 vs 응답 헤더
흥미롭게도 데이터에 대한 정책은 데이터를 전달하는 서버(비즈니스 로직을 담당하는)에서 지정해줄 거 같은데, no-store
, no-cache
모두 응답만이 아니라 요청 헤더에도 포함할 수 있습니다. 응답 헤더의 no-cache
는 클라이언트에게 캐시에 저장은 하되, 매번 revalidate 요청을 해달라는 의미입니다. 그럼 클라이언트는 왜 데이터를 제공하는 주체가 아님에도 no-cache
를 추가할까요? 이는 클라이언트의 요청이 프록시를 거칠 때 프락시 또한 반드시 revalidate 하도록 유도하기 위함입니다.
max-age
Cache-Control: max-age=604800
=
뒤에 적히는 숫자는 초 단위며, 저 시간 동안은 캐시의 데이터가 신선하다고 판단합니다. 저 시간보다 오래된 데이터는 stale 하다고 판단하고 revalidate 합니다.
요청 헤더 vs 응답 헤더
max-age
또한 요청, 응답 헤더 모두에 포함할 수 있습니다.
- 요청: 캐시나 서버에 질의할 때 지정한
max-age
보다 저장된 기간이 짧은 데이터만을 재사용하겠다는 의미. - 응답: 서버가 지정하는 데이터의 캐시 수명. 캐시에 저장된 시간이 max-age 시간보다 길어진다면 revalidate 하게끔 안내.
두 개가 비슷하게 들릴 수 있는데, 결국 신선도의 기준을 누가 정하는지의 문제로 볼 수 있습니다.
(여기서 의문이 생기는 한 가지는, 예를 들어 request는 max-age를 5초, response는 1분으로 지정하고 30초 전에 캐시에 저장된 내용이 있다고 가정해보자. 이 request에 의하면 캐시의 데이터는 5초를 경과하여 신선하지 않을 테니 결국 revalidate를 하게 되는 걸까? 이런 경우 서버가 주는 response max-age는 무의미해 보이는데 설계 오류일까 아니면 전혀 상관이 없는 부분일까? 🤔)
must-revalidate
must-revalidate
디렉티브는 max-age
와 보통 함께 쓰이며, age
가 max-age
보다 커서 데이터가 stale 한 경우 반드시 revalidate 하라는 의미입니다.
must-revalidate
디렉티브 없이도 max-age
를 기준으로 stale하다면 어쨌든 revalidate 하게 되는데, 차이가 발생하는 경우는 원 서버가 네트워크 오류 등으로 인해 사용할 수 없는 상태일 때입니다. must-revalidate
디렉티브가 있다면, 신선도 검사를 시도했을 때 서버가 응답이 불가하다면 504 Gateway Timeout error
를 반환합니다. 반대로 must-revalidate
디렉티브가 없다면 캐시의 가장 최신 데이터를 반환합니다. 서버와의 동기화가 필수라면 반드시 must-revalidate
를 함께 넣어주는 것이 맞습니다.
no-cache vs max-age=0
no-cache
도 max-age=0
도 결국 캐시에 저장은 하되 무조건 요청마다 revalidate를 하게끔 유도하는 디렉티브입니다. 엄밀하게는 위에서 설명 must-revalidate
가 함께 있을 경우 max-age=0
는 no-cache
와 동일하게 동작합니다. 이렇게 다양한 디렉티브로 같은 동작을 구현하게 되는 데는 브라우저간의 차이가 한 가지 이유입니다. 그중 흥미로운 예시로 IE9의 뒤로가기, 앞으로가기가 max-age=0
인지 no-cache
인지에 따라 동작이 다르다고 합니다. 전자라면 최신 캐시를, 후자라면 무조건 refetch 한다고 합니다.
즉, 정리해보면 이렇습니다.
must-revalidate & max-age=0 == no-cache
TL;DR
no-store
: 아예 캐시에 저장하지 마라no-cache
: 캐시에 저장은 해도 되는데 항상 revalidate 해줘라max-age
: 캐시에 저장하고 max-age보다 오래된 데이터면 revalidate 해줘라must-revalidate
: stale하다면 반드시 revalidate 해줘라
정도로 요약이 되겠습니다. 워낙 경우의 수가 다양하고, 캐시의 전략은 각양각색이라 정확하지 않은 정보가 있을 수도 있는 점 참고 부탁드립니다.