HTTP 캐시 검증과 전략

자주 요청되는 HTTP 리소스는 캐시를 고려해 볼 수 있다.

HTTP Cache-Control 헤더는 캐시의 생명주기를 결정한다.

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2025-01-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_11.59.21.png

Cache-Control: private, max-age=180 으로 설정한 index.js 파일과 index.css 파일은 리소스 재요청 시 (memory cache) 된 것을 확인할 수 있다.

Memory Cache 는 Disk Cache 보다 접근이 빠르다. - Naver D2, Android 웹뷰의 캐시 분석

리소스는 최대 3분 브라우저에 캐시된다. 3분이 경과되면 리소스는 서버로 재요청된다.

Chrome 개발자 도구에서 지원하는 Disable Cache 기능을 사용하면 HTTP Request Headers 에 Cache-Control: no-cache 로 설정되며 브라우저는 캐시 리소스가 있더라도 사용하지 않고 서버에 리소스를 요청한다.

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2025-01-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_11.59.47.png

캐시 만료 ≠ 리소스 갱신

캐시가 만료되면 클라이언트는 서버에 리소스를 재요청했다.

하지만 서버는 리소스는 변경하지 않았을 수 있다. 캐시 만료는 리소스 갱신을 의미하지 않는다.

클라이언트는 리소스 변경 여부를 조건부 요청을 통해 재검증 할 수 있다.

재요청 시 index.html 파일을 확인해보면 상태 코드 304 Not Modified를 확인해 볼 수 있다.

# Response Headers
HTTP/1.1 304 Not Modified
Date: Sat, 18 Jan 2025 13:15:54 GMT
Last-Modified: Sat, 18 Jan 2025 13:13:08 GMT
ETag: "62fc3fbfecbe1f72465cbdf151be1fc3"
Server: AmazonS3

# Request Headers
GET / HTTP/1.1
Accept: text/html
Accept-Encoding: gzip, deflate
Accept-Language: ko,en;q=0.9
Cache-Control: max-age=0
Connection: keep-alive
Host: jaehun.link.s3-website.ap-northeast-2.amazonaws.com
If-Modified-Since: Sat, 18 Jan 2025 13:13:08 GMT
If-None-Match: "62fc3fbfecbe1f72465cbdf151be1fc3"

서버는 리소스를 식별할 수 있는 Last-Modified(최종 수정일), ETag(리소스 태그)를 지정할 수 있다.

클라이언트는 캐시가 만료되면 조건부 요청 If-Modified-Since(수정일이 지났는지), If-None-Match(리소스 태그가 달라졌는지) 재검증 할 수 있다.

검증에 성공하면 리소스를 새로 받도록 한다. 만약 검증에 실패했다면 리소스가 변경되지 않았다는 304 Not Modified를 반환하고 HTTP Response 는 반환하지않는다. HTTP Response 만큼 네트워크 비용은 개선된다.

캐시 제어

다음은Cache-Control 을 지정하지 않은 리소스를 재요청한 네트워크다.

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2025-01-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_10.49.01.png

어떻게 캐시가 적용된 것일까?

HTTP Cache-Control 이 없고 만약 Last-Modified 응답 헤더가 있다면 휴리스틱 캐시가 적용된다.

특정 계산을 통해 임의의 시간이 설정되므로 HTTP Cache-Control 헤더 설정이 꼭 필요하다.

Cache-Control Directivesno-cacheno-store 는 헷갈리는 개념이다.

no-cache 는 이름 자체에서도 오류를 범할 수 있는데. 캐시를 허용하는 설정이다. 캐시를 허용하지만 재검증 요청을 수행한다.

반면 no-store 는 캐시를 허용하지 않는 옵션이다. 서버는 민감한 리소스에 해당 옵션을 사용한다.

no-cache 는 서버에 항상 재검증 요청을 보낸다. 재검증에 실패하면 캐시된 데이터를 응답한다.

재검증에 실패하는 상황에는 재검증을 수행하지 못하는 상황도 있다.

우리는 사용자가 서버와 물리적으로 먼 거리에 위치해 있을 때 CDN 과 같은 캐시 서버를 중간에 위치시킨다.

캐시 서버는 Origin 서버와 네트워크 통신한다. 만약 네트워크가 순간적으로 단절되어 재검증을 하지 못하는 상황이라면 어떨까? 이때 재검증은 수행과 관계없이 실패로 판단되며 클라이언트에 캐시 서버는 캐시된 리소스를 반환한다.

이 상황이 문제가 되는 이유는 무엇일까? 리소스가 신선도를 항상 유지해야하는 상황이라면 문제가 된다. (계좌의 잔액이 실시간으로 반영되지 않는다거나 입금한 내역이 반영되지 않는 상황)

네트워크가 단절되어 오래된 리소스를 보여주는 것이 오히려 혼란을 일으킬 수 있다면 차라리 오류를 반환하는 것이 옳은 판단이 될 수 있다.

must-revalidate 는 재검증에 실패하면 504 Gateway Timeout 상태 코드를 반환하며 오류를 일으킨다.

HTTP 확장으로 stale-while-revalidate 는 오래된 리소스를 사용하는 설정이다.

max-age=15, stale-while-revalidate=30 로 설정된 리소스는 15초 동안 캐시되며 재사용된다. 15초가 경과하고 15초 동안 재검증을 수행하며 캐시된 리소스를 사용하면서 재검증이 성공하면 백그라운드에서 새로운 리소스가 갱신된다. 갱신된 리소스는 30초가 경과한 이후부터 적용되며 그 동안은 오래된 리소스를 계속 사용한다.

캐시 전략

CDN 을 사용하면 캐시를 두 곳에서 관리해야한다. (브라우저 캐시, CDN 캐시)

브라우저 캐시는 사용자마다 관리되기 때문에 CDN 캐시를 무효화하더라도 브라우저 캐시가 무효화되지 않는다. 브라우저 캐시는 원격으로 제어할 수 없다.

max-ages-max-age 설정으로 CDN 만으로 캐시 제어가 가능하도록 할 수 있다.

max-age=0 으로 설정한 리소스는 캐시가 곧바로 만료되므로 항상 서버로 요청된다. 자연스럽게 클라이언트는 브라우저 캐시를 사용할 수 없는 상황이 된다. 그리고 s-max-age 로 CDN 캐시 시간을 설정하여 리소스를 캐시한다.

새로운 버전으로 리소스가 업데이트되면 CDN 캐시를 전부 무효화해야할까? 바뀐 파일만 변경할 수 없을까?

JavaScript 나 CSS 파일은 빌드 시 보통 해싱된 이름으로 생성된다. 파일의 내용을 기반으로 고유하게 해싱된다. 해싱 처리는 리소스를 식별하게 하므로 캐싱이 무효화된다. (의존 파일도 새로 해싱 되어야한다.)

반면 해싱이 처리되지 않은 리소스는 리소스를 식별할 수 있는 무언가가 필요하다. 위에서 언급한 재검증 요청이 될 수 있고 강제로 CDN 캐시를 무효화시킬 수 있다.