[HTTP 완벽 가이드] 7장 캐시
HTTP 완벽 가이드의 7장 캐시 챕터를 읽고 기억에 남는 것만 정리해보는 글. 사견이 들어가 있음 주의.
캐시란 개념은 상당히 광범위하다. 프론트엔드를 주로 다룬다면 보통 프레임워크단에서 제공하는 Store, 그리고 HTTP 통신 시 사용할 수 있는 브라우저 캐시 정도는 접해본 적이 있을 것이다. 이번 글에서는 이들 중 HTTP 캐시란 무엇인지, 그리고 어떻게 사용하면 좋을지에 대해 다뤄보고자 한다.
HTTP 캐시란?
웹 캐시란 자주 쓰이는 문서의 사본을 자동으로 보관하는 HTTP 장치다. 웹 요청이 캐시에 도달했을 때, 캐시된 로컬 사본이 존재할 경우 그 문서는 원 서버가 아니라 그 캐시로부터 제공된다. 이는 불필요한 HTTP 요청 또는 데이터 전송을 줄여주고, 거리로 인한 레이턴시를 줄여주는 효과가 있다. 즉 웹사이트의 성능을 개선하고 비용을 줄일 수 있는 효율적인 수단이라고 볼 수 있다.
캐시 처리 단계
HTTP GET 요청이라 가정해보면, 웹 캐시는 다음의 일곱 단계를 거쳐 처리될 것이다.
1. 요청 받기
- 네트워크 커넥션에서 활동을 감지하고, 들어오는 데이터를 읽어들인다.
2. 파싱
- 캐시는 요청 메세지를 여러 부분으로 파싱하여 헤더 부분을 조작하기 쉬운 자료 구조에 담는다.
- 캐싱 소프트웨어가 쉽게 헤더를 처리하고 조작하기 위해서다.
3. 검색
- URL을 알아내고 해당하는 로컬 사본이 있는지 검사한다.
- 로컬 사본은 메모리, 디스크에 저장되어 있을 수도 있고, 근처의 다른 컴퓨터에 존재할 수도 있다.
- 전문적인 캐시는 객체를 로컬에서 가져올 수 있는지 판단하기 위한 빠른 알고리즘을 사용한다.
4. 신선도 검사
- 사본의 유효기간(신선한 기간)을 체크, 신선도 기간이 지났을 경우 문서에 변경이 있는지 서버와 재검사를 한다.
- HTTP의 신선도 검사 규칙은 매우 복잡하다.
5. 응답 생성
- 캐시된 서버 응답 헤더를 토대로 응답 헤더를 생성한다.
- 캐시는 클라이언트에 맞게 헤더를 조정할 책임이 있다. (ex. HTTP 버전에 따른 헤더 번역, Via 헤더 삽입 등)
6. 전송
- 응답 헤더가 준비되면 캐시는 응답을 클라이언트에게 돌려준다.
7. 로깅
- 각 캐시 트랜잭션이 완료된 후, 캐시는 통계 캐시 적중과 부적중 횟수 통계를 내고, 요청 종류, URL 등 정보를 추가 로깅할 수 있다.
- 많이 쓰이는 캐시 로그 포맷으로는 스퀴드 로그 포맷, 넷스케이프 확장 공용 로그 포맷 등이 있다.
2022년인 현재에도 많이 쓰이는진 잘 모르겠다.
그렇다면 무조건 캐싱을 시키면 될까?
캐시는 분명 유용하다. 그러나 모든 문서의 사본을 저장해서 쓸 순 없다. 캐시에 요청이 도착했을 때, 만약 그에 대응하는 사본이 있다면 사본만으로 요청이 처리될 수 있다. 이를 캐시 적중(cache hit)이라고 부른다. 반면 대응하는 사본이 없다면 그냥 원 서버로 전달되기만 할 것이다. 이를 캐시 부적중(cache miss)이라고 부른다.
원 서버의 콘텐츠는 당연히 변경될 수 있다. 따라서 캐시는 가지고 있는 사본이 여전히 최신인지 아닌지 서버를 통해 점검해야 한다. 이러한 '신선도 검사'를 HTTP 재검사(Revalidation)라고 부른다. 네트워크 대역폭을 아끼기 위해, 대부분의 캐시는 클라이언트가 사본을 요청하였으며 그 사본이 검사를 할 필요가 있을 정도로 충분히 오래된 경우에만 재검사를 한다.
캐시 사본을 신선하게 유지하기
HTTP는 어떤 캐시가 사본을 갖고 있는지 서버가 기억하지 않더라도, 캐시된 사본이 서버와 충분히 일치하도록 유지할 수 있게 해주는 단순한 메커니즘을 갖고 있다. 이 메커니즘을 문서 만료와 서버 재검사라고 부른다.
1. 문서가 만료되었는가
서버 응답 헤더에 Cache-Control, Expires 헤더를 사용한다. 캐시 문서가 만료되기 전에, 캐시는 서버와의 접촉 없이 사본을 제공할 수 있다. 그러나 캐시된 문서가 만료되면, 캐시는 반드시 재검사를 통해 신선한 사본을 얻어와야 한다. (물론 새 유효기간과 함께!)
Expires: Fri, 05 Jul 2002, 05:00:00 GMT
Cache-Control: max-age=484200
2. 유효기간과 나이(age)의 차이
Expires와 Cache-Control:max-age 헤더는 기본적으로 같은 일을 하지만, 절대 시간을 사용할 경우 컴퓨터의 시계가 올바르게 맞추어져 있다는 것이 전제되어야 한다.
헤더 | 설명 |
Cache-Control: max-age | max-age 값은 문서의 최대 나이를 정의한다. 단위는 초. ex. Cache-Control: max-age=484200 |
Expires | 절대 유효기간을 명시한다. Expires: Fri, 05 Jul 2002, 05:00:00 GMT |
3. 서버 재검사
캐시된 문서가 만료되었다 = 다시 검사할 시간이 되었다는 뜻이다. 이는 그 문서가 원 서버에 존재하는 것과 실제로 다르다는 것을 의미하지는 않으며, 다만 검사할 시간이 되었음을 알려준다. 캐시는 원 서버에게 문서가 변경되었는지 여부를 물어보게 된다.
- 재검사 결과 콘텐츠가 변경되었다면, 캐시는 그 문서의 새로운 사본을 가져와 오래된 데이터 대신 저장한 뒤 클라이언트에게도 보내준다.
- 재검사 결과 콘텐츠가 변경되지 않았다면, 캐시는 새 만료일을 포함한 새 헤더들만 가져와서 캐시 안의 헤더들을 갱신한다.
따라서 HTTP 프로토콜은 캐시가 다음 중 하나를 반환하도록 요구한다.
- 충분히 신선한 캐시 사본
- 원 서버와 재검사되었기에 충분히 신선해진 캐시 사본
- 에러 메세지(재검사해야하는 원 서버가 다운된 경우)
- 경고 메세지가 부착된 캐시 사본
4. 조건부 메서드와의 재검사
HTTP의 조건부 메서드(조건부 GET요청)는 재검사를 효율적으로 할 수 있도록 해준다. 이는 서버가 갖고 있는 문서가 캐시가 갖고 있는 것과 다른 경우에만 객체 본문을 보내달라고 하는 것이다. 이를 통해 신선도 검사와 객체를 받아오는 것이 하나의 조건부 GET으로 결합된다. 캐시 재검사를 할 때 유용한 조건부 요청 헤더는 다음과 같다.
헤더 | 설명 |
If-Modified-Since: <data> | 만약 문서가 주어진 날짜 이후로 수정되었다면 요청 메서드를 처리한다. 캐시된 버전으로부터 콘텐츠가 변경된 경우에만 데이터를 가져오기 위함. 따라서 서버의 Last-Modified 응답 헤더와 함께 사용된다. |
If-None-Match: <tags> | 마지막 변경일을 맞춰보는 대신, 서버는 문서에 대한 일련번호와 같이 동작하는 태그 (ETag)를 제공할 수 있다. If-None-Match 헤더는 캐시된 태그가 서버에 있는 문서의 태그와 다를 때에만 요청을 처리한다. |
캐시는 캐시된 버전이 갖고 있는 것에 대해 최신인지 확인하기 위해 엔터티 태그(ETag)를 사용한다. 때때로 서버는 모든 캐시 사본을 무효화시키지 않고 문서를 살짝 고칠 수 있도록 허용하고 싶은 경우가 있다. HTTP/1.1은 비록 콘텐츠가 조금 변경되었더라도 “그 정도면 같은 것"이라고 서버가 주장할 수 있도록 해주는 “약한 검사기(weak validator)”를 지원한다.
강한 검사기(strong validator)는 콘텐츠가 바뀔 때마다 바뀐다. 약한 검사기는 어느 정도 콘텐츠 변경을 허용하지만, 콘텐츠의 중요한 의미가 변경되면 함께 변경된다. 서버는 “W/” 접두사로 약한 검사기를 구분한다.
ETag: W/"v2.6"
If-None-Match: W/"v2.6"
언제 ETag를 사용하고 언제 Last-Modified 일시를 사용하는가
HTTP/1.1 클라이언트는 만약 서버가 ETag를 반환했다면 반드시 ETag 검사기를 사용해야 한다. 만약 서버가 Last-Modified 값만을 반환했다면, 클라이언트는 If-Modified-Since 검사를 사용할 수 있다. 만약 엔터티 태그와 최근 변경일시가 모두 사용 가능하다면, HTTP1.0, HTTP1.1 캐시 모두 적절히 응답할 수 있도록 클라이언트는 두 가지의 재검사 정책을 모두 사용해야 한다.
캐시 제어
문서가 만료되기 전까지 얼마나 오랫동안 캐시될 수 있게 할 것인가? HTTP는 이를 위한 여러 방법을 제시한다.
no-cache와 no-store 응답 헤더
이 두 헤더는 캐시가 검증되지 않은 캐시된 객체로 응답하는 것을 막는다. 'no-store'가 표시된 응답은 캐시가 그 응답의 사본을 만드는 것을 금지한다. 즉, 클라이언트에게 no-store 응답을 전달하고 나면 객체를 삭제할 것이다. 'no-cache'로 표시된 응답은 사실 로컬 캐시 저장소에 저장될 수 있다. 다만 먼저 서버와 재검사를 하지 않고서는 캐시에서 클라이언트로 제공될 수 없을 뿐이다. 헤더 이름만 보면 절대 캐시를 하면 안 될 것만 같은데, 작명이 잘못된 것 같다. 이 책의 저자조차 차라리 "Do-Not-Serve-From-Cache-Without-Revalidation" 이런 이름이 낫겠다고 말하고 있다.
Max-Age 응답 헤더
Cache-Control: max-age 헤더는 초로 나타내고, 리소스가 유효하다고 판단되는 최대 시간이라고 볼 수 있다. 이를 0으로 설정할 경우, 캐시가 매 접근마다 문서를 캐시하거나 리프레시하도록 요청할 수 있다.
Expires 응답 헤더
이 헤더는 현재 Deprecated 되었다. 그래도 굳이 설명을 덧붙여보자면, 초 단위의 시간 대신 실제 만료 날짜를 명시한다. HTTP를 설계한 사람들은 많은 서버가 동기화되어있지 않거나 부정확한 시계를 갖고 있기 때문에, 만료를 이 헤더처럼 절대시각으로 표현하는 것은 문제의 소지가 있다고 판단했다. 대신 max-age같은 경과된 시간으로 표현하는 방법으로 대체될 수 있을 것이다.
Must-Revalidate 응답 헤더
만약 캐시가 만료 정보를 엄격하게 따르길 원한다면, 원 서버는 다음과 같은 Cache-Control을 붙일 수 있다.
Cache-Control: must-revalidate
이 헤더는 캐시가 객체의 신선하지 않은 사본을 원 서버와의 최초의 재검사 없이는 제공해서는 안 됨을 의미한다. 그런데 만약 캐시가 must-revalidate 신선도 검사를 시도했을 때 원 서버가 사용할 수 없는 상태라면, 캐시는 반드시 504 Gateway Timeout error를 반환해야 한다.
그렇다면 이 헤더는 no-cache와 뭐가 다른 것일까? Must-Revalidate 헤더는 캐시 사본이 신선한 경우 원 서버와의 재검사 없이 바로 객체를 반환할 수 있다. 반면 no-cache 의 경우 매번 원 서버와 재검사를 해야 한다. 즉, no-store 상태란 must-revalidate이며 max-age=0인 경우와 같다고 볼 수 있겠다.
Reference
데이빗 고울리 외 4명, <HTTP 완벽 가이드> 7장