- Published on
HTTP Cache 이해하기
HTTP
HTTP Cache 에 대해서 알려면 먼저 HTTP 에 대한 이해가 필요합니다. HTTP 만해도 사실 어마어마한 분량을 자랑하는 내용을 담고있는 부분이라 여기서는 간략하게 캐시와 관련이 있을 내용만 정리하고 넘어가려고 합니다.
HTTP 는 Hypertext Transfer Ptorocol 의 약자로 모든 웹 브라우저와 서버, 웹 어플리케이션은 HTTP 를 통해서 서로 대화합니다. HTTP 는 웹상에서 배달부역할이며 배달을 정확히 하기 위해서는 일정한 규칙을 가진 주소에 대한 정보가 필요하듯이 웹에서도 요청과 응답을통해 리소스를 전달받으려면 대화가 통해야 합니다. 올바른 대화를 하기위해서 HTTP 는 특별한 규약을 갖고 서로 통신하게 됩니다.
우리는 앉은자리에서 인터넷만 연결되어있으면 지구 반대편에 있는 리소스도 다운로드 받을 수 있습니다. 즉 거리가 아무리 멀더라도 HTTP 통신 규약에 의해서 데이터가 손상되거나 꼬이지 않음을 보장받을 수 있습니다. 이러한 리소스들은 웹 서버에서 관리하고 제공합니다.
HTTP 통신은 요청과 응답으로 동작합니다. 그리고 이 요청과 응답에는 모두 헤더가 포함되어 있습니다. 이 헤더를 HTTP 메세지 헤더라고 부릅니다. 이 메세지 헤더에 캐시와 관련된 필드들이 포함되어 있습니다.
캐시란?
캐시라는 개념자체가 사실 그렇게 어려운건 아닙니다. 위키백과에 따르면 데이터나 값을 미리 복사해놓는 임시장소를 가리킨다고 합니다. 즉 원본데이터에 접근하는 시간을 단축시키고 더 빠르게 리소스를 전달받기위해 활용한다고 볼 수 있습니다. 캐시는 다양한 곳에서 활용되고 있습니다.
캐시는 지역성을 갖는데
- 시간적 지역성
- 공간적 지역성
두가지를 갖습니다. 캐시는 값을 미리 복사해두는 임시 저장소 같은 역할을 한다고 했습니다. 이때 지역성이란 캐싱될 데이터에 대한 접근이 시간적 또는 공간적으로 가깝게 일어나는 것을 말합니다.
시간적 지역성
시간적 지역성은 특정 데이터에 한번 접근하고 나서 가장 가까운 미래에 다시 한번 동일 데이터에 접근할 가능성이 높은것을 말합니다.
공간적 지역성
특정 데이터가 저장된 메모리 주소와 가까운 주소에 접근하는것을 공간적 지역성 이라고 합니다. CPU 캐시나 디스크 캐시의 경우 특정 메모리 주소에 접근할때 해당 주소 뿐만 아니라 해당 블록 자체를 전부 캐싱합니다.
여기서 CPU 캐시나 디스크 캐시에 별도로 지정해 줄 수 있는것은 아니고 리소스의 사이즈나 요청 빈도수에 따라서 브라우저에서 자체적인 알고리즘으로 처리하는 부분입니다.
캐시 활용의 장점
네트워크 비용 절감
캐시는 한번 접근했던 데이터를 기억하고 있다고 했습니다. 때문에 동일한 요청에 의해서 동일한 리소스를 반환해야 할때 본 서버까지 요청이 전달되지 않고도 캐시에서 처리가 가능하기때문에 불필요한 요청을 줄여 네트워크 비용을 줄일 수 있습니다.
빠른 응답
본 서버까지 갔다가 리소스를 받는것이 아니라 가장 가까운 캐시에서 다시 리소스를 받아오기때문에 첫 요청 이후에는 더 빠른 응답을 기대할 수 있습니다. 물리적인 거리가 멀면 자연스레 지연시간이 발생 할 수 있는데 이런 지연 시간을 줄일 수 있습니다. 비슷한 개념으로 CDN 이 있습니다.
서버의 부하 감소
불필요한 중복 요청에 대한 동일한 리소스의 반환은 캐시에서 처리하기때문에 병목현상을 줄일 수 있고 서버의 부하도 줄여서 정말 필요한 요청에 대한 필요한 응답만 할 수 있습니다.
캐시 처리 단계
- 캐시는 네트워크로부터 도착한 요청 메세지를 읽습니다.
- 캐시는 메세지를 파싱하여 URL 과 헤더들을 추출합니다.
- 캐시는 로컬 복사본이 있는지 검사하고 복사본이 없다면 사본을 서버로부터 받아옵니다. 그리고 로컬에 저장합니다.
- 캐시는 복사본이 유요한지 확인하고 유효하지않다면 서버에 변경사항이 있는지 물어봅니다.
- 캐시는 새로운 헤더와 캐시된 본문으로 클라언트에 응답합니다.
캐시와 연관성있는 헤더필드
캐시와 관련있는 헤더필드가 HTTP 에 담겨있다고 했습니다. 어떤 필드들인지 확인해보려고 합니다.
cache-control
cache-control 에는 다양한 디렉티브를 지정할 수 있고 이 디렉티브는 파라미터를 갖는것도 있고 아닌것도 있습니다. 그리고 디렉티브를 활용하여 캐시의 유효기간, 재검사 방식, 만료시간 등을 지정할 수 있습니다.Cache-Control: private, max-age=0, no-cache, no-store…
위와같이 붙여서 사용할 수도 있고 단일로도 사용할 수 있습니다.
no-cache
캐시를 생성하지않는다 또는 허용하지않는다 라고 생각할수도 있는 디렉티브인데 실제 사용은 캐시는 사용하지만 서버의 확답 없이는 클라이언트에게 응답하지 않겠다 라는 겁니다.Cache-Control: no-cache 를 사용할때 Pragma: no-cache 를 함께 사용하는것이 좋습니다. HTTP/1.0 과의 하위호환성을 위해 HTTP/1.1 에 포함되어 있습니다.
no-store
no-store 디렉티브가 캐시를 하지 않는 디렉티브입니다. 해당 디렉티브를 설정하면 무조건 서버를 통해 리스폰스를 반환하겠다. 서버에 완전한 요청과 응답을 받겠다 라는것과 마찬가지입니다. 캐시를 사용하지않는다 라고 보면 될것같습니다.
private
단어에서 알 수 있듯이 특정 유저만을 대상으로 캐시하는 리소스를 정할 수 있습니다.
public
private 와 반대로 모든 유저에게 동일한 캐시를 응답하겠다 라는 디렉티브입니다.
max-age=0 (단위: 초)
max-age 는 파라미터로 숫자를 가지며 이 숫자의 단위는 초를 나타냅니다. 캐시가 발생한 시점으로부터 지정한 초단위동안 캐시를 유지하겠다는 디렉티브입니다. max-age=31557600 로 설정하면 캐시를 1년동안 유지하겠다 라는것과 같습니다.
Expires
cache-control 에서 사용하는 디렉티브는 아닙니다. cache-control 과 동일하게 헤더필드중 하나인데 max-age 와 비슷하게 캐시 유지 기간을 설정할 수 있습니다. 만약 Expires 헤더필드와 Cache-control: max-age=0 가 함께 존재한다면 HTTP/1.1 캐시 서버는 Expires 헤더필드를 무시합니다. HTTP/1.0 캐시서버는 반대로 max-age 디렉티브를 무시합니다. HTTP 버전에 대해서 매번 체크할 수 없기 때문에 유효기간을 정할때 max-age 디렉티브와 Expires 헤더필드를 함께 설정하는게 좋습니다. Expires 가 max-age 와 다른점은 시간이 아닌 실제 만료 날짜를 명시합니다. Expires: Fri, 05 Jul 2021, 06:00:00 GMT
s-maxage=0
기본적으로 max-age 와 동일한데 다른부분은 여러 유저가 이용하는 공유 캐시 서버에만 적용됩니다. 이 디렉티브가 사용되는 경우 Expires 헤더필드와 max-age 디렉티브는 무시됩니다.
must-revalidate
Cache-Control: must-revalidate 디렉티브는 응답의 캐시가 현재도 유효한지 아닌지 본 서버에 조회합니다. 만약 프록시가 본서버에 도달할 수 없고 리소스를 다시 요구할 수 없는경우 캐시는 클라이언트에 504(Gateway Timeout)를 반환합니다.
여기까지가 Cache-control 과 기간 설정에 관련된 디렉티브와 헤더필드에 대한 내용입니다. 이외에도 다른 디렉티브들이 존재하지만 여기까지가 많이 사용되고 알아둬야하는 내용들 이라고 생각하시면 될것같습니다.
캐시의 재검사와 적중, 부적중
캐시는 Cache-control 의 디렉티브에 따라서 재검사를 실시할 수도 있다고 했습니다. 이때 재검사시 클라이언트의 요청을 그대로 전달하고 서버의 응답을 그대로 받아서 사용한다면 캐시를 사용하는 의미가 없을것입니다. 이 재검사의 요청과 응답은 클라이언트에서 본 서버에 직접 요청하고 응답 받는것보단 빠르지만 캐시만을 사용하여 응답하는것 보다는 느립니다. 그렇다면 어떻게 캐시는 재검사를 하는지 적중과 부적중이란 용어는 무엇인지 알아보겠습니다.
캐시 적중 (Cache Hit)
캐시에 요청이 도착했을 때 만약 그에 대응하는 리소스가 있다면 그것을 활용하여 요청을 처리하는데 이것을 캐시 적중 이라고 합니다.
캐시 부적중(Cache Miss)
만약 요청이 도착했을때 그에 대응하는 리소스가 없다면 본 서버로 요청을 그대로 전달합니다. 이것을 캐시 부적중이라고 합니다.
재검사
HTTP의 조건부 메서드는 재검사를 효율적으로 하게 해줍니다. 다섯가지 조건부 요청 헤더를 정의하는데 그중 캐시 재검사를 유용하게 해주는 두가지가 있습니다.
If-Modified-Since
If-None-Match
HTTP의 모든 조건부 헤더는 If- 접두어로 시작합니다.
If-Modified-Since
헤더의 값은 날짜가 들어갑니다. 만약 리소스가 주어진 날짜 이후로 수정되었다면 요청 메서드를 처리합니다. 이것은 캐시된 버전으로부터 변경된 경우에만 리소스를 가져오기 위해 서버의 응답 헤더필드인 Last-Modified 와 함께 사용됩니다.
만약 주어진 날짜 이후 변경사항이 없다면 서버는 304 Not Modified 응답 메시지를 돌려줍니다. 이때 효율을 위해 본문은 보내지 않습니다. 만약 재검사가 실패해서 캐시 부적중이 발생했다면 새 본문과 함께 200 ok 를 클라이언트에 반환하게 됩니다.
If-None-Match
날짜로 재검사를 수행 했을 경우 적절히 재검사가 이루어지지 않을 상황들이 있습니다.
백그라운드 프로세스에 의해서 일정간격으로 다시 쓰여지는 문서 그러나 실제로는 동일한 데이터를 포함하는 경우 내용은 변화가 없지만 날짜와 시간은 변경될 수 있습니다.
어떤 리소스 또는 문서의 변경은 캐시가 다시 읽어들이기에 매우 사소한 것일 수 있습니다.
어떤 서버는 리소스에 대한 최근 변경일시를 정확하게 판별하기 어려울 수 있습니다.
1초보다 더 작은 단위의 간격으로 갱신되는 문서를 제공하는 서버에는 1초정도의 정밀도가 그리 정확하지 않을 수 있습니다.
이런 케이스의 경우 If-None-Match 헤더필드인 엔터티 태그검사로 재검사를 수행할 수 있습니다. If-None-Match 헤더필드 값에 지정된 엔터티 태그값이 지정된 리소스의 ETag 값과 일치하지 않으면 요청을 받겠다는 뜻입니다.
강한검사기와 약한검사기
재검사를위해 엔터티 태그를 비교할 수 있다고 했습니다. 이때 약간의 변경사항에 대해서는 관대하게 허용하고 싶은 경우가 있을 수 있습니다. HTTP/1.1 은 컨텐츠가 약간의 변경사항이 있더라도 그 정도면 같다 라고 서버가 주장할 수 있도록 해주는 약한 검사기를 지원합니다.
약한 검사기는 엔터티값에 'W/' 접두사가 붙어 어떤 변경사항에도 매번 바뀌어야하는 강한 엔터티태그와 구분할 수 있습니다.
그렇다면 언제 엔터티 태그를 사용하고 언제 Last-Modified 날짜를 사용하는가?
엔터티태그와 Last-Modified 는 둘다 재검사할때 활용한다고 했습니다. 그렇다면 언제 어떤 검사를 수행하는지 궁금할 수 있을것같습니다.
HTTP/1.1 클라이언트는 만약 서버가 엔터티 태그를 반환했다면 반드시 엔터티 태그 검사기를 사용해야 합니다. 만약 Last-Modified 값만 반환했다면 If-Modified-Since 검사를 사용할 수 있습니다.
만약 HTTP/1.1 캐시나 서버가 엔터티태그와 날짜 조건부헤더를 모두 받았다면 두 재검사 조건에 모두 부합되어야만 304 Not Modified 응답을 반환할 수 있습니다. 둘중 하나라도 조건과 일치하지 않는다면 200 OK 응답을 반환하고 이말은 캐시에서 리소스를 받지 않았다는것과 같습니다.
ETag
ETag 헤더필드는 응답 헤더에 포함되어있고 서버는 리소스마다 ETag 값을 할당합니다. ETag 값의 문자열은 특별한 룰이 정해져있는 것이 아니라 서버에 따라 다양한 ETag 값을 할당합니다.
웹을 구성하는 리소스별로는 캐시를 어떻게 적용하면 좋을까?
HTTP 에는 많은 헤더필드가 존재하고 그중에서도 Cache 와 연관성이 있는 헤더필드와 디렉티브에 대해서 알아봤습니다. 그렇다면 과연 이것들을 웹을 구성하는 리소스들에 어떻게 적용하면 좋을지 생각해 봤습니다.
html
SPA 에서의 html 기준으로 보겠습니다. 모던웹 프론트에서의 html 은 하나의 파일안에 자바스크립트와 스타일을 가지며 한페이지 내에서 전부 이루어진다고 볼 수 있습니다. 그리고 빌드를 하게되면 웹팩에 의해서 각 리소스들은 매 빌드시마다 해시값이 붙어서 생성됩니다.
html 은 자바스크립트와 css 리소스를 갖고있기도합니다. 때문에 html 은 항상 최신상태가 유지되어야 합니다.
Cache-Control: no-cache or max-age=0
no-store 를 사용할수도 있겠지만 no-cache 가 항상 최신상태를 유지하면서도 갱신이 필요하지 않다면 불필요한 네트워크 비용의 낭비를 줄이고 no-store 보단 클라이언트에 빠르게 응답할 수 있기때문에 no-cache 를 사용하면 좋을것같습니다.
JS / CSS / IMAGE
자바스크립트와 스타일 리소스는 길게 잡아주어도 될것같습니다. html 이 항상 최신 상태로 유지되기때문에 그 내부에 정의된 리소스들 또한 최신상태로 유지될 수 있고 해시값이 매 빌드시마다 변경되기때문에 동일한 이름의 리소스가 아니라는것은 항상 변경사항이 있다는것이고 캐시에는 새로운 리소스가 저장될것입니다.
Cache-Control: public, max-age: 한달 또는 1년
마무리
여기까지 HTTP Cache 에 대해서 알아 보았습니다. 캐시를 잘 활용한다면 사용자에게 더 나은 웹서비스 경험을 줄 수 있고
또 여러부분에서 불필요한 리소스의 낭비를 줄일 수 있다는걸 확인 할 수 있었고 HTTP 버전에 따라 같은내용이지만 다른 헤더필드를 정해줘야 하는것도 알 수 있었습니다.
캐시와 웹 서비스의 성능 개선에 도움이 되기를 바랍니다.
참고자료
- HTTP 완벽 가이드
- 그림으로 배우는 HTTP & Network Basic
- MDN cahce-control
- 토스 테크 블로그: 웹 서비스 캐시 똑똑하게 다루기