안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

웹, 앱 (Web, Application)

[Web] HTTP 캐싱 (Caching) : Cache-Control 헤더

피그브라더 2021. 5. 10. 23:35

캐싱(Caching)이란 한 번 가져온 데이터를 가까운 곳에 저장해 두고 다음번에 다시 먼 곳에서 그것을 가져올 필요 없이 저장해둔 것을 사용하는 일종의 성능 향상 기법이다. 그런데 캐싱은 특정 분야에서만 활용되는 기법이 아니다. 예를 들어, 하드웨어 수준에서는 먼 곳에 있는 메인 메모리 대신 가까운 곳에 있는 캐시 메모리에 자주 사용되는 데이터를 저장해둬서 CPU가 데이터를 빠르게 가져올 수 있도록 하는데, 이것도 캐싱의 한 예이다. 또한 일상생활에서도 쉽게 발견할 수 있다. 자주 사용하는 물건을 먼 곳에 있는 선반에 두지 않고 책상 바로 위에 올려두는 것도 캐싱의 한 예이다. 또는 자주 사용하는 프로그램의 아이콘을 바탕화면에 두는 것도 마찬가지이다. 이처럼 캐싱은 정말 다양한 곳에서 활용되고 있다.

 

그중에서 오늘 우리가 공부할 것은 HTTP(S) 프로토콜을 이용한 웹 통신 과정에서 일어나는 캐싱, 줄여서 HTTP 캐싱이다. 결론부터 짧게 요약을 해보자면, HTTP 요청을 매번 서버에게 보내지 않고 한 번 받았던 HTTP 응답을 재활용함으로써 성능도 향상하고 서버의 부담도 줄이는 캐싱이다. 그러면 이제 이것에 대해 조금 더 자세히 알아보도록 하자.

 

1. HTTP 캐시의 종류

앞서 말했듯 HTTP 캐싱이란 결국 HTTP 응답을 저장해 두고 다음번에 동일한 HTTP 요청이 시도되면 저장해 둔 HTTP 응답을 재활용하는 것이다. 그렇다면 HTTP 응답을 저장해 두는 저장소는 어디일까? 크게 두 가지로 분류해볼 수 있다.

 

출처 : MDN

 

먼저, 사설 캐시(Private Cache)가 있다. 지역 캐시(Local Cache)라고도 부른다. 이는 한 사용자에 의해서만 재활용될 수 있는 것들이 저장되는 저장소이다. 최초의 HTTP 요청은 서버에게 전송되어 HTTP 응답을 받아오게 되고, 이를 사설 캐시에 저장해 두면 다음번에 동일한 HTTP 요청이 시도될 때는 서버에 해당 HTTP 요청을 다시 보내지 않고 저장되어 있는 HTTP 응답을 재활용하게 된다. 대표적인 사설 캐시로는 브라우저 캐시가 있다. 브라우저 캐시는 기본적으로 사용자가 HTTP 요청을 통해 다운로드한 모든 문서들을 저장하고 있다. 이렇게 저장된 문서들은 뒤로 가기 혹은 앞으로 가기를 할 때, 문서 저장을 할 때, 페이지 소스 보기를 할 때 등의 경우에 재활용이 된다.

 

다음으로, 공유 캐시(Shared Cache)가 있다. 이는 여러 사용자들에 의해 재활용될 수 있는 것들이 저장되는 저장소이다. 최초의 HTTP 요청은 공유 캐시를 거쳐 서버에게 전송되어 HTTP 응답을 받아오게 되고, 이를 공유 캐시에 저장해 두면 다음번에 동일한 HTTP 요청이 공유 캐시에게 전달될 때 서버에 해당 HTTP 요청을 다시 보내지 않고 저장되어 있는 HTTP 응답을 클라이언트에게 반환하게 된다. 대표적인 공유 캐시로는 프록시 캐시가 있다. ISP 혹은 회사 측에서 로컬 네트워크 인프라의 일부로서 구축해둔 웹 프록시가 이러한 역할을 담당할 수 있다. 그러면 자주 사용되는 데이터들의 경우에 네트워크 트래픽을 최소화하여 많은 구성원들이 해당 데이터를 효율적으로 사용할 수 있게 되는 것이다.

 

