HTTP Cache는 js, css, image같은 정적 파일들을 빠르게 응답하기 위해 사용한다. URL을 key로 사용해 hit여부를 판단해서 정적 파일들을 빠르게 가져온다. GET메서드에서만 사용되고, POST나 PUT같은 메서드에서는 캐싱을 하지 않는다.
HTTP Cache는 브라우저에 저장되는 PrivateCache와 서버에 저장되는 SharedCache로 나뉜다. 각 Cache가 어떻게 작동하는지, 어떤 설정을 줘서 웹 성능을 향상시킬 수 있는 지 알아보자.
PrivateCache는 memory cache와 disk cache로 나뉜다.
용어 그대로 disk cache는 사용자의 하드디스크에, memory cache는 사용자의 RAM에서 저장된다.
당연하게도, 속도는 memory cache가 빠르지만 지속성이 없고 저장할 수 있는 용량이 작다.


아래 사진 처럼 사용자의 디스크에 cache들이 저장된다.

브라우저가 아닌 서버에서 저장하는 캐시이다. 프록시 서버나 CDN에서 관리한다.
Shared Cache는 proxy cache와 managed cache로 나뉜다.
캐시는 revalidate를 수행할 수 있다. Cache에 있는 파일이 최신 파일인지를 검증한다.

우아한테크코스 구구의 HTTP 활용하기 강의
validate를 위해선 조건부 요청(conditional request)을 해야한다. 조건부 요청에서 가장 많이 알려진 방식은 If-Modified-Since , If-None-Match 가 있다.

우아한테크코스 구구의 HTTP 활용하기 강의
첫 번째 방법은 Last-Modified와 If-Modified-Since를 사용해 검증하는 방법이다. 플로우는 다음과 같다.
Last-Modified헤더를 같이 보내준다. If-Modified-Since라는 헤더에 Last-Modified헤더의 값을 담아 보낸다.If-Modified-Since이후에 원본 리소스가 업데이트 되었는 지를 확인한다.Last-Modified & If-Modified-Since 방식은 ms단위로 검증할 수 없고, 스페이스바나 주석추가같이 유의미하지않은 변화에도 파일의 수정날짜가 변경되어 stale해진다는 단점이 있다.
이를 보완한 것이 ETag & If-None-Match방식이다.
ETag는 EntityTag의 약자로 리소스의 식별자 역할을 한다. ETag를 생성하는 방식은 다양하다.
Nginx의 경우에는 수정 시간 content 길이를 해싱한다.
플로우는 다음과 같다.
ETag헤더를 같이 보내준다. If-None-Match라는 헤더에 ETag헤더의 값을 담아 보낸다.If-None-Match값이 원본 리소스의 ETag값과 같은지 판단한다.둘을 같이 명시하면, Last-Modified & If-Modified-Since가 무시당한다. 설령 브라우저가 ETag & If-None-Match를 지원하지 않더라도 무조건 무시당한다!
Last-Modified & If-Modified-Since보다 ETag & If-None-Match를 쓰는 게 더 좋아보여서 Last-Modified & If-Modified-Since는 어떤 상황에서 쓰는 게 좋은 지 궁금해졌다.
스택오버플로우에서 잘 정리가 되어있는데,
파일의 경우에는 파일 마지막 수정날짜를 가져오는 것이 쉽기 때문에 Last-Modified & If-Modified-Since가 더 편할 수 있다.
SQL 쿼리로 구축된 웹 페이지의 경우, 쿼리에서 반환된 데이터가 변경되었는지 확인하는 게 어렵다.(이건 사용자마다 무조건 달라지는 동적페이지가 아니라 인기순 정렬같은 경우를 말하는 듯 싶다.) 모든 쿼리에 마지막 수정 날짜가 있는 경우가 아니라면 Last-Modified를 설정하기가 어렵다. 이런 경우에는 페이지의 content를 해싱해서 ETag로 사용하는 것이 더 쉽다.
즉, 리소스의 성질에 따라서 더 유리한 설정이 존재한다.
그 외,
ETag가 inode를 기준으로 생성된다면? 파일 시스템에 변경이 생긴다면 무조건 ETag가 변한 것으로 간주할 것. Last-Modified가 기준이라면 같은 리소스임을 알 수 있다. (물론 이 경우는 변경이 잘 일어나지 않는 리소스라는게 전제인듯하다.)
크롤링봇이 Last-Modified를 활용할 수도 있기에 일단 넣음
이전 버전과의 호환성 문제
휴리스틱 캐싱을 위해서
Since origin servers do not always provide explicit expiration times, HTTP caches typically assign heuristic expiration times, employing algorithms that use other header values (such as the Last-Modified time) to estimate a plausible expiration time. The HTTP/1.1 specification does not provide specific algorithms, but does impose worst-case constraints on their results. Since heuristic expiration times might compromise semantic transparency, they ought to used cautiously, and we encourage origin servers to provide explicit expiration times as much as possible.
대충 expiration times을 알지 못하면, 휴리스틱하게 캐싱을 하는 데 어려움이 있는 것 같다. 때문에 명시적으로 Last-Modified time을 제공하는 것을 권장한다.
결국 Last-Modified는 조건부 요청에서 사용되지 않더라도 명시적으로 있는 것이 좋다.
Cache-Control헤더를 사용해 캐시에 대한 설정을 할 수 있다.
Cache-Control이 private이라면 SharedCache에는 저장하지 않고, PrivateCache에만 정보를 저장하겠다는 의미이다. 보통 카드정보같이 민감한 정보를 캐시할 때 사용한다.
캐시가 얼마동안 fresh한 지를 의미한다. 캐시의 유효기간이 지나지 않았다면 브라우저는 항상 memory나 disk에서 캐시를 읽어와 사용한다. 유효기간이 지나면 revalidate를 수행한다.
캐시값을 저장하긴 하지만, 매번 revalidate요청을 보낼 때 사용한다. 사실상 max-age=0; must-revalidate와 동일
캐시를 저장하지 않는다!
캐시는 사용하기 이전에 원본 리소스의 상태를 반드시 확인해야한다.
Cache-Control 설정을 따로 해주지 않으면 브라우저가 휴리스틱하게 캐싱을 한다. Cache설정을 안한 상태에서 정적 파일을 새로 배포했는데 클라이언트에게 적용이 안되는 경우, 클라이언트가 임의적으로 휴리스틱 캐싱을 해둔 탓일 수도 있다.
휴리스틱 캐싱을 막기위해 기본적으로 no-cache를 사용하는 게 좋을지도?
캐시는 URL을 기준으로 hit을 판단한다. 캐시무효화는 리소스의 이름을 바꿔 URL을 변경함으로써 캐시를 무효화하는 것을 의미한다.
정적 파일의 경우 변경될 가능성이 적으므로 max-age를 길게 설정하게 된다. 그렇기에 파일이 변경되면 새 파일로 다시 캐시를 해오기까지 시간이 오래 걸릴 수도 있다. 이를 막기 위해 리소스의 이름을 변경해 캐시를 해오게 한다.
아래는 네이밍 예시이다.
# version in filename
bundle.v123.js
# version in query
bundle.js?v=123
# hash in filename
bundle.YsAIAAAA-QG4G6kCMAMBAAAAAAAoK.js
# hash in query
bundle.js?v=YsAIAAAA-QG4G6kCMAMBAAAAAAAoK