웹 캐시는 자주 쓰이는 문서의 사본을 자동으로 보관하는 HTTP 장치이다.
웹 요청이 캐시에 도착했을 때, 캐시된 로컬 사본이 존재한다면 문서는 원 서버가 아닌 그 캐시로부터 제공된다.
캐시의 장점
- 불필요한 데이터 전송을 줄여 네트워크 비용을 줄일 수 있다.
- 네트워크 병목을 줄여준다.
- 원서버에 대한 요청을 줄여준다.
- 캐시는 물리적 거리로 인한 페이지 요청 지연 시간을 줄여준다.
불필요한 데이터 전송
복수의 클라이언트가 자주 쓰이는 원 서버 페이지에 접근 시, 서버가 같은 문서를 클라이언트에게 각각 한 번씩 전송한다면?
똑같은 바이트들이 네트워크를 통해 계속 반복해서 이동하게 된다.
캐시를 이용하면, 첫 번째 서버 응답은 캐시에 보관되어 중복해서 트래픽을 주고 받는 낭비를 줄일 수 있다.
대역폭 병목
캐시는 네트워크 병목을 줄여준다. 많은 네트워크가 원격 서버보다 로컬 네트워크 클라이언트에 더 넓은 대역폭을 제공한다.
때문에, 클라이언트들이 서버에 접근할 때의 속도는 그 경로에 있는 가장 느린 네트워크의 속도와 같다.
만약 클라이언트가 빠른 LAN에 있는 캐시로부터 사본을 가져올 수 있다면, 속도와 관련된 성능을 개선할 수 있게 된다.
갑작스러운 요청 쇄도(Flash Crowds)
캐싱은 갑작스러운 요청 쇄도에 대처할 수 있는 방법이다.
트래픽이 갑작스럽게 증가하게 되면 네트워크와 웹 서버의 심각한 장애를 야기시킨다.
거리로 인한 지연
모든 네트워크 라우터는 네트워크 트래픽을 지연시키게 된다. 라우터가 많지 않더라고 하더라도, 광속 그 자체가 유의미한 지연을 유발하게 된다.
적중과 부적중
캐시를 유용하지만, 캐시가 세상 모든 문서의 사본을 저장할 수는 없다.
캐시에 요청이 도착했을 때, 해당 요청에 대응될 수 있는 사본이 존재한다면 요청이 처리될 수 있으며 캐시 적중(cache hit)이라고 부른다.
대응될 수 없다면 캐시 부적중(cache miss)라고 부른다.
1) 재검사(Revalidation)
원 서버 컨텐츠는 변경될 수 있으므로, 캐시는 반드시 그들이 갖고 있는 사본이 최신인지를 검증해야 한다. 이러한 '신선도 검사'를 HTTP 재검사라고 부른다. HTTP는 콘텐츠가 여전히 신선한지 빠르게 검사할 수 있는 요청을 정의했다.
캐시는 원한다면 사본을 재검사할 수 있지만 네트워크 대역폭 부족문제로 인해 아래와 같은 조건의 경우을 만족한 경우에만 재검사를 한다.
- 클라이언트가 사본을 요청하였고
- 그 사본이 검사할 필요가 있을 정도로 충분히 오래된 경우
캐시는 사본의 재검사가 필요할 때, 원 서버에 작은 재검사 요청을 보낸다.
원서버의 컨텐츠가 변경되지 않은 경우, 304 Not Modified 응답을 보낸다. 304 응답을 받은 캐시는 해당 사본을 클라이언트에 제공한다. 이러한 요청-응답을 재검사 적중(혹은 느린 적중)이라고 부른다.
속도 순서
순수 캐시 적중 > 재검사 적중 > 실패한 재검사 = 캐시 부적중
HTTP는 캐시된 객체를 재확인하기 위한 몇 가지 도구를 제공하는데, 그중 많이 쓰이는 것은 `If-Modified-Since` 헤더다.
이는 서버에게 보내는 Get 요청에 이 헤더를 추가하면 캐시된 시간 이후에 변경된 경우에만 사본을 보내달라는 의미가 된다.
GET If-Modified-Since 요청이 서버에 도착한 경우 일어날 수 있는 3가지 상황
1. 재검사 적중(서버 콘텐츠가 변경되지 않은 경우)
만약 서버 객체가 변경되지 않았다면, 서버는 클라이언트에게 작은 HTTP 304 Not Modified 응답을 보낸다.
2. 재검사 부적중(서버 콘텐츠가 변경된 경우)
만약 서버 객체가 캐시된 사본가 다른 경우, 서버는 콘텐츠 전체와 함께 평범한 HTTP 200 OK 응답을 클라이언트에게 보낸다.
3. 객체 삭제(객체가 삭제된 경우)
만약 서버 객체가 삭제되었다면, 서버는 404 Not Found 응답을 돌려보내며, 캐시는 사본을 삭제한다.
2) 적중률
캐시가 요청을 처리하는 비율을 캐시 적중률이라고 부른다.
캐시 적중률은 얼마나 많은 웹 트랜잭션을 외부로 내보내지 않았는지 보여준다. 트랜잭션은 고정된 소요시간(서버로의 TCP 커넥션을 맺는 등)을 포함하게 되므로, 문서 적중률을 개선하면 전체 대기시간(지연)이 줄어든다.
오늘날 적중률 40%면 웹 캐시로 괜찮은 편이다.
3) 바이트 적중률
문서들이 모두 같은 크기가 아니므로 문서 적중률이 모든 성능을 대변하지 않는다.
몇몇 큰 객체는 덜 접근되지만 그 크기 때문에, 전체 트래픽에는 더 크게 기여한다.
바이트 단위 적중률은 캐시를 통해 제공된 모든 바이트의 비율을 표현한다. 이 측정값은 트래픽이 절감된 정도를 포착해낸다.
바이트 단위 적중률 100%는 모든 바이트가 캐시에서 왔으며, 어떤 트래픽도 인터넷으로 나가지 않았음을 의미한다.
바이트 단위 적중률은 얼마나 많은 바이트가 인터넷으로 나가지 않았는지 보여주며, 바이트 단위 적중률을 개선하면 대역폭 절약을 최적화한다.
4) 적중과 부적중의 구별
HTTP는 클라이언트에게 응답이 캐시 적중이었는지 아니면 원 서버 접근인지 말해줄 수 있는 방법을 제공하지 않는다. 두 경우 모두 응답 코드는 응답이 본문을 갖고 있음을 의미하는 200 OK가 될 것이다. 어떤 상용 프락시 캐시는 캐시에 무슨 일이 있어났는지 설명하기 위해 Via 헤더에 추가 정보를 붙인다.
클라이언트가 응답이 캐시에서 왔는지 확인하기 위한 방법
- `Date` 헤더 이용
응답의 Date 헤더 값을 현재 시각과 비교하여 응답의 생성일이 더 오래되었다면 클라이언트는 캐시된 것이다.
- `Via` 헤더 이용
캐시 토폴로지
캐시는 한 명의 사용자에게만 할당(private cache)될 수도 있고, 수천 명의 사용자들 간에 공유(public cache)될 수도 있다.
1) 개인 전용 캐시
개인 전용 캐시는 많은 에너지나 저장공간을 필요로 하지 않으므로, 작고 저렴할 수 있다. 웹 브라우저는 개인 전용 캐시를 내장하고 있다.
대부분 브라우저는 자주 쓰이는 문서를 PC의 디스크와 메모리에 캐시해 놓고, 사용자가 캐시 사이즈와 설정을 수정할 수 있도록 허용한다.
2) 공용 프락시 캐시
공용 캐시는 캐시 프락시 서버(프락시 캐시)라고 불리는 특별한 종류의 공유된 프락시 서버다.
프락시 캐시는 로컬 캐시에서 문서를 제공하거나, 사용자의 입장에서 서버에 접근한다.
공용 캐시에는 여러 사용자가 접근하기 때문에, 불필요한 트래픽을 줄일 수 있는 더 많은 기회가 있다.
클라이언트들은 개인 전용 캐시에 들어있지 않은 문서에 제각각 접근한다. 각 개인 전용 캐시는 같은 문서를 네트워크를 거쳐 여러 번 가져온다.
공유된 공용 캐시에서 캐시는 자주 찾는 객체를 단 한 번만 가져와 모든 요청에 대해 공유된 사본을 제공함으로써 네트워크 트래픽을 줄인다.
수동 프락시를 지정하거나, 프락시 자동설정 파일을 설정함으로써, 브라우저가 프락시 캐시를 사용하도록 설정할 수 있다.
3) 프락시 캐시 계층들
작은 캐시에서 캐시 부적중이 발생했을 때, 더 큰 부모 캐시가 걸러 남겨진 트래픽을 처리하도록 하는 계층을 만드는 방식이 합리적인 경우가 많다.
이 아이디어는 클라이언트 주위에는 작고 저렴한 캐시를 사용하고, 계층 상단에는 많은 사용자들에 의해 공유되는 문서를 유지하기 위해 더 크고 강력한 캐시를 사용하자는 것이다.
캐시 계층이 깊다면 캐시의 긴 연쇄를 따라가게 되고, 프락시 연쇄가 길어질 수록 각 중간 프락시는 현저한 성능 저하가 발생할 것이다.
캐시 프락시 계층이 깊어지면 성능 저하가 발생하는 이유는 각 중간 프락시가 요청을 처리할 때 발생하는 추가적인 지연과 리소스 소비 때문이다. 자세한 원인들은 다음과 같다.
1. 요청 전달 지연: 요청이 각 계층을 거칠 때마다 프락시는 요청을 받아들이고, 적절한 대상에게 전달해야 한다. 각 프락시 계층이 요청을 처리하는 데 소요되는 시간이 누적되면서 전체 지연 시간이 증가한다.
2. 캐시 미스 발생 가능성: 여러 프락시 계층을 거칠 때, 각 프락시가 자신의 캐시를 먼저 확인한다. 만약 요청한 데이터가 해당 프락시에 캐시되어 있지 않다면 다음 계층으로 전달되는데, 이런 캐시 미스가 여러 번 발생하면 실제 데이터 소스를 요청하는 데까지 시간이 길어진다.
3. 리소스 사용 증가: 각 프락시가 요청을 처리하고, 캐시를 확인하고, 응답을 반환하는 과정에서 CPU, 메모리 등의 리소스를 사용한다. 프락시 계층이 많아질수록 이 리소스 사용이 누적되며 성능에 부정적인 영향을 미친다.
4. 네트워크 오버헤드: 프락시 계층이 깊어질수록 네트워크에서 발생하는 오버헤드도 증가한다. 프락시 사이에서 데이터가 전송될 때 발생하는 지연과 패킷 처리 시간이 전체 성능을 저하시키는 요인으로 작용한다.
결국, 캐시 프락시 계층이 깊어질수록 각 계층에서 발생하는 지연과 리소스 소모가 누적되어 성능 저하가 발생하는 것이다.
4) 캐시망, 콘텐츠 라우팅, 피어링
몇몇 네트워크 아키텍처는 단순한 캐시 계층 대신 복잡한 캐시망을 만든다. 캐시망의 프락시 캐시는 복잡한 방법으로 서로 대화하여, 어떤 부모 캐시와 대화할 것인지, 캐시를 우회하여 원서버로 바로 갈 것인지에 대한 캐시 커뮤니케이션 결정을 동적으로 내린다.
캐시망 내의 콘텐츠 라우팅을 위해 설계된 캐시들은 다음에 나열된 일들을 모두 할 수 있다.
- URL에 근거하여 부모 캐시와 원 서버 중 하나를 동적으로 선택
- URL에 근거하여 특정 부모 캐시를 동적으로 설정
- 부모 캐시에 가기전 캐시된 사본을 로컬에서 탐색
- 다른 캐시들이 그들의 캐시된 콘텐츠에 부분적으로 접근할 수 있도록 허용하되, 그들의 캐시를 통한 인터넷 트랜짓(Internet Transit)은 허용하지 않음.
선택적인 피어링을 지원하는 캐시는 형제 캐시라고 부른다.
캐시 처리 단계
HTTP GET 메시지를 처리하는 기본적인 웹 캐시의 기본적인 동작
- 요청 받기
캐시는 네트워크로부터 도착한 요청 메시지를 읽는다.
고성능 캐시는 여러 개의 커넥션들로부터 데이터를 동시에 읽고, 메시지 전체가 도착하기 전에 트랜잭션 처리를 시작한다.
- 파싱
캐시는 요청 메시지를 파싱하여 URL과 헤더들을 추출하여 조작하기 쉬운 자료구조에 담는다.
이 자료구조를 통해 캐싱 소프트웨어가 헤더 필드를 처리하고 조작하기 쉽게 만들어준다.
- 검색
캐시는 로컬 복사본이 있는지 검사하고, 사본이 없다면 원 서버나 부모 프락시에서 사본을 받아와 로컬에 저장한다.
캐시된 객체는 서버 응답 본문과 원 서버 응답 헤더를 포함하고 있으므로, 캐시 적중 동안 올바른 서버 헤더가 반환될 수 있다. 캐시된 객체는 또한 얼마나 오랫동안 캐시에 머무르고 있었는지를 알려주는 기록이나 얼마나 자주 사용되었는지 등에 대한 몇몇 메타데이터를 포함한다.
- 신선도 검사
캐시는 캐시된 사본이 충분히 신선한지 검사하고, 신선하지 않다면 변경사항이 있는지 서버에게 물어본다.
HTTP는 캐시가 일정 기간 동안 서버 문서의 사본을 보유할 수 있도록 해준다. 이 기간 동안, 문서는 '신선'한 것으로 간주되고 캐시는 서버와의 접촉없이 문서를 제공할 수 있다.
- 응답 생성
캐시는 새로운 헤더와 캐시된 본문으로 응답 메시지를 만든다.
캐시된 응답을 원 서버에서 온 것처럼 보이게 하고 싶으므로, 캐시는 캐시된 서버 응답 헤더를 토대로 응답 헤더를 생성한다.
캐시는 클라이언트에 맞게 헤더를 조정해야 하는 책임이 있다.
- 클라이언트가 HTTP/1.1 응답을 기대하는 상황에서 서버가 HTTP/1.0을 반환했다면, 캐시는 헤더를 반드시 헤더를 적절하게 번역해야 한다.
캐시는 신선도 정보를 삽입하며(Cache-Control, Age, Expires 헤더), 요청이 프락시 캐시를 거쳐갔음을 알려주기 위해 종종 Via 헤더를 포함시킨다.
⚠️ 캐시가 Date 헤더를 조정해서는 안된다 (Date 헤더는 응답 객체가 원 서버에서 최초로 생겨난 일시를 표현하는 것이다)
- 발송
캐시는 네트워크를 통해 응답을 클라이언트에게 돌려준다.
모든 프락시 서버들과 마찬가지로, 프락시 캐시는 클라이언트와의 커넥션을 유지할 필요가 있다.
- 로깅(선택적)
캐시는 로그파일에 트랜잭션에 대해 서술한 로그 하나를 남긴다.
캐시 처리 플로 차트
사본을 신선하게 유지하기
캐시된 사본이 서버의 문서와 항상 일치하는 것이 아니므로, 캐시된 데이터는 서버의 데이터와 일치하도록 관리되어야 한다.
HTTP는 어떤 캐시가 사본을 갖고 있는지 서버가 기억하지 않더라도, 캐시된 사본이 서버와 충분히 일치하도록 유지할 수 있는 메커니즘을 갖고 있으며, 이를 문서 만료와 서버 재검사라고 부른다.
1) 문서 만료
HTTP는 Cache-Control과 Expires라는 헤더를 사용하여 원 서버가 각 문서에 유효기간을 붙일 수 있도록 해준다.
캐시 문서가 만료되기 전에 캐시는 필요하다면 서버와의 접촉 없이 사본을 제공할 수 있다.
캐시 문서가 만료되면, 캐시는 서버와 문서에 변경된 것이 있는지 검사해야 하며, 변경되었다면 신선한 사본을 얻어와야 한다. (새 유효기간 포함)
2) 유효기간과 나이
서버는 응답 본문과 함께 하는 `HTTP/1.0+ Expires`나 `HTTP/1.1 Cache-Control:max-age` 응답 헤더를 이용해서 유효기간을 명시한다.
헤더 |
설명 |
Cache-Control:max-age |
max-age 값은 문서의 최대 나이를 정의한다. 최대 나이는 문서가 처음 생성된 이후부터 제공하기엔 더 이상 신선하지 않다고 간주될 때까지의 경과한 시간의 합법적인 최댓값(초 단위)이다.
Cache-Control: max-age=484200 |
Expires |
절대 유효기간을 명시한다 만약 유효기간이 경과했다면, 그 문서는 더이상 신선하지 않다.
Expires: Fri, 05 Jul 2002, 05:00:00 GMT |
3) 서버 재검사
캐시된 문서가 만료되었다는 것이 사본과 원본이 다르다는 것은 아니다. 검사할 시간이 되었다는 것이다.
이 검사를 캐시가 원 서버에게 문서가 변경되었는지 여부를 물어볼 필요가 있음을 의미하는 '서버 재검사'라고 부른다.
- 재검사 결과 콘텐츠가 변경되었다면, 캐시는 문서의 새로운 사본을 가져와 오래된 데이터 대신 저장한 뒤 클라이언트에게도 보내준다.
- 재검사 결과 콘텐츠가 변경되지 않았다면, 캐시는 새 만료일을 포함한 새 헤더들만 가져와서 캐시 안의 헤더들을 갱신한다.
HTTP 프로토콜은 캐시가 다음 중 하나를 반환하는 적절한 행동을 할 것을 요구한다.
- '충분히 신선한' 캐시 사본
- 원 서버와 재검사되었기 때문에 충분히 신선하다고 확신할 수 있는 캐시된 사본
- 에러 메시지(원 서버가 다운된 경우)
- 경고 메시지가 부착된 캐시된 사본(부정확한 경우)
4) 조건부 메서드와의 재검사
HTTP의 조건부 메서드는 재검사를 효율적으로 만들어준다.
HTTP는 캐시가 서버에게 '조건부 GET'이라는 요청을 보낼 수 있도록 해준다. 이 요청은 서버가 갖고 있는 문서가 캐시가 갖고 있는 것과 다른 경우에만 객체 본문을 보내달라고 하는 것이다.
조건부 GET은 GET 요청 메시지에 특별한 조건부 헤더를 추가함으로써 시작된다. (서버는 조건이 참일 때만 객체를 반환한다)
모든 조건부 헤더는 `If-` 접두어로 시작한다.
캐시 재검사를 위해 사용되는 조건부 응답 헤더들
1. If-Modified-Since: 날짜 재검사
IMS 요청이라고 불리며, 만약 문서가 주어진 날짜 이후로 수정되었다면 요청 메서드를 처리한다.
[처리 방식]
- 문서가 주어진 날짜 이후에 변경된 경우
조건은 참이고, GET 요청은 평범하게 성공한다. 새 문서, 새로운 만료 날짜, 그 외 다른 정보들이 담긴 헤더들과 함께 캐시에 반환된다.
- 문서가 주어진 날짜 이후에 변경되지 않은 경우
조건은 거짓이고 서버는 작은 304 Not Modified 응답 메시지를 보낸다 (본문은 효율을 위해 보내지 않는다)
캐시된 버전으로 콘텐츠가 변경된 경우에만 콘텐츠를 가져오기 위해 `Last-Modified `서버 응답 헤더와 함께 사용된다. 원 서버는 제공하는 문서에 최근 변경 일시를 붙인다. 캐시가 캐시된 문서를 재검사 하려고 할 때, 캐시된 사본이 마지막으로 수정된 날짜가 담긴 `If-Modified-Since` 헤더를 포함한다.
몇몇 웹 서버는 `If-Modified-Since`를 실제 날짜 비교로 구현하지 않는다. 대신 그들은 IMS 날짜와 최근 변경일 간의 문자열 비교를 수행한다. ➡️ "이 날짜 이후로 변경되었다면"이 아닌 "정확히 이 날짜에 마지막 변경이 일어난 것이 아니라면"이라는 의미로 동작한다.
2. If-None-Match: 엔티티 태그 재검사
최근 변경 일시 재검사가 적절히 행해지기 어려운 상황이 있다.
퍼블리셔가 문서를 변경했을 때, 문서의 엔티티 태그를 새로운 버전으로 표현할 수 있다. 엔티티 태그가 변경되었다면 캐시는 새 문서의 사본을 얻기 위해 `If-None-Match` 조건부 헤더를 사용할 수 있다.
[예시]
캐시는 엔티티 태그 'v2.6'인 문서를 갖고 있다. 캐시는 원 서버에게 태그가 더 이상 'v2.6'이 아닌 경우에만 새 객체를 달라고 요청하는 방법으로 유효한지 여부를 재검사한다. 예시에서는 태그가 여전히 변경되지 않았기 때문에 304 Modified 응답이 반환된다.
캐시가 객체에 대한 여러 개의 사본을 갖고 있는 경우, 그 사실을 서버에 알리기 위해 하나의 If-None-Match 헤더에 여러 개의 엔티티 태그를 포함시킬 수 있다.
If-None-Match: "v2.6"
If-None-Match: "v2.4", "v2.5", "v2.6"
If-None-Match: "foobar", "A34FAC0095", "Profiles in Courage"
5) 약한 검사기와 강한 검사기
캐시는 캐시된 버전이 서버가 갖고 있는 것에 대해 최신인지 확인하기 위해 엔티티 태그를 사용한다.
서버는 때때로 모든 캐시된 사본을 무효화시키지 않고 문서를 살짝 고칠 수 있도록 허용하고 싶은 경우가 있다.
HTTP/1.1은 비록 콘텐츠가 조금 변경되었더라도 "그 정도면 같은 것"이라고 서버가 주장할 수 있도록 해주는 '약한 검사기'를 지원한다.
강한 검사기는 콘텐츠가 바뀔 때마다 바뀐다.
약한 검사기는 어느정도 콘텐츠 변경을 허용하지만, 콘텐츠의 중요한 의미가 변경되면 함께 변경된다.
서버는 'W/' 접두사로 약한 검사기를 구분한다.
ETag: W/"v2.6"
If-None-Match: W/"v2.6"
ETag는 서버가 특정 리소스에 대해 생성한 고유 식별자다.
ETag는 리소스의 상태나 콘텐츠가 변경되면 함께 변경되며, 클라이언트가 요청 시 사용하여 서버의 리소스가 변경되었는지 확인할 수 있다.
6) 언제 엔티티 태그를 사용하고 언제 Last-Modified 일시를 사용하는가
HTTP/1.1 클라이언트는 만약 서버가 엔티티 태그를 반환했다면, 반드시 엔티티 태그 검사기를 사용해야 한다.
만약 Last-Modified(최근 변경일시) 값만을 반환한 경우, 클라이언트는 If-Modified-Since 검사를 사용할 수 있다.
만약 엔터티 태그와 최근 변경일시가 모두 사용 가능하다면, 모두 적절히 응답할 수 있도록 클라이언트는 각각을 위해 두 가지 잭머사 정책을 모두 사용해야 한다.
캐시 제어
HTTP는 문서가 만료되기 전까지 얼마나 오랫동안 캐시될 수 있게 할 것인지 서버가 설정할 수 있는 여러 가지 방법을 정의한다.
- Cache-Control: no-store 헤더
- Cache-Control: no-cache 헤더
- Cache-Control: must-revalidate 헤더
- Cache-Control: max-age 헤더
- Expires 날짜 헤더
- 아무 만료 정보도 주지 않고 캐시가 휴리스틱 방법으로 결정하게 할 수 있다.
1) no-cache, no-store 응답 헤더
HTTP/1.1은 신선도를 관리하기 위해, 객체를 캐시하는 것을 제한하거나 캐시된 객체를 제공하는 여러가지 방법을 제공한다.
`no-cache`, `no-store` 헤더는 캐시가 검증되지 않은 캐시된 객체로 응답하는 것을 막는다.
- no-cache
응답은 로컬 캐시 저장소에 저장될 수 있다. 다만 먼저 서버와 재검사를 하지 않고서는 캐시에서 클라이언트로 제공될 수 없을 뿐이다.
이 헤더의 더 나은 이름은 Do-Not-Serve-From-Cache-Without-Revalidation(재검사 없이 캐시에서 제공하지 마라)일 것이다.
서버의 리소스가 자주 변경될 가능성이 있지만, 캐시가 빠르게 확인하고 최신 버전이라면 재사용할 수 있도록 하고 싶을 때 사용된다. 자주 변경되지만, 요청마다 새 데이터를 반드시 받을 필요는 없는 경우에 유용하다.
- no-store
캐시가 그 응답의 사본을 만드는 것을 금지한다. 캐시는 클라이언트에게 `no-store` 응답을 전달하고 나면 객체를 삭제한다.
매우 민감한 데이터(예: 금융 정보, 개인 데이터)를 다룰 때 사용되며, 캐시에 저장되지 않기 때문에 안전성이 요구되는 환경에서 자주 사용된다. 또한, 최신 데이터가 항상 필요한 경우에도 사용될 수 있다.
2) Max-Age 응답 헤더
Cache-Control: max-age 헤더는 신선하다고 간주되었던 문서가 서버로부터 온 이후로 흐른 시간이고, 초로 나타낸다.
s-maxage는 max-age 처럼 행동하지만 공유된(공용) 캐시에만 적용된다.
Cache-Control: max-age=3600
Cache-Control: s-maxage=3600
서버는 maximum aging을 0으로 설정하여, 캐시가 매 접근마다 문서를 캐시하거나 리프레시하지 않도록 요청할 수 있다.
Cache-Control: max-age=0
3) Expires 응답 헤더
deprecated 예정인 Expires 헤더는 초 단위 시간 대신 실제 만료 날짜를 명시한다.
4) Must-Revalidate 응답 헤더
캐시는 성능을 개선하기 위해 만료된 객체를 제공하도록 설정될 수 있다.
만약 캐시가 만료정보를 엄격하게 따르기를 원한다면, `Cache-Control: must-revalidate` 헤더를 붙일 수 있다.
`Cache-Control: must-revalidate` 헤더는 캐시가 이 객체의 신선하지 않은 사본을 원 서버와의 최초의 재검사 없이는 제공해서는 안 됨을 의미한다. 캐시가 must-revalidate 신선도 검사를 시도했을 때, 원 서버가 사용할 수 없는 상태라면 캐시는 반드시 504 Gateway Timeout error를 반환해야 한다.
5) 휴리스틱 만료
응답이 Cache-Control: max-age 헤더나 Expires 헤더 중 어느 것도 포함하고 있지 않다면?
캐시는 휴리스틱 방법으로 최대 나이를 계산하게 된다. 계산 결과로 얻은 최대 나이 값이 24 시간보다 크다면 Heuristic Expiration 경고 헤더가 응답 헤더에 추가되어야 한다.
일반적으로, 사람들은 휴리스틱 신선도 유지기간에 상한을 설정하여 지나치게 커지는 것을 막는다. 보통 1주일로 하지만 보수적인 사이트는 하루로 설정한다. 많은 원 서버들이 Expires와 max-age 헤더를 생성하지 않기 때문에 캐시 만료값을 신중하게 선택해야 한다.
6) 클라이언트 신선도 제약
웹 브라우저는 브라우저나 프락시 캐시의 신선하지 않은 콘텐츠를 강제로 갱신시켜주는 리프레시나 리로드 버튼을 갖고 있다.
리프레시 버튼은 Cache-Control 요청 헤더가 추가된 GET 요청을 발생시켜 강제로 재검사하거나 서버로부터 콘텐츠를 무조건 가져온다.
클라이언트는 Cache-Control 요청 헤더를 사용하여 만료 제약을 엄격하게 하거나 느슨하게 할 수 있다. 클라이언트는 문서를 최신으로 유지할 필요가 있는 애플리케이션(수동 리프레시 버튼)을 위해 Cache-Control 헤더를 사용해 만료를 더 엄격하게 할 수 있다.
Cache-Control 요청 지시어
지시어 |
목적 |
Cache-Control: max-stale Cache-Control: max-stale = <s> |
캐시는 신선하지 않은 문서라도 자유롭게 제공할 수 있다. 만약 <s> 매개변수가 지정되면, 클라이언트는 만료 시간이 그 매개변수의 값만큼 지난 문서도 받아들인다. 이것은 캐싱 규칙을 느슨하게 한다. |
Cache-Control: min-fresh = <s> |
클라이언트는 지금으로부터 적어도 <s>초 후까지 신선한 문서만을 받아들인다. 이것은 캐싱 규칙을 엄격하게 한다. |
Cache-Control: max-age = <s> |
캐시는 <s>초보다 오랫동안 캐시된 문서를 반환할 수 없다. 나이가 유효기간을 넘어서게 되는 max-stale 지시어가 함께 설정되지 않는 이상, 이 지시어는 캐싱 규칙을 더 엄격하게 만든다. |
Cache-Control: no-cache Pragma: no-cache |
이 클라이언트는 캐시된 리소스가 재검사하기 전에는 받아들이지 않는다. |
Cache-Control: no-store |
이 캐시는 저장소에서 문서의 흔적을 최대한 빨리 삭제해야 한다. 문서에는 민감한 정보가 포함되어 있기 때문이다. |
Cache-Control: only-if-cached |
클라이언트는 캐시에 들어있는 사본만을 원한다. |
예시) max-stale과 max-age를 함께 쓰는 경우
Cache-Control: max-age=3600, max-stale=1800
7) 주의할 점
문서 완료는 완벽한 시스템이 아니다.
퍼블리셔가 잘못해서 유효기간을 먼 미래로 설정한 경우, 만료되기 전까지는 그 문서에 대한 어떤 변경도 캐시에 반영되지 않게 된다.
이러한 이슈로 인해 유효기간을 사용조차 하지 않는 경우도 많다.
조건부 요청(Conditional Request)과 Cache-Control을 함께 사용하는 경우가 많다. 둘을 결합하면 효율적인 캐싱과 더불어 서버에서 최신 데이터를 확인하는 요청을 할 수 있어 성능 최적화와 데이터 정확성을 모두 달성할 수 있다.
조건부 요청(Conditional Request)
조건부 요청은 클라이언트가 리소스를 요청할 때 서버에 해당 리소스가 변경되었는지 확인하도록 하는 방식이다. 캐시된 리소스가 여전히 유효한지 판단하기 위해 조건부 요청을 보낸다.
두 가지 주요 헤더가 있다
1) ETag(Entity Tag)
서버는 리소스의 특정 버전을 나타내는 ETag 값을 응답 헤더에 추가할 수 있다. 이후 클라이언트는 이 ETag 값을 저장하고, 리소스가 다시 요청될 때 If-None-Match 헤더에 ETag 값을 포함해 요청을 보낸다. 서버는 클라이언트가 보낸 ETag 값과 비교하여 리소스가 변경되지 않았으면 304 Not Modified 응답을 보내고, 클라이언트는 캐시된 리소스를 그대로 사용한다.
2) Last-Modified
서버는 리소스가 마지막으로 수정된 시간을 나타내는 Last-Modified 헤더를 응답에 추가할 수 있다. 클라이언트는 이를 저장하고, 이후 요청 시 If-Modified-Since 헤더에 이 시간을 포함하여 서버에 리소스가 변경되었는지 확인한다. 서버는 이 날짜 이후에 리소스가 수정되지 않았다면 304 Not Modified 응답을 보내고, 클라이언트는 캐시된 리소스를 사용한다.
Cache-Control과의 결합
Cache-Control 헤더를 사용하여 리소스의 캐싱 기간을 설정하면서, 이 기간이 끝나면 클라이언트는 조건부 요청을 통해 서버에서 리소스의 유효성을 확인하는 구조로 결합한다.
결합 예시)
서버가 Cache-Control: max-age=3600 (1시간 캐시)와 함께 ETag를 제공한다고 가정해보자.
클라이언트는 1시간 동안 캐시된 리소스를 사용하지만, 1시간이 지난 후에는 If-None-Match 헤더에 ETag 값을 포함한 조건부 요청을 보내 서버가 리소스를 업데이트했는지 확인한다. 만약 리소스가 변경되지 않았다면 304 Not Modified 응답을 받아서 네트워크 대역폭을 절약하면서 캐시된 리소스를 재사용한다. 리소스가 변경되었다면, 서버는 새로운 데이터를 보내고, 클라이언트는 새로운 리소스를 캐시에 저장한다.
결합의 장점
1) 최신 데이터 보장 - 클라이언트는 캐시된 데이터를 재사용하면서도, 필요할 때마다 조건부 요청을 통해 최신 데이터가 유효한지 확인할 수 있다.
2) 성능 최적화 - 리소스가 변경되지 않았다면 304 응답을 받아 네트워크 자원을 절약하고, 서버로부터 불필요한 데이터를 전송받지 않게 된다.
3) 효율적인 캐시 관리 - Cache-Control로 캐시 유효 기간을 설정하고, 유효 기간이 지난 이후에도 조건부 요청을 통해 캐시 재사용 여부를 결정할 수 있다.
따라서 Cache-Control과 조건부 요청은 서로 보완적이며, 이를 함께 사용하면 성능과 데이터 정확성을 모두 확보할 수 있다.
자세한 알고리즘
HTTP 명세는 문서의 나이와 캐시 신선도를 계산하는 알고리즘을 제공한다.
1) 나이와 신선도 수명
캐시된 문서가 제공하기에 충분히 신선한지 알려주려면, 캐시는 단 두 가지 값만 계산하면 된다.
만약 캐시된 사본의 나이가 신선도 수명보다 작으면 사본은 제공해주기에 충분히 신선한 것이다.
$충분히_신선한가 = ($나이 < $신선도_수명)
문서의 나이 = 서버가 문서를 보낸 후 그 문서가 '나이를 먹은' 시간의 총합
캐시는 문서 응답이 업스트림 캐시에서 왔는지 서버에서 왔는지 모를 수 있기 때문에, 문서가 완전히 새롭다고 가정하지 못한다.
`Age` 헤더를 통해 명시적으로든, 서버가 생성한 `Date` 헤더를 통해 계산하든 간에 문서의 나이를 판별해야 한다.
어떤 클라이언트는 `Cache-Control: max-stale `헤더를 사용하여 약간 신선하지 않은 문서라도 받아들이려고 할 수 있다.
다른 클라이언트는 `Cache-Control: min-fresh` 헤더를 사용하여 조만간 신선하지 않게 될 문서조차 받아들이지 않으려 할 수도 있다.
캐시는 서버 만료 정보와 클라이언트 신선도 요구사항을 조합해서 최대 신선도 수명을 판별한다.
2) 나이 계산
응답의 나이 = 응답이 서버에서 생성되었을 때부터 지금까지의 총 시간 (라우터, 게이트웨이를 거친 시간 + 응답이 캐시에 머물렀던 시간 포함)
// 캐시는 응답이 캐시에 도착했을 때, Date나 Age 헤더를 분석해서 얼마나 오래된 것인지 파악
$겉보기_나이 = max(0, $응답을_받은_시각 - $Date_헤더값);
$보정된_겉보기_나이 = max($겉보기_나이, $Age_헤더값);
// 캐시는 문서가 로컬 캐시에 얼마나 오래 머물렀는지 판단
$응답_지연_추정값 = ($응답을_받은_시각 - $요청을_보낸_시각);
$문서가_우리의_캐시에_도착했을_때의_나이 = $보정된_겉보기_나이 + $응답_지연_추정값;
$사본이_우리의_캐시에_머무른_시간 = $현재_시각 - $응답을_받은_시각;
$나이 = $문서가_우리의_캐시에_도착했을_때의_나이 + $사본이_우리의_캐시에_머무른_시간;
캐시는 캐시된 사본이 로컬에서 얼마나 오랫동안 캐시되었는지 쉽게 판단할 수 있다.
그러나 캐시에서 온 응답의 나이를 알아내는 것은 더 어렵다. (모든 서버가 동기화된 시계를 갖고 있지 않으며, 응답이 어디에서 왔는지 모르기 때문이다)
3) 신선도 수명 계산
캐시된 문서가 클라이언트에게 제공해주기에 충분히 신선한지 알아내려 시도하고 있다.
우리는 이를 파악하기 위해 캐시된 문서의 나이를 알아내고, 서버와 클라이언트의 제약조건에 따라 신선도 수명을 계산해야 한다.
신선도 수명은 서버와 클라이언트의 제약조건에 의존한다.
- 서버는 수년 동안 신선한 상태를 유지하는 문서를 가질 수도 있고, 매일 변경되는 문서를 가질 수도 있다.
- 클라이언트는 데이터를 받는 속도가 조금 더 빠르다면 약간 신선하지 못한 콘텐츠를 받아들이고자 할 때도 있고, 최신의 콘텐츠만을 받아들이고자 할 때도 있다.
캐시는 사용자를 위해 존재하므로, 그들의 요구에 충실히 따라야 한다.
4) 완전한 서버 신선도 알고리즘
sub 서버_신선도_한계 {
local($휴리스틱, $서버_신선도_한계, $마지막으로_변경된_시각)
$휴리스틱 = 0;
if ($Max_Age_값이_설정되었나) { $서버_신선도_한계 = $Max_Age_값 }
elsif ($Expires_값이설정되었나) { $서버_신선도_한계 = $Expires_값 - $Date_값 }
elsif ($Last_Modified_값이_설정되었나) {
$마지막으로_변경된_시각 = max(0, $Date_값 - $Last_Modified_값)
$서버_신선도_한계 = int($마지막으로_변경된_시각 * $lm_인자);
$휴리스틱 = 1;
} else {
$서버_신선도_한계 = $캐시_최소_수명_기본값;
$휴리스틱 = 1;
}
if ($휴리스틱) {
if ($서버_신선도_한계 > $캐시_최대_수명_기본값) $서버_신선도_한계 = $캐시_최대_수명_기본값
if ($서버_신선도_한계 < $캐시_최소_수명_기본값) $서버_신선도_한계 = $캐시_최소_수명_기본값
}
return $서버_신선도_한계
}
sub 클라이언트가_수정한_신선도_한계 {
$나이_한계 = 서버_신선도_한계()
if ($Max_Stale_값이_설정되었나) {
if ($Max_Stale_값 == $INT_MAX) { $나이_한계 = $INT_MAX }
else { $나이_한계 = 서버_신선도_한계() + $Max_Stale_값 }
}
if ($Min_Fresh_값이_설정되었나) {
$나이_한계 = min($나이_한계, 서버_신선도_한계() - $Min_Fresh_값)
}
if ($Max_Age_값이_설정되었나) { $나이_한계 = min($나이_한계, $Max_Age_값) }
}