브라우저 캐시와 프록시 캐시 외에도 다양한 종류의 캐시가 존재한다. 게이트웨이 캐시, CDN, 리버스 프록시 캐시, 로드 밸런서 등이 대표적인 예시이다. 그러나 이번 포스팅에서 이러한 것들까지는 다루지 않는다. 이번 포스팅에서는 하단에 링크되어 있는 MDN 문서에서 소개하는 정도의 내용만을 다루도록 할 것이기 때문이다.

 

2. HTTP 캐시 엔트리 (= 캐싱하는 대상)

HTTP 캐시에 저장되는 데이터 뭉치 하나하나를 캐시 엔트리라고 부른다. 그리고 각 캐시 엔트리를 구분하는 기준은 캐시 키(Cache Key)이다. 기본적인(Primary) 캐시 키는 HTTP 요청의 메소드와 URI의 조합으로 결정된다(일반적으로 GET 요청에 대해서만 캐싱하므로 URI로만 결정되는 경우도 있음). 즉, 간단히 생각해서 메소드와 URI가 동일한 하나의 HTTP 요청은 하나의 캐시 엔트리에 대응하는 것이다. 캐시 엔트리의 일반적인 형태를 살펴보면 다음과 같다.

 

  • HTML 문서, 이미지, 파일 등의 리소스를 포함하는 GET 요청에 대한 200 (OK) 응답
  • 301 (Moved Permanently) 응답
  • 404 (Not Found) 응답
  • 206 (Partial Content) 응답
  • (캐시 키로 사용하기에 적절한 무언가가 정의된 경우) GET이 아닌 HTTP 요청에 대한 HTTP 응답

 

이때 주의해야 할 것은, 하나의 캐시 엔트리가 여러 개의 HTTP 응답들로 구성되어 있을 수도 있다는 것이다. 이 경우에 해당 HTTP 응답들은 그 캐시 엔트리 내에서 두 번째(Secondary) 키에 의해 구분이 된다. 이는 보통 그 캐시 엔트리에 대응하는 HTTP 요청이 컨텐츠 협상(Content Negotiation)의 타겟인 경우에 해당한다. 이와 관련한 더 자세한 내용은 여기를 읽어보도록 하자.

 

3. Cache-Control 헤더

HTTP/1.1의 Cache-Control 헤더에는 HTTP 요청/응답에서의 캐싱 메커니즘을 결정하는 여러 디렉티브(Directive)들을 나열할 수 있다. 각각의 디렉티브는 캐싱을 어떻게 할 것인지와 관련된 일종의 옵션이라고 보면 된다. 이때 Cache-Control 헤더는 HTTP 요청 헤더와 HTTP 응답 헤더에 모두 사용할 수 있지만, 각각에 나열하는 디렉티브들은 서로 다른 의미를 지니며 나열할 수 있는 디렉티브들의 종류도 조금씩 다르다. 따라서 HTTP 요청의 Cache-Control 헤더에 나열된 디렉티브들이 반드시 HTTP 응답의 Cache-Control 헤더에 나열되리라는 보장은 없다. 디렉티브 이름은 대소문자를 구별하지 않으며, 여러 디렉티브들을 나열하는 경우에는 콤마로 구분한다.

 

3-1. HTTP 요청의 Cache-Control 헤더에 나열할 수 있는 디렉티브

디렉티브 설명
max-age=<seconds> 명시된 시간보다 나이를 많이 먹은 HTTP 응답은 받아들이지 않겠다는 것을 나타낸다. 그리고 max-stale 디렉티브가 존재하지 않는다면 만료된 HTTP 응답도 받아들이지 않는다.

참고로, 캐시에 남아 있는 기존 HTTP 응답을 삭제하려면 max-age=0 디렉티브를 명시하면 된다. 이는 서버에게 검증(Validation) 요청을 보내도록 강제할 것이기 때문이다.
max-stale[=<seconds>] 만료된 HTTP 응답을 받아들이겠다는 것을 나타낸다. 만약 이 디렉티브에 시간이 명시된다면 만료 이후의 초과 시간이 그 시간보다 크지 않은 HTTP 응답을 받아들이겠다는 것이고, 시간이 명시되지 않는다면 만료된 HTTP 응답을 무조건 받아들이겠다는 것이다.
min-fresh=<seconds> 만료될 때까지 명시된 시간 이상의 시간이 남은 HTTP 응답을 받아들이겠다는 것을 나타낸다. 즉, 최소한 명시된 시간 만큼은 유효할 HTTP 응답을 받아들이겠다는 것이다.
no-cache 저장되어 있는 HTTP 응답을 사용하기 전에 반드시 서버에게 검증(Validation) 요청을 보내야 한다는 것을 나타낸다.
no-store 해당 HTTP 요청을 통해 받아오는 HTTP 응답을 어떠한 종류의 캐시에도 저장하면 안 된다는 것을 나타낸다. 이때 주의해야 할 것은, 이미 캐시에 해당 HTTP 요청에 대한 HTTP 응답이 저장되어 있다면 그것은 평소와 같이 재활용될 수 있다는 것이다. 즉, 이 디렉티브는 새로운 HTTP 응답을 저장할 것이냐의 문제인 것이다.

 

3-2. HTTP 응답의 Cache-Control 헤더에 나열할 수 있는 디렉티브

디렉티브 설명
must-revalidate 해당 HTTP 응답이 만료되었다면 반드시 서버에게 검증(Validation) 요청을 보내야 한다는 것을 나타낸다. 이는 설령 만료된 HTTP 응답을 받아들이도록 설정이 되어 있는 캐시라고 할지라도 마찬가지로 적용된다.
no-cache 해당 HTTP 응답을 사용하기 전에 반드시 서버에게 검증(Validation) 요청을 보내야 한다는 것을 나타낸다. 이는 설령 만료된 HTTP 응답을 받아들이도록 설정이 되어 있는 캐시라고 할지라도 마찬가지로 적용된다.
no-store 해당 HTTP 응답을 어떠한 종류의 캐시에도 저장하면 안 된다는 것을 나타낸다.
public 해당 HTTP 응답이 어떠한 캐시에도 저장될 수 있다는 것을 나타낸다. 이는 보통의 경우라면 캐시가 되지 않는 유형의 HTTP 응답까지도 캐싱될 수 있도록 한다.
private 해당 HTTP 응답이 사설 캐시에만 저장될 수 있다는 것을 나타낸다. 이는 보통의 경우라면 캐시가 되지 않는 유형의 HTTP 응답까지도 캐싱될 수 있도록 한다.
proxy-revalidate must-revalidate 디렉티브와 완전히 동일한 의미를 갖는다. 단, 공유 캐시에만 적용되기 때문에 사설 캐시에서는 무시된다.
max-age=<seconds>
해당 HTTP 응답이 유효하다고 판단될 수 있는 최대 시간을 나타낸다. 해당 시간보다 나이를 많이 먹고 나면 만료된 HTTP 응답으로 판단된다. 그 시간은 요청 시각에 상대적이며, Expires 헤더가 설정되어 있어도 그것보다 우선시 된다.
s-maxage=<seconds> 해당 HTTP 응답이 유효하다고 판단될 수 있는 최대 시간을 나타낸다. max-age 디렉티브나 Expires 헤더가 설정되어 있어도 그것들보다 우선시 된다. 단, 공유 캐시에만 적용되기 때문에 사설 캐시에서는 무시된다. 이 디렉티브를 사용하면 암시적으로 proxy-revalidate 디렉티브도 사용이 된다.
※ Django의 never_cache() 데코레이터가 응답에 추가하는 헤더
- Expires : 현재 시각
- Cache-Control : max-age=0, no-cache, no-store, must-revalidate

 

4. 유효성 (Freshness), 검증 (Validation)

캐시에 저장되는 각각의 HTTP 응답은 수명(Lifetime)을 가지고 있다. 혹은 유효 기간이라고도 한다. 각 HTTP 응답의 수명은 다음과 같은 순서의 로직에 의해 결정이 된다. 기본적으로 1번과 2번이 공통이며, 3번은 브라우저마다 알고리즘이 조금씩 다르다.

 

  1. Cache-Control 헤더의 max-age=N 디렉티브가 존재한다면, 수명은 N과 같다.
  2. Expires 헤더가 존재한다면, 수명은 Expires 헤더의 값에서 Date 헤더의 값을 뺀 것과 같다.
  3. Last-Modified 헤더가 존재한다면, 수명은 Date 헤더의 값에서 Last-Modified 헤더의 값을 뺀 것을 10으로 나눈 것과 같다. (휴리스틱 알고리즘)

 

만약 수명이 아직 다하지 않았다면 해당 HTTP 응답은 유효하다(Fresh)고 표현하며, 수명이 다했다면 만료되었다(Stale)고 표현한다. 기본적으로 캐시는 만료된 자원에 대한 HTTP 요청을 받으면 그 자원이 여전히 유효한지 검증하기 위해 서버에게 해당 HTTP 요청을 전달한다. 이러한 과정을 검증(Validation)이라고 한다. 이때 검증하고자 하는 HTTP 응답의 헤더 구성에 따라 검증 요청 시 서버에 전달하는 정보의 종류가 조금 달라진다. 다음 그림은 검증 요청이 일어나기까지의 과정을 보여준다.

 

출처 : MDN

 

검증 방식은 크게 두 가지이다. 먼저, 검증하고자 하는 HTTP 응답에 ETag 헤더가 존재한다면 ETag 헤더의 값을 If-None-Match 헤더에 포함시켜서 서버에게 검증 요청을 보낸다. 다음으로, ETag 헤더가 존재하지 않는다면 Last-Modified 헤더를 활용한다. 즉, Last-Modified 헤더의 값을 If-Modified-Since 헤더에 포함시켜서 서버에게 검증 요청을 보낸다.

 

이제 서버는 전달받은 ETag 값 혹은 Last-Modified 값을 이용하여 자원의 유효성을 검증한다. 만약 여전히 해당 자원이 유효하다면 서버는 Body가 없는 가벼운 304 (Not Modified) 응답을 반환하고, 더 이상 유효하지 않다면 새로운 자원의 내용을 Body에 담아서 200 응답을 반환한다. 전자의 경우에는 기존에 캐싱되어 있던 자원을 다시 사용하게 하는 것이므로 나이(Age)를 0으로 초기화시키게 되고, 필요한 경우에는 만료 시각을 갱신해줄 수도 있다. 지금까지 설명한 두 가지의 검증 방식을 그림으로 나타내면 다음과 같다.

 

출처 : MDN

 

5. CDN (Content Delivery Network)

HTTP 캐싱을 다루는 김에 CDN도 간단히 한 번 알아보도록 하자. CDN은 Content Delivery Network의 약자로, 풀어서 얘기하자면 자원을 공급해주는 네트워크라는 뜻이다. 이번 포스팅에서 공부한 내용을 바탕으로 조금 더 이해하기 쉽게 바꿔보자면, 여러 사용자들이 자원을 가져다 쓸 수 있는 일종의 공유(Shared) 캐시라고 볼 수 있다. 즉, 여러 사용자들에 의해 재활용될 수 있는 자원들이 저장되는 곳이다.

 

 

그런데 여기서 핵심은, CDN을 구성하는 공유 캐시 서버가 여러 지역에 분산되어 있으며, 그렇게 분산되어 있는 공유 캐시 서버들이 자원을 가져오는 실제 서버는 따로 있다는 점이다(위 그림 참고). 이것이 중요한 이유는, 자원을 요청하는 사용자의 위치에 따라 더 가까운 공유 캐시 서버로부터 자원을 가져올 수 있게 하기 때문이다. 이는 GSLB(Global Server Load Balancing)라고 하는 발전된 형태의 DNS 기술에 의해 가능한 것인데, 간단히 요약하자면 자원을 요청하는 사용자와 가장 가까운 공유 캐시 서버의 IP 주소를 알려주도록 DNS 서버가 구현되어 있다는 것이다.

 

예를 들어, 자원이 위치한 실제 서버가 미국에 있다고 할 때, 공유 캐시 서버가 따로 없다면 사용자들은 그 자원을 얻기 위해 미국 서버까지 가서 자원을 가져와야 한다. 하지만 공유 캐시 서버가 한국에도 있고, 최초의 누군가가 이미 그 자원을 요청하여 한국 공유 캐시 서버에 그 자원을 캐싱해 두었다면, 이후의 한국 사용자들은 그 자원을 한국 서버에서만 가져오면 된다. 이러한 원리로 네트워크 트래픽을 최소화시키고 자원을 가져오는 성능을 향상하는 기술이 바로 CDN이다.

 

 

 

 

 

 

본 글은 아래 링크의 내용을 참고하여 학습한 내용을 나름대로 정리한 글임을 밝힙니다.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control

https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.2