개요
RFC 9111은 분산되고 협업적인 하이퍼텍스트 정보 시스템을 위한 상태 비저장(Stateless) 애플리케이션 계층 프로토콜인 HTTP의 핵심 구성 요소 중 하나인 캐싱 메커니즘을 정의한다. 이 문서는 HTTP 캐시의 개념적 정의와 더불어, 캐시의 동작을 정밀하게 제어하거나 특정 응답 메시지가 캐싱될 수 있음을 나타내는 다양한 헤더 필드들을 규정하고 있다.
본 명세는 기존의 HTTP 캐싱 표준이었던 RFC 7234를 폐기하고 이를 대체한다. 이는 현대 웹 생태계에서 발생하는 복잡한 통신 구조와 보안 요구사항을 반영하여, 캐시가 요청과 응답을 처리하는 방식을 더욱 명확하게 정립하기 위함이다.
HTTP 캐싱의 주된 목적은 다음과 같다.
- 네트워크 대역폭 소모 감소
- 응답 지연 시간 단축
- 원 서버의 부하 경감
RFC 9111은 이러한 목적을 달성하기 위해 캐시가 갖추어야 할 요건과 서버가 캐시에게 전달하는 지침들을 상세히 기술한다.
이 문서에서 다루는 캐싱 모델은 크게 두 가지 관점에 집중한다. 첫째는 응답이 신선한지(Fresh) 혹은 **만료되었는지(Stale)**를 판단하는 신선도 계산 방식이고, 둘째는 만료된 응답을 다시 사용하기 위해 원 서버와 통신하는 유효성 재검증 절차다. 이러한 일련의 규칙들은 전 세계의 브라우저, 프록시, CDN이 동일한 논리에 기반하여 데이터를 관리할 수 있도록 돕는 기술적 토대가 된다.
1. Introduction
HTTP는 확장 가능한 의미론과 자기 설명적인 메시지를 사용하는 상태 비저장 프로토콜이며, 분산 정보 시스템에서 성능을 향상시키기 위해 응답 캐시를 적극적으로 활용한다. 섹션 1에서는 HTTP 캐시를 응답 메시지의 로컬 저장소이자 메시지의 저장, 검색 및 삭제를 제어하는 하위 시스템으로 정의한다. 캐시의 존재 목적은 향후 발생하는 동일한 요청에 대해 응답 시간을 단축하고 네트워크 대역폭 소비를 줄이는 데 있다. 터널로 동작하는 경우를 제외하면 어떠한 클라이언트나 서버도 캐시를 사용할 수 있다.
캐시는 사용 범위에 따라 **공유 캐시(Shared Cache)**와 **개인용 캐시(Private Cache)**로 나뉜다.
- 공유 캐시: 두 명 이상의 사용자가 응답을 재사용할 수 있도록 저장하는 캐시로, 주로 CDN이나 프록시 서버와 같은 중간 장치(Intermediary)에 배치된다.
- 개인용 캐시: 단일 사용자만을 위해 할당된 캐시이며, 흔히 웹 브라우저와 같은 사용자 에이전트의 구성 요소로 구현된다.
HTTP 캐싱의 핵심 목표는 이전 응답 메시지를 재사용하여 현재 요청을 처리함으로써 시스템 성능을 유의미하게 개선하는 것이다. 캐시는 4.2. Freshness에서 정의된 기준에 따라 저장된 응답이 **신선하다(Fresh)**고 판단되면, 원 서버에 해당 응답이 여전히 유효한지 확인하는 재검증(Validation) 절차 없이 이를 즉시 재사용한다. 이렇게 신선한 응답을 활용하면 매번 발생하는 지연 시간과 네트워크 오버헤드를 획기적으로 줄일 수 있다.
저장된 응답이 더 이상 신선하지 않은 상태가 되더라도, 4.3. Validation에 정의된 재검증 과정을 통해 다시 신선하게 갱신하거나 원 서버를 사용할 수 없는 특수한 상황(Serving Stale Response)에서 제한적으로 재사용될 수 있다.
1.1 Requirements Notation
RFC 9111 명세에서 사용되는 MUST, SHOULD, MAY와 같은 핵심 단어들은 BCP 14 표준에 따라 해석된다. 이 단어들이 대문자로 표기되었을 때만 기술적 요구사항으로서의 구속력을 갖는다.
- MUST와 REQUIRED: 해당 규격을 준수하기 위해 반드시 이행해야 하는 절대적인 의무를 나타낸다. 만약 이를 준수하지 않으면 해당 구현체는 표준을 따르는 것으로 인정받지 못한다.
- SHOULD와 RECOMMENDED: 기술적으로 권장되는 사항이나 특정 환경에서 무시될 수 있는 항목을 의미한다. 하지만 이를 따르지 않을 때는 그로 인한 영향을 충분히 이해하고 신중하게 결정해야 한다.
- MAY와 OPTIONAL: 구현자가 상황에 맞춰 선택할 수 있는 허용 범위를 뜻한다.
이러한 용어들의 구체적인 정의는 RFC 2119와 RFC 8174에 근거한다. 또한 캐싱 명세는 HTTP 프로토콜의 일부이므로 전반적인 적합성 기준이나 오류 처리에 관한 원칙은 RFC 9110 섹션 2의 정의를 따른다. 이는 프로토콜을 구현하는 다양한 개발자들이 서로 같은 기준을 가지고 시스템을 설계할 수 있도록 돕는 역할을 한다.
1.2. Syntax Notation
이 명세서에서 사용되는 구문 정의 방식인 ABNF(Augmented Backus-Naur Form)와 그 확장 규칙을 기술한다. HTTP와 같은 네트워크 프로토콜은 전 세계의 다양한 시스템이 서로 통신해야 하므로, 메시지의 형식을 수학적이고 엄격하게 정의할 필요가 있다. 이때 사용하는 도구가 RFC 5234에서 정의된 ABNF다.
여기에 더해 문자열의 대소문자 구분 여부를 명확히 하기 위해 RFC 7405의 표기법을 도입했다. 예를 들어 특정 지시어가 대문자와 소문자를 동일하게 취급하는지 아니면 엄격히 구분하는지를 이 규칙에 따라 기술한다. 이는 구현체 간의 해석 차이로 인한 오류를 방지하는 중요한 기준이 된다.
또한 HTTP 명세 특유의 리스트 확장 표기법인 # 연산자를 사용한다.
RFC 9110 섹션 5.6.1에서 정의된 이 연산자는 Cache-Control: no-cache, no-store와 같이 쉼표로 구분된 리스트를 간결하게 표현하기 위해 사용된다.
표준 ABNF에서 반복을 의미하는 * 연산자와 유사하지만, 리스트 요소 사이에 쉼표와 선택적인 공백이 올 수 있음을 함축한다.
명세의 부록 A(Appendix A)에는 이러한 모든 확장 기호와 약속된 기법들을 표준 ABNF 형식으로 풀어서 정리해 두었다. 개발자는 이 부록을 참조하여 자신의 프로그램이 HTTP 헤더를 올바르게 해석(Parsing)하고 생성하는지 검증할 수 있다. 이러한 구문 표기법의 엄격한 정의는 프로토콜의 상호 운용성을 보장하는 핵심적인 장치다.
1.2.1. Imported Rules
캐시 관련 구문을 정의하기 위해 다른 RFC 명세에서 이미 확립된 규칙들을 가져와 사용하는 수입 규칙에 대해 설명한다.
이는 HTTP 프로토콜의 상호 운용성을 보장하기 위해 기존의 정의를 재사용하는 방식이다.
우선 RFC 5234에서 정의된 DIGIT 규칙을 참조하는데, 이는 0부터 9까지의 10진수 숫자를 의미하며 캐시의 수명을 초 단위로 기술할 때 기초가 된다.
또한 최신 HTTP 의미론 명세인 RFC 9110에서 정의된 다섯 가지 핵심 규칙을 가져온다.
HTTP-date: 날짜와 시간을 표현하는 표준 형식으로Expires헤더 등에 적용된다.OWS: 선택적 공백을 의미하며 헤더 내의 쉼표 전후에 올 수 있는 공백 처리를 규정한다.field-name: 헤더 필드의 이름을 의미한다.quoted-string: 큰따옴표로 감싸진 문자열을 의미한다.token: 특수 기호를 제외한 기본적인 문자 시퀀스를 의미한다.
이러한 규칙들은 캐시 시스템이 수신한 메시지를 해석할 때 각 요소가 어떤 의미와 형식을 갖는지 판단하는 엄격한 기준이 된다.
1.2.2. Delta Seconds
delta-seconds 규칙은 시간을 초 단위로 나타내는 0 이상의 정수를 의미한다.
ABNF 구문으로는 하나 이상의 숫자로 구성된 1*DIGIT으로 표현되며, 이는 주로 Cache-Control 헤더의 max-age와 같이 상대적인 시간 길이를 지정할 때 사용된다.
이 값을 해석하여 이진 형식으로 변환하는 수신측은 최소 31비트 이상의 범위를 가진 부호 없는 정수 타입을 사용해야 한다.
만약 캐시가 자신이 표현할 수 있는 최대 정수보다 큰 delta-seconds 값을 받거나, 이후 계산 과정에서 오버플로가 발생한다면 해당 값을 $2^{31}$ 또는 구현체가 편리하게 표현할 수 있는 최대 양의 정수로 간주해야 한다.
여기서 $2^{31}$이라는 특정 수치는 역사적인 이유로 선택된 것이며, 시간으로 환산하면 약 68년이 넘는 기간에 해당한다.
이는 캐싱 문맥에서 사실상 무한대와 다름없는 충분히 긴 시간으로 취급된다.
구현체가 이 숫자를 이진 형태로 직접 저장할 수 없는 산술 타입을 사용하더라도, 오버플로가 발생했을 때 이를 문자열 등으로 처리하여 결과적으로 이 수치만큼의 긴 시간으로 인식하게 만드는 것이 중요하다. 이 규칙의 핵심 의도는 계산 과정에서 오버플로가 발생했을 때 이를 음수나 작은 값으로 잘못 처리하지 않고, 매우 긴 유효 기간을 가진 리소스로 안전하게 다루는 데 있다.
2. Overview of Cache Operation
HTTP 캐시의 기본 원칙과 동작 방식을 규정한다. 캐시의 핵심 역할은 HTTP 전송의 의미론을 보존하면서도 이미 보유한 정보를 재활용하여 불필요한 데이터 전송을 줄이는 것이다. 캐싱은 HTTP의 Optional 한 기능이지만, 특별한 제약이나 설정이 없다면 응답을 재사용하는 것이 기본 동작(Default behavior)으로 간주된다. 따라서 명세의 요구사항은 캐시가 특정 응답을 반드시 저장하도록 강제하기보다는, 재사용 불가능한 응답을 저장하거나 부적절하게 재사용하는 것을 방지하는 데 초점을 맞춘다.
캐시는 저장된 응답을 선택하기 위한 식별자로 **캐시 키(Cache Key)**를 사용한다.
캐시 키는 최소한 요청 메서드(Request Method)와 대상 URI(Target URI)로 구성된다.
메서드는 해당 응답이 이후 어떤 요청을 충족할 수 있는지 결정하는 기준이 된다.
다만 오늘날 흔히 쓰이는 많은 캐시 시스템은 GET 응답만을 저장하기 때문에 URI만을 캐시 키로 사용하는 경우가 많다.
콘텐츠 협상(Content Negotiation)이 적용되는 리소스의 경우, 캐시는 동일한 URI에 대해 여러 응답을 저장할 수 있다.
이때 캐시는 4.1. Calculating Cache Keys with the Vary Header Field에 따라 Vary 응답 헤더의 정보를 활용하여 원본 요청의 특정 헤더 필드들을 캐시 키에 추가함으로써 응답들을 구분한다.
또한 사용자 에이전트는 프라이버시 위험을 방지하기 위해 참조 사이트의 신원 정보를 캐시 키에 포함하는 더블 키잉(Double Keying) 방식을 채택하기도 한다.
가장 일반적인 캐싱 대상은 GET 요청에 대한 200 (OK) 응답이다.
하지만 메서드 정의가 캐싱을 허용하고 적절한 캐시 키를 정의할 수 있다면, 리다이렉션, 404 (Not Found)와 같은 부정적 결과, 206 (Partial Content)과 같은 불완전한 결과, 그리고 GET 이외의 메서드 응답도 저장 가능하다.
마지막으로 캐시가 원 서버와 통신할 수 없거나 경로를 찾지 못하는 상태를 **연결 끊김(Disconnected)**이라고 한다. 이러한 상태에서 캐시는 4.2.4. Serving Stale Response의 규정에 따라 특정 상황에서 신선하지 않은(Stale) 응답을 클라이언트에게 제공할 수 있다.
3. Storing Responses in Caches
캐시가 서버로부터 받은 응답을 저장소에 보관하기 위해 반드시 충족해야 하는 엄격한 조건들을 정의한다. 캐시는 다음의 모든 조건이 충족되지 않는 한 응답을 저장해서는 안 된다.
- 캐시가 해당 요청 메서드를 이해하고 있어야 한다. 여기서 '이해한다'는 것은 단순히 메서드 이름을 아는 것을 넘어 해당 메서드와 관련된 모든 캐싱 동작을 구현했음을 의미한다.
- 응답 상태 코드가 **최종적(Final)**이어야 한다.
1xx계열의 정보 응답은 저장 대상이 아니다. 206 (Partial Content)이나304 (Not Modified)처럼 복잡한 처리가 필요한 상태 코드이거나 must-understand 지시어가 있는 경우, 캐시는 해당 상태 코드의 의미를 완벽히 파악하고 있어야 한다.- 응답에 no-store 지시어가 포함되어 있지 않아야 한다.
- 공유 캐시(Shared Cache)의 경우
private지시어가 없거나, 공유 캐시의 저장을 허용하는 특정 조건이 갖춰져야 한다. - 요청에
Authorization헤더가 포함된 경우 원칙적으로 공유 캐시는 이를 저장할 수 없으나, 3.5. Storing Responses to Authenticated Requests에서 정의한 것처럼 명시적으로 공유 캐싱을 허용하는 지시어가 있다면 저장이 가능하다. - 마지막으로, 응답은 다음 중 적어도 하나 이상의 정보를 포함해야 캐시될 가치가 있는 것으로 간주된다.
public지시어- 개인용 캐시일 경우
private지시어 Expires헤더 또는max-age지시어- 공유 캐시일 경우
s-maxage지시어 - 캐싱을 허용하는 캐시 확장(Extension)
- 휴리스틱 캐싱(Heuristic Caching)이 가능한 것으로 정의된 상태 코드
참고로, 캐시 확장 기능은 위의 모든 요구사항을 재정의(Override)할 수 있는 권한을 가진다. 또한 실제 운영 환경에서 많은 캐시는 유효성 검사기(Validator)나 명시적인 만료 시간이 없는 응답은 저장 효율이 낮다고 판단하여 무시하기도 하지만, 명세상 이러한 응답의 저장이 금지된 것은 아니다.
3.1. Strong Header and Trailer Fields
응답을 저장할 때 헤더(Header)와 트레일러(Trailer) 필드를 어떻게 취급해야 하는지에 대한 규칙을 정의한다. 기본적으로 캐시는 응답을 저장할 때 자신이 인식하지 못하는 필드를 포함하여 수신된 모든 응답 헤더 필드를 함께 저장해야 한다. 이는 새로운 HTTP 헤더가 도입되었을 때 캐시 시스템을 거치더라도 해당 정보가 유실되지 않고 성공적으로 배포될 수 있도록 보장하기 위함이다.
다만, 몇 가지 중요한 예외 사항이 존재한다.
- 우선
Connection헤더 필드와 해당 헤더에 나열된 필드들은 RFC 9110 섹션 7.6.1에 따라 메시지를 전달하기 전에 반드시 제거되어야 하므로, 저장하기 전에 미리 제거하는 것이 허용된다. - 또한 특정 필드의 의미론상 전달 전 제거가 필요한 경우에도 저장 전 제거가 가능하다.
캐시 지시어에 의한 제한도 적용된다. no-cache나 private 지시어에 특정 헤더 필드 이름이 인자로 포함되어 있다면, 모든 캐시 또는 공유 캐시는 해당 필드를 저장해서는 안 된다.
또한, 캐시가 요청을 전달할 때 사용하는 프록시 전용 헤더 필드들은 저장해서는 안 된다. 여기에는 Proxy-Authenticate, Proxy-Authentication-Info, Proxy-Authorization 등이 포함되며, 이는 해당 정보가 특정 프록시와의 연결에만 유효하기 때문이다.
단, 캐시가 해당 프록시의 식별 정보를 캐시 키에 포함하는 경우에는 예외적으로 저장이 가능할 수 있다.
마지막으로, HTTP 메시지 본문(Body) 뒤에 오는 트레일러 필드의 경우 캐시는 이를 헤더 필드와 별도로 저장하거나 아예 폐기할 수 있다. 그러나 트레일러 필드를 일반 헤더 필드와 합쳐서 저장하는 행위는 엄격히 금지된다. 이는 헤더와 트레일러의 전송 시점과 의미론적 차이를 명확히 유지하기 위한 조치다.
3.2. Updating Stored Header Fields
캐시는 유효성 재검사 등을 통해 새로운 응답(예: 304 Not Modified)을 받았을 때, 기존에 저장된 응답의 헤더 필드를 업데이트해야 한다.
기본적으로 제공된 새로운 응답의 헤더 필드를 기존 응답에 추가하거나, 이미 존재하는 필드라면 새로운 값으로 교체하는 것이 원칙이다.
하지만 모든 헤더를 무조건 교체해서는 안 되며 아래 네 가지 예외 상황이 있다.
no-cache와private지시어로 지정한 특정 헤더- 캐시가 저장한 응답 데이터 자체가 의존하고 있는 헤더 필드 (데이터의 형식이나 상태를 정의하기 위해 사용했던 메타데이터. 가장 대표적인 사례는
Content-Encoding헤더이다. 서버가 gzip 방식으로 압축된 데이터를 보냈고, 브라우저가 이를 받아 압축을 해제한 상태로 캐싱했다고 가정했을 때, 이후 유효성 재검사 시 서버가304응답을 보내며Content-Encoding: br이라는 새로운 압축 방법을 명시한 헤더를 보낸다면, 캐시는 이 헤더를 업데이트해서는 안 된다.) - 수신자에 의해 자동으로 처리되고 제거되는 필드
Content-Length헤더 필드 (304 Not Modified는 본문을 포함하지 않으므로 이를 기준으로 헤더 값을 수정하면 무결성이 깨질 수 있기 때문)
특히 브라우저 같은 사용자 에이전트 캐시는 원본 응답 그대로 저장하기보다, 처리 과정을 거친 결과물을 저장하는 경우가 많다.
예를 들어 서버로부터 받은 압축된 데이터를 해제해서 저장하거나, HTML을 파싱해서 생성된 DOM 트리 형태로 저장하기도 한다.
이 과정에서 새로운 응답의 헤더가 저장해놓은 캐시 데이터의 Content-Encoding이나 Content-Type 헤더 값과 다른 경우 불일치 문제가 생길 수 있으므로 브라우저는 특정 헤더의 업데이트를 생략할 수 있다.
3.3. Storing Incomplete Response
네트워크 연결이 예기치 않게 종료되었거나 리소스의 일부분만 요청된 경우에도 캐시를 효율적으로 활용하기 위한 명세가 있다.
캐시는 GET 메서드를 통한 200 OK 응답의 헤더를 모두 수신했다면, 본문 데이터가 전부 도착하지 않더라도 이를 저장할 수 있는 권한을 가진다.
이때 반드시 해당 데이터가 불완전하다는 표시를 해야 한다.
206 Partial Content 응답 또한 나중에 전체를 채워 넣을 수 있는 미완성된 200 OK 응답과 같은 성격으로 간주하여 저장할 수 있다.
하지만, 캐시가 Range나 Content-Range 헤더 필드를 지원하지 않거나 해당 필드에서 사용되는 값의 단위를 이해하지 못한다면, 데이터 무결성을 보장할 수 없으므로 불완전한 응답을 저장하면 안 된다.
이미 저장된 불완전한 응답은 캐시가 스스로 부족한 부분에 대해 추가적인 범위 요청을 수행함으로써 완성될 수 있다. 서버로부터 받은 새로운 데이터 조각을 기존 데이터와 결합하는 과정은 3.4 Combining Partial Content의 규정을 따른다. 이렇게 조립된 데이터가 완벽한 전체를 이루기 전까지 캐시는 이를 일반적인 요청에 대한 응답으로 사용해서는 안 된다.
클라이언트에게 응답을 보낼 때의 규칙도 명확하다.
캐시는 불완전한 데이터를 전체 데이터인 것처럼 속여서 전달할 수 없으며, 오직 클라이언트가 요청한 범위가 캐시된 영역 내에 완벽하게 포함될 때만 일부분을 떼어 응답할 수 있다.
이 경우에도 캐시는 해당 응답이 전체가 아님을 나타내기 위해 반드시 206 Partial Content 상태 코드를 명시해야 한다.
3.4. Combining Partial Content
HTTP 통신 과정에서 응답 본문 전체가 아닌 일부분만 전달되는 경우는 흔히 발생한다.
네트워크 연결이 도중에 끊겨 데이터의 일부만 수신되었거나, 클라이언트가 특정 범위만 요청하는 Range 헤더를 사용한 경우이다.
캐시는 이러한 과정을 반복하면서 동일한 리소스에 대해 서로 다른 여러 데이터 조각을 보유하게 될 수 있다.
캐시는 이 조각들을 하나의 응답으로 병합하여 저장할 수 있는 권한을 가진다. 하지만 엄격한 전제 조건이 붙는다. 병합하려는 모든 조각이 동일한 강한 검사기(Strong Validator, 주로 ETag)를 공유해야 한다는 점이다. 이는 각 조각이 정확히 동일한 버전의 리소스에서 파생된 것임을 보장하기 위함이다.
데이터 조각들을 병합할 때, 캐시는 단순히 본문(Body)만 합치는 것이 아니라 메타데이터인 헤더 필드도 함께 관리해야 한다. 캐시는 새롭게 수신된 응답에 포함된 헤더 필드들을 사용하여 기존에 저장된 응답의 헤더를 업데이트해야 합니다. 이 과정은 앞서 살펴본 섹션 3.2의 업데이트 규칙을 따른다.
3.5. Storing Responses to Authenticated Requests
인증 정보(Authorization)가 포함된 요청에 대한 응답을 공유 캐시(Shared Cache)가 어떻게 처리해야 하는지에 대한 엄격한 보안 규칙이 있다.
기본적으로 공유 캐시(CDN, 기업 프록시 등)는 여러 사용자가 공용으로 사용하는 저장소이다.
요청에 Authorization 헤더가 포함되어 있다는 것은, 해당 응답이 특정 사용자의 권한에 따라 생성된 개인적인 정보일 가능성이 매우 높음을 의미한다.
만약 공유 캐시가 이 응답을 무분별하게 저장하고 다른 사용자에게 제공한다면, A 사용자의 개인 정보가 B 사용자에게 유출되는 심각한 보안 사고가 발생할 수 있다.
따라서 공유 캐시는 Authorization 헤더가 있는 요청에 대한 응답을 원칙적으로 재사용할 수 없다.
하지만 서버가 해당 콘텐츠를 공유 캐시에 저장해도 안전하다고 판단할 때가 있다.
서버가 다음과 같은 Cache-Control 지시어를 응답에 포함했을 경우에만 예외적으로 공유 캐시의 재사용을 허용한다.
must-revalidate: 캐시된 응답이 Stale한 상태가 되면, 반드시 원본 서버를 통해 유효성 재검사를 거쳐야만 사용할 수 있다는 지시어이다. 이를 통해 서버는 매번 권한을 다시 확인할 기회를 갖게 된다.public: 해당 응답이 인증된 요청에 대한 결과일지라도 공유 캐시에 저장될 수 있음을 명시적으로 선언하는 지시어이다.s-maxage: 공유 캐시 전용 만료 시간을 지정한다. 이 지시어가 존재한다는 것 자체가 공유 캐시의 저장을 암시적으로 허용하는 것으로 간주된다.
4. Constructing Responses from Caches
캐시가 저장된 응답을 클라이언트에게 다시 응답하기 위해 반드시 충족해야 하는 조건과 운영 방식이 존재한다.
캐시는 요청을 받았을 때 다음의 조건들이 모두 만족되어야만 저장된 응답을 재사용할 수 있다.
- 요청된 URI와 저장된 응답의 URI가 일치해야 한다.
- 요청 메서드 역시, 저장된 응답을 사용하기에 적절해야 한다.
Vary헤더 등에 의해 지정된 요청 헤더 필드들이 서로 일치해야 한다. (참고: 4.1 Calculating Cache Keys with the Vary Header Field)- 만약 응답에
no-cache지시어가 있다면, 서버를 통한 유효성 검사를 성공적으로 마친 경우에만 재사용이 가능하다. - 해당 응답이 신선한 상태(Fresh)이거나, 신선하지 않더라도 서빙이 허용된 상태(Stale) 또는 유효성 검사가 완료된 상태여야 한다.
유효성 검사 없이 저장된 응답을 즉시 사용하는 경우, 캐시는 반드시 Age 헤더 필드를 생성해야 한다.
이 헤더는 해당 응답이 캐시 저장소에 머문 시간을 초 단위로 나타낸다.
기존에 응답에 포함되어 있던 Age 헤더가 있다면, 이를 현재 계산된 값으로 교체하여 전달해야 한다.
POST, PUT, DELETE 같이 서버의 상태를 변경할 수 있는 **안전하지 않은 메서드(Unsafe methods)**의 경우, 캐시는 이를 가로채서 응답할 수 없으며 반드시 원 서버로 전달(Write-through)해야 한다.
이러한 요청들은 기존에 저장되어 있던 캐시 데이터를 무효화(Invalidate)시킬 수도 있다.
반면 캐시는 동일한 리소스에 대해 여러 요청이 동시에 들어올 때 이를 하나로 묶어 서버에 보내는 요청 결합(Request Collapsing) 기능을 수행할 수 있다. 캐시에 데이터가 없는 상태(Cache Miss)에서 여러 명의 사용자가 같은 페이지를 요청하면, 캐시는 단 하나의 요청만 서버에 보내고 그 결과를 공유하여 서버와 네트워크의 부하를 줄인다. 다만 서버가 보낸 응답이 특정 요청들에 대해 재사용이 불가능한 조건이라면, 캐시는 다시 개별적인 요청을 서버에 전달해야 하므로 지연 시간이 발생할 수 있다.
저장소에 적합한 응답이 여러 개 존재할 때는, Date 헤더를 기준으로 가장 최근에 생성된 응답을 사용해야 한다.
하지만 네트워크 지연이나 서버 설정 문제로 Date 값이 불분명하거나, 여러 응답 중 어느 것이 서버의 현재 상태를 정확히 반영하는지 확신할 수 없는 모호한 상황이라면, max-age=0(요청자가 가장 Fresh한 데이터를 원한다는 의미다.) 또는 no-cache(서버가 캐시된 데이터를 사용하기 전에 반드시 자신의 현재 데이터와 대조하여 유효성을 확인하게 만든다.)를 포함해서 서버에 요청을 보냄으로써 이를 명확히 할 수 있다.
또한 자체적인 시계 장치가 없는 캐시는 시간 흐름에 따른 신선도 계산이 불가능하므로, 저장된 데이터를 사용할 때마다 반드시 서버에 유효성 재검사를 요청해야 한다.
4.1. Calculating Cache Keys with the Vary Header Field
동일한 URI에 대해 요청 헤더의 내용에 따라 서로 다른 응답을 제공해야 하는 상황에서 캐시가 어떻게 정확한 응답을 식별하고 선택해야 하는지를 다루는 명세가 존재한다.
서버는 동일한 URI라도 클라이언트의 언어 설정(Accept-Language)이나 압축 지원 여부(Accept-Encoding)에 따라 다른 콘텐츠를 보낼 수 있다.
이때 서버는 응답에 Vary 헤더를 포함하여, 어떤 요청 헤더가 응답의 내용을 결정했는지 캐시에게 알려준다. (예: Vary: Accept-Language)
캐시는 Vary 헤더가 포함된 응답을 재사용하기 전에, 현재 들어온 요청의 헤더들이 해당 응답을 저장하게 만들었던 원본 요청의 헤더들과 일치하는지 반드시 확인해야 한다.
만약 일치하지 않는다면, 서버를 통한 유효성 재검사 없이는 해당 응답을 사용할 수 없다.
특히 Vary: *가 포함된 응답은 모든 요청과 일치하지 않는 것으로 간주되어 항상 재사용에 실패한다.
두 요청의 헤더가 일치한다고 판단하는 기준은 단순히 텍스트가 똑같은지에 국한되지 않는다. 다음과 같은 정규화 과정을 거친 후의 의미적 동일성을 기준으로 삼는다.
- 허용된 위치에서의 공백 추가 또는 제거
- 동일한 이름을 가진 여러 헤더 라인의 결합
- 해당 헤더 명세에 따른 의미론적 정규화 (예: 순서가 중요하지 않은 값들의 재배치, 대소문자 구분 없는 값의 정규화)
만약 특정 헤더가 한 요청에는 있고 다른 요청에는 없다면, 두 요청은 일치하지 않는 것으로 간주된다.
캐시 저장소에 특정 URI에 대해 일치하는 응답이 여러 개 존재할 수 있다. 이때 캐시는 최선의 응답을 선택하기 위해 다음과 같은 우선순위를 따른다.
- 우선순위 가중치(qvalues) 활용:
Accept계열의 헤더처럼 클라이언트가 선호도를 점수화하여 보낸 경우, 이를 기반으로 가장 적합한 응답을 선택할 수 있다. - 최신성 기준: 선호도 메커니즘이 없거나 동일한 선호도를 가진 응답이 여러 개라면,
Date헤더를 기준으로 가장 최근에 생성된 응답을 선택한다. Vary누락 응답 처리: 일부 서버가 기본 응답에서Vary헤더를 실수로 누락시키는 경우가 있다. 이로 인해 부적절한 응답이 전달되는 것을 막기 위해, 캐시는Vary헤더가 없는 응답보다는 유효한Vary헤더를 가진 가장 최근의 응답을 우선적으로 선택한다.
저장된 어떤 응답도 현재 요청과 일치하지 않는다면, 캐시는 해당 요청을 스스로 처리할 수 없다. 이 경우 캐시는 요청을 원 서버로 전달하며, 이때 자신이 이미 보유하고 있는 응답들을 설명하는 조건부 헤더(Preconditions)를 추가하여 서버가 더 효율적으로 판단할 수 있도록 돕는다.
4.2. Freshness
캐시된 응답은 신선한(Fresh) 상태와 신선하지 않은(Stale, 오래된) 상태 중 하나로 분류된다. 응답의 나이(Age)가 신선도 수명(Freshness Lifetime)을 초과하지 않았다면 해당 응답은 신선하다고 간주된다. 반대로 나이가 수명을 초과하면 신선하지 않은 것으로 판단한다.
여기서 나이란, 응답이 서버에서 생성되거나 유효성 검사를 마친 시점부터 흐른 시간을 의미하며, 신선도 수명은 서버가 해당 응답을 캐시가 서버 확인 없이 사용해도 좋다고 허용한 전체 기간을 뜻한다.
신선도 여부를 결정하는 공식은 다음과 같다.
response_is_fresh = (freshness_lifetime > current_age)
서버는 아래 두 가지 방식을 통해 응답의 만료 시간을 캐시에게 알린다.
- 명시적 만료 시간(Explicit Expiration Time):
Expires헤더 필드나Cache-Control의max-age지시어를 사용하여 미래의 특정 시점을 지정하는 방식이다. 서버는 보통 해당 리소스가 지정된 시간 전까지는 의미 있게 변하지 않을 것이라는 판단하에 이 값을 설정한다. 만약 서버가 캐시로 하여금 매번 유효성 검사를 하게 만들고 싶다면, 만료 시간을 과거의 날짜로 설정하여 응답이 생성되자마자 신선하지 않은 상태가 되도록 유도할 수도 있다. - 경험적 만료 시간(Heuristic Expiration Time): 서버가 명시적인 만료 정보를 제공하지 않을 경우, 캐시는 특정 상황에서 자체적인 알고리즘을 동원하여 임의의 만료 시간을 할당할 수 있다. 이는 4.2.2. Calculating Heuristic Freshness에 규정된 로직을 따른다.
캐시는 신선도를 계산할 때 날짜 파싱에서 발생할 수 있는 오류를 방지하기 위해 엄격한 규칙을 준수해야 한다. 모든 HTTP 날짜 형식은 대소문자를 구분하도록 되어 있지만, 캐시는 이를 대소문자 구분 없이 처리할 것이 권장된다. 또한 캐시의 내부 시계 해상도(시스템이 시간을 인지하고 기록할 수 있는 최소한의 시간 간격이나 정밀도)가 HTTP 날짜 값보다 낮을 경우, 만료 시간을 실제 값보다 이전 시간으로 처리하여 보수적으로 관리해야 한다.
특히 시간대 처리에서 중요한 규칙은 로컬 시간대가 계산에 영향을 주어서는 안 된다는 점이다. 모든 계산은 **GMT(그리니치 표준시)**를 기준으로 이루어져야 하며, GMT 이외의 시간대 약어가 포함된 날짜는 만료 시간 계산 시 무효한 것으로 간주될 수 있다.
클라이언트는 요청 시 max-age나 min-fresh 지시어를 사용하여 서버가 정한 신선도 계산 결과에 제한을 두도록 제안할 수 있다.
하지만 캐시가 반드시 이 제안을 따라야 할 의무는 없다.
마지막으로 주의할 점은 신선도 개념이 오직 캐시 운영에만 적용된다는 것이다. 신선도가 지났다고 해서 사용자 에이전트(브라우저)가 화면을 강제로 새로고침하거나 리소스를 다시 불러와야 하는 것은 아니다. 이는 캐싱 메커니즘과 브라우저의 히스토리 메커니즘(뒤로 가기 등)이 서로 다르게 동작하기 때문이며, 이에 대한 상세한 차이는 RFC 9111 섹션 6에서 다루고 있습니다.
4.2.1. Calculating Freshness Lifetime
캐시가 응답의 신선도 수명(freshness_lifetime)을 결정할 때, 아래 나열된 규칙 중 가장 먼저 일치하는 항목을 선택하여 결정한다.
s-maxage: 만약 캐시가 공유 캐시이고 응답에s-maxage가 포함되어 있다면, 그 값을 신선도 수명으로 사용한다.max-age:s-maxage가 없거나 캐시가 개인 캐시인 경우max-age값을 수명으로 채택한다.Expires: 위 지시어들이 없다면Expires - Date계산식을 통해 수명을 계산한다. 여기서Date는 응답이 생성된 시점의 서버 시각을 의미한다. 만약, 응답에Date헤더가 없다면 응답 메시지를 수신한 시각을 대신 사용한다.
명시적인 만료 정보가 전혀 없는 경우에는 바로 아래의 경험적 신선도(Heuristic Freshness) 규칙을 적용할 수 있다.
데이터의 무결성을 위해 캐시는 중복되거나 충돌하는 지시어에 대한 처리 규칙도 준수해야 한다.
동일한 지시어가 여러 번 나타나면 가장 먼저 나타난 값을 사용하거나, 혹은 해당 응답을 아예 신선하지 않은(Stale) 것으로 간주하여 응답을 안전하게 처리해야 한다.
만약 max-age와 no-cache처럼 서로 충돌하는 지시어가 동시에 존재한다면, 캐시는 항상 더 엄격하고 제한적인 쪽(이 경우 no-cache)을 우선시하여 동작해야 한다.
마지막으로 캐시는 신선도 정보가 정수가 아니거나 형식이 잘못된 경우 등 유효하지 않은 정보를 포함하고 있다면, 해당 응답을 즉시 신선하지 않은 상태로 간주할 것을 권장받는다. 이는 잘못된 캐시 정보로 인해 데이터가 의도치 않게 오랫동안 서빙되는 부작용을 막기 위한 보수적인 접근 방식이다.
4.2.2. Calculating Heuristic Freshness
**경험적 신선도(Heuristic Freshness)**는 서버가 응답의 명시적인 만료 정보를 제공하지 않을 경우, 캐시는 특정 상황에서 자체적인 알고리즘을 이용하여 할당하는 임의의 만료 시간을 의미한다.
캐시는 저장된 응답에 명시적인 만료 시간이 이미 포함되어 있다면 절대로 경험적 추정 방식을 사용해서는 안 된다.
또한 아무 응답이나 마음대로 추정할 수 있는 것도 아니다.
RFC 9111 섹션 3의 요건에 따라, 명시적인 신선도 정보가 없으면서도 상태 코드가 200, 301, 404 처럼 표준상 경험적으로 캐싱이 가능하다고 정의된 경우나, public 지시어 등을 통해 명시적으로 캐싱이 허용된 응답에 대해서만 이 방식을 적용할 수 있다.
과거의 명세에서는 이러한 상태 코드들을 기본적으로 캐싱 가능(cacheable by default)하다고 불렀으나, 현재 명세에서는 경험적으로 캐싱 가능(heuristically cacheable)하다는 용어로 정리되었다.
가장 대표적인 추정 방식은 Last-Modified 헤더를 이용하는 것이다.
리소스가 마지막으로 수정된 시각이 포함되어 있다면, 캐시는 리소스가 생성된 시각(Date)과 마지막 수정 시각 사이의 간격을 계산한다.
표준 명세는 이 간격의 일정 비율을 만료 시간으로 잡는 방식을 권장하며, 보통 그 비율은 10% 정도로 설정된다.
예를 들어 어떤 리소스가 서버에서 100시간 전에 수정되었다면, 캐시는 앞으로 10시간 동안은 리소스가 변하지 않을 것이라고 추정하여 신선한 상태로 간주할 수 있다. 이는 오래전에 수정된 데이터일수록 앞으로도 한동안은 유지될 가능성이 높다는 가설에 기반한다.
과거 RFC 2616 명세에서는 물음표(?)가 포함된 쿼리 컴포넌트 URI의 경우 경험적 신선도 계산을 아예 금지하기도 했다.
동적인 데이터를 다루는 경우가 많아 캐싱하기에 위험하다고 판단했기 때문이다.
하지만 실제 실무 환경에서는 이러한 제약이 널리 지켜지지 않았고, 현재의 표준은 이를 강제로 금지하지 않는다.
대신 서버 운영자들에게 캐싱을 원치 않는 동적 리소스가 있다면 표준의 자동적인 처리에 기대지 말고 Cache-Control: no-cache와 같은 명시적인 지시어를 반드시 보낼 것을 강력히 권고하고 있다.
4.2.3. Calculating Age
캐시된 응답의 **현재 나이(current_age)**를 계산하는 정교한 알고리즘을 정의한다.
Age 헤더는 응답이 원 서버에서 생성되거나 유효성이 확인된 이후부터 현재까지 흐른 전체 시간을 초 단위로 나타내며, 이는 여러 캐시 계층을 거치며 합산된 시간과 네트워크 전송 시간을 모두 포함한다.
정확한 계산을 위해 다음 다섯 가지 데이터가 사용된다.
age_value: 서버로부터 받은 응답에 포함된Age헤더 값이다. 없으면 0으로 간주한다.date_value: 서버가 응답을 생성한 시각을 나타내는Date헤더 값이다.now: 현재 캐시 시스템의 시계 값이다.request_time: 캐시가 서버에 요청을 보낸 시점의 시각이다.response_time: 캐시가 서버로부터 응답을 받은 시점의 시각이다.
캐시는 두 가지 독립적인 방식으로 응답의 초기 나이를 추정한다.
- 겉보기 나이(apparent_age): 캐시가 응답을 받은 시각에서 서버의 생성 시각을 뺀 값이다 ($response_time - date_value$). 서버와 캐시의 시계가 잘 동기화되어 있다는 전제하에 작동하며, 음수가 나오면 0으로 처리한다.
- 보정된 나이 값(corrected_age_value): 네트워크 지연 시간을 고려한 방식이다. 요청을 보낸 시점부터 응답을 받은 시점까지의 시간($response_delay$)을 $age_value$에 더하여 계산합니다. 이는 응답이 전송되는 동안 흐른 시간까지 나이에 포함시키기 위함이다.
두 방식 중 더 보수적(안전)인 값을 선택하기 위해 다음 공식을 사용합니다.
$$corrected_initial_age = max(apparent_age, corrected_age_value)$$
이 값은 응답이 캐시에 막 도착했을 때의 시점 기준 나이가 됩니다.
마지막으로 응답이 캐시 저장소에 머문 시간($resident_time$)을 고려해야 한다. 캐시에 저장된 이후 현재($now$)까지 흐른 시간을 초기 나이에 더하면 비로소 최종적인 현재 나이가 도출된다.
$$resident_time = now - response_time$$ $$current_age = corrected_initial_age + resident_time$$
이 알고리즘의 핵심 목적은 서버와 캐시 간의 시계 불일치(Clock Skew)나 네트워크 전송 지연으로 인해 발생할 수 있는 오차를 최소화하는 것이다. 캐시는 항상 가능한 가장 큰 값을 나이로 채택함으로써, 리소스를 실제보다 신선하다고 오판하여 오래된 데이터를 서빙하는 보안 및 무결성 위험을 방지한다.
4.2.4. Serving Stale Response
신선하지 않은(Stale) 응답이란 명시적인 만료 정보가 있거나 경험적 만료 시간이 계산되었음에도 불구하고, 앞서 살펴본 신선도 계산 공식에 의해 유효 기간이 지난 응답을 의미한다. 캐시의 기본 원칙은 이러한 오래된 데이터를 사용하지 않는 것이지만, 특정 예외 상황에서는 이를 서빙하는 것이 허용된다.
프로토콜상의 명시적인 지시어가 있는 경우, 캐시는 어떤 상황에서도 오래된 응답을 클라이언트에게 보내서는 안 된다. 다음과 같은 지시어들이 이에 해당한다.
no-cache: 저장소의 응답을 사용하기 전 반드시 서버의 재검증을 거쳐야 한다. 이름 때문에 저장 자체를 하지 않는 것으로 오해하기 쉽지만, HTTP 명세에서 저장조차 허용하지 않는 지시어는no-store이다.no-cache는 저장된 응답을 사용할 때마다 원본 서버에 데이터 유효성을 검증 받으라는 강제 재검증 지시이다.must-revalidate: 응답이 신선하지 않게 되면 반드시 서버의 재검증을 거쳐야 하며, 서버 연결 실패 시 오래된 응답을 대신 보내는 행위도 금지된다.proxy-revalidate/s-maxage: 공유 캐시에 대해must-revalidate와 유사한 엄격한 재검증 의무를 부여한다.
위의 금지 지시어가 없는 경우에 한해서, 다음 상황에서만 Stale한 응답을 전달할 수 있다.
- 연결 단절(Disconnected): 캐시가 원 서버와 통신할 수 없는 네트워크 단절 상태일 때, 서비스 연속성을 위해 저장된 데이터를 제공할 수 있다.
- 클라이언트의 허용: 클라이언트가 요청 헤더에
max-stale지시어를 포함하여, "N초가 지난 데이터라도 괜찮다"고 명시한 경우다. - 서버의 허용 및 확장 지시어: 서버가
stale-while-revalidate나stale-if-error(RFC 5861)와 같은 확장 지시어를 통해 특정 조건에서 오래된 응답의 사용을 허용한 경우다. - 별도 계약: 프로토콜 외부의 대역 외(Out-of-band) 설정이나 계약에 따라 합의된 경우다.
4.3. Validation
검증(Validation) 또는 재검증(Revalidation)은, 캐시가 저장된 데이터를 가지고는 있지만 그 데이터를 그대로 사용하기에는 확신이 없을 때(신선도가 지났거나 여러 후보 중 선택이 어려울 때), 원 서버의 판단을 구하는 과정이다.
캐시가 저장된 응답을 직접 내보낼 수 없는 상황이 되면, 해당 요청을 서버로 전달하면서 캐시가 이미 가지고 있는 정보들을 함께 보낸다.
이때 사용되는 것이 HTTP의 조건부 요청(Conditional Request) 메커니즘이다.
캐시는 If-None-Match(ETag 기반)나 If-Modified-Since(날짜 기반) 같은 헤더를 요청에 추가하여, "내가 이런 버전을 가지고 있는데, 혹시 바뀐 게 있나요?"라고 서버에 묻는다.
요청을 받은 서버는 캐시가 보낸 조건부 헤더를 보고 두 가지 중 하나를 선택합니다.
- 기존 데이터가 유효한 경우 (304 Not Modified): 서버의 리소스가 캐시의 것과 다르지 않다면, 서버는 본문을 생략한
304응답을 보낸다. 이 응답에는 새로운 만료 시간 정보 등이 포함될 수 있으며, 캐시는 이를 바탕으로 기존 저장된 응답의 메타데이터(헤더)를 업데이트하여 다시 신선한 상태로 만든다. - 데이터가 바뀌었거나 새로운 응답이 필요한 경우 (200 OK 등): 서버의 리소스가 변경되었다면, 서버는 새로운 전체 응답을 보냅니다. 캐시는 기존에 저장되어 있던 낡은 데이터를 폐기하고 이 새로운 응답으로 교체하여 저장한다.
이 과정의 가장 큰 이점은 데이터의 최신성을 보장하면서도 네트워크 비용을 최소화한다는 점이다.
304 응답이 오는 경우 실제 데이터(Body)의 전송이 생략되므로, 캐시는 아주 적은 네트워크 비용만으로 기존 데이터를 다시 사용할 수 있는 정당성을 확보하게 된다.
4.3.1. Sending a Validation Request
캐시가 서버에 데이터 유효성을 확인하기 위해 조건부 요청을 생성하고 전송하는 구체적인 절차를 다룬다.
캐시는 클라이언트의 요청을 처리하는 과정에서 검증이 필요할 때 해당 요청을 기반으로 시작하거나, 스스로 독립적인 요청을 만들어낼 수도 있다.
독립적인 요청을 만들 때는 기존에 저장된 응답에서 메서드, 대상 URI, 그리고 Vary 헤더에 정의된 요청 헤더들을 복사하여 요청의 뼈대를 구성한다.
여기에 캐시는 저장된 응답에 포함되어 있던 검증자(Validator) 메타데이터를 활용하여 전제 조건(Precondition) 헤더를 추가한다.
이 과정은 서버가 캐시의 데이터와 현재 서버의 데이터가 동일한지 비교할 수 있는 근거를 제공한다.
명세에서는 두 가지 주요 검증자를 제시한다.
Last-Modified(타임스탬프): 리소스가 마지막으로 수정된 시각을 나타낸다. 주로If-Modified-Since헤더에 사용되어 해당 시각 이후에 데이터가 변경되었는지 확인하는 데 쓰인다.ETag(엔티티 태그): 리소스의 특정 버전을 식별하는 고유한 문자열이다. 하나 이상의ETag를If-None-Match헤더에 실어 보내 서버의 현재 데이터가 캐시된 버전 중 하나와 일치하는지 확인한다.ETag는 시간 기반 검증보다 정밀하다.
캐시는 조건부 요청을 보낼 때 다음과 같은 의무 사항을 준수해야 한다.
ETag우선 전송 (MUST): 저장된 응답에ETag가 포함되어 있었다면, 반드시 관련 엔티티 태그를 전송해야 한다. 이는 데이터의 정확한 버전을 식별하는 가장 확실한 방법이기 때문이다.Last-Modified전송 권장 (SHOULD): 범위 요청(Subrange)이 아니고 단일 응답을 검증하는 상황에서Last-Modified값이 있다면 이를If-Modified-Since와 함께 보낼 것이 권장된다.- 범위 요청 시의 예외 (MAY): 부분 데이터(Subrange)를 요청할 때
ETag없이Last-Modified만 있다면 이를If-Unmodified-Since나If-Range와 함께 보낼 수 있다.
실무적으로 캐시는 ETag가 더 우수함에도 불구하고 두 종류의 검증자를 모두 보내는 경우가 많다. 이는 ETag를 이해하지 못하는 오래된 중간 서버들도 적절하게 응답할 수 있도록 호환성을 배려하기 위함이다.
4.3.2. Handling a Received Validation Request
클라이언트(브라우저나 하위 캐시)로부터 조건부 요청을 받은 중간 캐시(프록시, CDN 등)가 이를 어떻게 처리해야 하는지에 대한 규칙을 정의한다.
캐시 체인(Request Chain)에서 중간 캐시는 서버 역할과 클라이언트 역할을 동시에 수행한다.
하위 캐시로부터 조건부 요청을 받았을 때, 중간 캐시는 자신이 저장한 응답이 그 조건을 만족하는지 먼저 확인해야 한다.
만약 저장된 200 OK 또는 206 Partial Content 응답으로 요청을 해결할 수 있다면, 요청에 포함된 조건부 헤더(Precondition)를 저장된 응답의 검증자(Validator)와 대조하여 평가해야 한다.
단, 캐시는 모든 조건부 헤더를 처리해서는 안 된다. 원 서버에만 적용되어야 하는 조건(예: 특정 리소스의 존재 여부 확인 등), 캐시된 응답으로 해결할 수 없는 의미를 가진 요청, 또는 해당 리소스에 대한 저장된 응답이 없는 경우에는 조건부 헤더를 평가하지 말고 서버로 전달해야 한다.
캐시가 조건부 요청을 평가할 때는 아래 우선순위를 따른다.
If-None-Match가 있다면If-Modified-Since보다 먼저 평가한다.If-Match와If-Unmodified-Since는 일반적으로 캐시에서 처리하기보다는 원 서버로 전달되어야 하는 헤더로 간주한다.- 만약
If-Modified-Since요청을 받았는데 저장된 응답에Last-Modified헤더가 없다면, 캐시는 저장된 응답의Date헤더(없다면 수신 시각)를 기준으로 수정 여부를 판단해야 한다.
중간 캐시가 자신의 저장된 응답을 서버로부터 재검증받기 위해 요청을 전달할 때, ETag 결합(Union) 기법을 사용할 수 있다.
- 클라이언트가 보낸
If-None-Match리스트와 캐시 자신이 보낸 리소스를 검증하기 위한ETag리스트를 하나로 합쳐서 서버에 보낼 수 있다. 이를 통해 한 번의 요청으로 클라이언트와 캐시 모두의 데이터를 동시에 검증한다. - 단, 캐시가 리소스의 일부분(Partial Content)만 가지고 있다면, 클라이언트의 요청 범위가 자신이 가진 데이터로 완전히 충족될 때만 해당
ETag를 리스트에 포함할 수 있다.
서버로부터 304 Not Modified 응답을 받았을 때, 서버가 보낸 ETag가 캐시에는 있지만 클라이언트의 요청 리스트에는 없는 경우를 주의해야 한다.
이 상황은 캐시가 클라이언트보다 최신 버전(또는 다른 버전)을 가지고 있음을 의미한다.
이때 캐시는 클라이언트에게 304를 그대로 전달하는 대신, 자신이 가진 저장된 응답을 304의 메타데이터로 업데이트한 후 200 OK 응답으로 조립하여 클라이언트에게 보내야 한다.
이는 클라이언트가 요구한 조건을 캐시가 이미 만족하는 최신 데이터로 보완하여 해결해 주는 과정이다.
4.3.3. Handling a Validation Response
캐시가 보낸 조건부 요청에 대해 서버가 보내온 응답 상태 코드에 따라 캐시가 어떻게 행동해야 하는지 정의한다.
서버의 응답은 크게 세 가지 시나리오로 나뉜다.
304 Not Modified응답을 받은 경우: 캐시가 서버에 보낸 검증자(ETag또는Last-Modified)가 서버의 현재 데이터와 일치함을 의미한다. 이때 캐시는 서버로부터 본문(Body)을 다시 받을 필요가 없으므로, 기존에 저장된 응답을 최신 상태로 업데이트하여 재사용한다. 구체적인 업데이트 방법은 4.3.4. Freshening Stored Responses upon Validation의 규칙을 따른다.- 전체 응답(
200 OK등)을 받은 경우: 캐시가 보낸 조건부 요청의 전제 조건이 충족되지 않았음을 의미하며, 서버의 리소스가 변경되었거나 캐시가 가진 것보다 최신 버전의 데이터가 서버에 있음을 뜻한다. 이때 캐시는 저장된 응답을 무시하고 서버가 보낸 전체 응답을 사용하여 클라이언트의 요청에 답해야 한다. 또한, 3. Storing Responses in Caches의 저장 제약 조건을 만족한다면 이 새로운 전체 응답을 저장소에 기록하여 기존 데이터를 대체할 수 있다.
캐시가 유효성 검사를 시도하는 도중에 서버로부터 5xx (Server Error) 응답을 받는 특수한 상황이 발생할 수 있다. 이때 캐시는 두 가지 선택권을 가진다.
- 서버에서 받은
5xx에러를 클라이언트에게 그대로 전달한다. - 서버가 응답하지 않은 것으로 처리하고 자체적으로 대응한다. 이 경우 캐시는 4.2.4. Serving Stale Responses의 제약 조건(예:
must-revalidate가 없는 경우 등)을 충족한다면 이전에 저장된 응답(Stale Response)을 대신 클라이언트에게 보낼 수 있다. 또는 유효성 검사 요청을 다시 시도할 수도 있다.
4.3.4. Freshening Stored Responses upon Validation
서버로부터 304 (Not Modified) 응답을 받았을 때, 캐시가 저장소에 있는 어떤 응답들을 찾아내어 업데이트해야 하는지 그 정교한 필터링 과정을 정의한다.
캐시는 먼저 현재 요청에 대해 사용될 수 있었던 모든 저장된 응답들을 후보군으로 삼는다. 이는 4. Constructing Responses from Caches에서 정의한 조건(URI 일치, 메서드 허용 등)을 만족하는 응답들을 의미한다. 다만, 신선도(Freshness) 요건은 제외한다. 유효성 검사 자체가 신선하지 않은 데이터를 갱신하기 위해 수행되는 것이기 때문이다.
후보군이 정해지면, 캐시는 서버가 보낸 304 응답의 검증자 정보를 바탕으로 다음의 우선순위에 따라 최종 업데이트 대상을 확정한다.
- 강한 검사기(Strong Validator) 우선 적용: 서버 응답에 하나 이상의 강한 검사기(예:
ETag)가 포함된 경우, 캐시는 후보군 중 이와 정확히 일치하는 강한 검사기를 가진 모든 응답을 업데이트 대상으로 선택한다. 만약 후보군 중에 서버가 보낸 것과 일치하는 강한 검사기가 하나도 없다면, 캐시는 해당304응답을 사용하여 어떤 기존 응답도 업데이트해서는 안 된다. - 약한 검사기(Weak Validator) 적용: 서버 응답에 강한 검사기는 없고 약한 검사기(예:
W/접두사가 붙은ETag)만 있는 경우이다. 이때 캐시는 후보군 중에서 이 약한 검사기와 일치하는 응답들을 찾고, 그중 가장 최신의 응답 하나만을 선택하여 업데이트한다. - 검사기가 없는 경우: 서버 응답에 어떤 검증자도 포함되어 있지 않은 특수한 상황(예: 클라이언트가
Last-Modified없이If-Modified-Since를 생성한 경우 등)이다. 이때 후보군에 단 하나의 응답만 존재하고, 그 응답 역시 검증자를 가지고 있지 않다면 해당 응답을 업데이트 대상으로 선택한다.
최종적으로 선택된 저장된 응답에 대해, 캐시는 반드시 서버가 보낸 304 응답의 헤더 필드를 사용하여 기존 헤더를 업데이트해야 한다.
이 과정은 3.2. Updating Stored Header Fields에서 정의한 규칙을 엄격히 따른다.
이를 통해 캐시된 데이터의 본문(Body)은 그대로 유지하면서, 서버가 새로 보낸 신선도 정보(Cache-Control 등)를 반영하여 리소스의 유효 수명을 연장하게 된다.
4.3.5. Freshening Responses with HEAD
HEAD 메서드를 사용하여, 저장된 GET 응답을 갱신하거나 무효화하는 방법을 정의한다.
HEAD 응답은 본문 데이터 없이 메타데이터(헤더)만 포함하므로, 전체 데이터를 다시 받지 않고도 캐시를 관리하는 효율적인 수단이 된다.
HEAD 메서드의 특성은 응답 본문을 제외하고 GET과 동일한 헤더 정보를 제공한다는 점이다.
이 속성은 두 가지 상황에서 유용하다.
- 저장된 응답에 검증자(Validator)가 없어 효율적인 조건부
GET요청을 보낼 수 없는 경우이다. - 리소스가 변경되었더라도 당장 본문 데이터를 전송받고 싶지 않을 때이다.
캐시는 원 서버에 HEAD 요청을 보내 받은 200 OK 응답을 바탕으로 기존의 GET 응답들을 최신 상태로 유지하거나, 더 이상 유효하지 않다고 판단하여 무효화할 수 있다.
캐시는 HEAD 요청에 대한 200 OK 응답을 받으면, 해당 요청에 선택될 수 있었던 모든 저장된 GET 응답들을 대상으로 다음의 일치 여부를 검사한다.
- 검증자 일치: 저장된 응답과
HEAD응답이 가진 검증자 필드(ETag,Last-Modified)의 값이 서로 일치해야 한다. - 콘텐츠 길이 일치: 만약
HEAD응답에Content-Length헤더가 포함되어 있다면, 이 값이 저장된GET응답의 본문 크기와 정확히 일치해야 한다.
이 조건들을 모두 만족하면 캐시는 저장된 응답이 여전히 유효하다고 판단하여 메타데이터를 업데이트한다. 만약 하나라도 일치하지 않는다면, 캐시는 해당 저장된 응답이 더 이상 최신 상태가 아니라고 간주하여 신선하지 않은(Stale) 상태로 처리해야 한다.
조건을 만족하여 업데이트를 진행하기로 결정했다면, 캐시는 반드시 HEAD 응답에서 제공된 헤더 필드들을 사용하여 기존 저장된 GET 응답의 메타데이터를 갱신해야 한다.
이 과정은 3.2. Updating Stored Header Fields에서 정의한 일반적인 헤더 업데이트 규칙을 엄격히 따른다.
이를 통해 본문 데이터를 다시 다운로드하지 않고도, Cache-Control이나 Expires 같은 수명 관련 정보를 서버의 최신 지침에 맞게 수정할 수 있다.
4.4. Invalidating Stored Responses
서버의 상태를 변경할 수 있는 '안전하지 않은 메서드(Unsafe methods)' 요청이 발생했을 때, 캐시가 데이터의 최신성을 유지하기 위해 기존에 저장된 응답을 어떻게 무효화해야 하는지 정의한다.
PUT, POST, DELETE와 같은 메서드는 서버의 데이터를 수정, 생성 또는 삭제하는 작업에 사용된다.
만약 어떤 URI에 대해 POST 요청이 성공했다면, 해당 URI에 대해 이전에 저장되어 있던 GET 응답은 더 이상 유효하지 않을 가능성이 높다.
따라서 중간 캐시는 데이터 불일치를 방지하기 위해 관련된 캐시 엔트리를 무효화해야 할 의무가 있다.
캐시는 안전하지 않은 메서드(또는 안전 여부를 알 수 없는 메서드)에 대해 2xx (성공) 또는 3xx (리다이렉션)와 같은 '오류가 없는 응답(Non-error response)'을 받았을 때 다음과 같이 동작한다.
- 대상 URI 무효화 (MUST): 요청의 대상이 된 URI(Target URI)에 대한 저장된 응답은 반드시 무효화해야 한다.
- 연관 URI 무효화 (MAY): 응답 헤더의
Location이나Content-Location에 명시된 URI들 또한 무효화할 수 있다. 서버가 리소스를 생성하거나 이동시킨 경우, 해당 주소들의 기존 캐시도 최신이 아닐 수 있기 때문이다. - 보안 제약 (MUST NOT): 무효화하려는 URI의 오리진(Origin, 스킴/호스트/포트 조합)이 요청 대상 URI의 오리진과 다르다면 절대로 무효화를 수행해서는 안 된다. 이는 공격자가 타 도메인의 캐시를 임의로 삭제하는 서비스 거부(DoS) 공격을 방지하기 위한 필수적인 보안 장치이다.
여기서 무효화란 단순히 데이터를 지우는 것만을 의미하지 않는다. 캐시는 해당 URI와 일치하는 모든 저장된 응답을 물리적으로 삭제하거나, 혹은 '무효(Invalid)' 상태로 표시하여 이후의 요청에 대해 반드시 **서버의 재검증(Mandatory Validation)**을 거치도록 강제해야 한다.
이러한 무효화 규칙은 완벽한 전역 동기화를 보장하지 않는다.
특정 상태 변경 요청은 오직 해당 요청이 거쳐 간 경로상의 캐시들에 대해서만 무효화를 트리거한다.
따라서 네트워크의 다른 경로에 있는 캐시들은 여전히 과거의 응답을 보유하고 있을 수 있다.
이는 HTTP 캐싱 시스템이 가진 구조적 특성이며, 이를 보완하기 위해 서버는 적절한 만료 시간(Expiration) 정책을 병행하여 사용해야 한다.
5. Field Definitions
이 섹션에서는 캐싱과 관련된 HTTP 필드의 구문과 의미를 정의한다.
5.1. Age
Age 응답 헤더 필드의 정의와 구체적인 처리 규칙을 명시한다.
이 헤더는 클라이언트가 받은 응답이 원 서버에서 생성되거나 유효성 검사를 마친 시점부터 현재까지 얼마나 시간이 흘렀는지를 나타내는 지표이다.
Age 헤더의 값은 0 이상의 정수(delta-seconds)로 표현되며, 단위는 초이다.
이 값은 응답이 거쳐 온 모든 중간 캐시에서의 체류 시간과 네트워크 전송 시간을 합산하여 계산된다.
즉, 클라이언트가 받은 Age 값이 Age: 3600이라면, 해당 리소스는 원 서버에서 생성되거나 마지막으로 검증된 지 1시간이 지났음을 의미한다.
Age 헤더는 원칙적으로 하나의 값만 가질 수 있는 싱글톤(Singleton) 필드이다.
하지만 구현상의 오류로 인해 쉼표로 구분된 리스트 형태의 여러 값이 포함된 경우, 캐시는 첫 번째 값만을 사용하고 나머지는 폐기해야 한다.
또한, 폐기 후 남은 값이 0 이상의 정수가 아닌 유효하지 않은 형식이라면 캐시는 해당 필드 자체를 무시해야 한다.
응답에 Age 헤더가 포함되어 있다는 것은, 이 응답이 원 서버가 아닌 중간 캐시나 로컬 저장소에서 제공되었음을 시사한다.
만약 원 서버가 클라이언트의 요청을 직접 받아서 응답을 새로 생성하거나 유효성을 검증하는 바로 그 순간에는 Age는 0이다. 따라서 원 서버는 일반적으로 Age 헤더를 보내지 않거나 Age: 0으로 보낸다.
즉, 이번 요청을 위해 원 서버가 직접 데이터를 생성하거나 검증하지 않았고, 캐시로부터 전달받은 응답이다.
반대로 Age 헤더가 없다고 해서 반드시 원 서버와 통신했다는 뜻은 아니다.
일부 오래된 캐시 구현체나 특정 설정의 서버는 Age 헤더를 누락할 수 있기 때문이다.
따라서 Age 헤더의 부재를 서버 직접 응답의 증거로 삼아서는 안 되며, 공식적인 확인이 필요한 경우 Date 헤더의 시각과 현재 시각을 비교하거나 다른 메타데이터를 종합적으로 판단해야 한다.
5.2. Cache-Control
HTTP 캐싱 제어의 중추 역할을 하는 Cache-Control 헤더의 기본 정의와 구문 규칙을 명시한다.
이 헤더는 요청과 응답 체인에 있는 모든 캐시에게 전달되는 **지시어(Directive)**들의 목록을 포함한다.
Cache-Control 지시어는 **단방향(Unidirectional)**으로 작동한다.
즉, 클라이언트가 요청에 특정 지시어를 포함했다고 해서 서버가 응답에 동일한 지시어를 포함하거나 복사해야 할 의무는 없다.
반대로 서버의 지시어 역시 응답에만 유효하며 요청의 성격에 직접적인 영향을 주지는 않는다.
또한, 프록시 서버는 자신이 캐시 기능을 구현하고 있는지 여부와 상관없이 수신한 캐시 지시어를 그대로 다음 서버나 클라이언트에게 전달(Pass through)해야 한다. 이는 특정 지시어가 현재 거쳐 가는 프록시가 아닌, 그 뒤에 있는 다른 캐시 시스템(예: 브라우저 캐시나 CDN)을 대상으로 할 수 있기 때문이다. HTTP 명세상, 특정 지시어를 특정 캐시에게만 타겟팅하는 방법은 존재하지 않으며, 모든 지시어는 경로상의 모든 수신자에게 열람 가능하다.
Cache-Control 지시어는 다음과 같은 문법적 특징을 가진다.
- 대소문자 구분 없음: 지시어를 식별하는 토큰은 대소문자를 구분하지 않고 비교된다. (예:
no-cache와NO-CACHE는 동일하게 취급된다.) - 선택적 인자: 지시어는 필요에 따라
=뒤에 인자를 가질 수 있다. 인자는 토큰 형태(알파벳, 숫자, 일부 특수문자(!, #, $, %, &, ', *, +, -, ., ^, _,, |, ~))이거나(예:Cache-Control: max-age=3600) 큰따옴표로 감싸진 문자열 형태(예:Cache-Control: private="Set-Cookie"`)일 수 있으며, 수신자는 두 형식을 모두 수용해야 한다. - 다중 지시어: 쉼표로 구분하여 여러 지시어를 한 헤더에 나열할 수 있다. (예:
Cache-Control: public, max-age=3600)
만약 명세에서 인자를 정의하거나 허용하지 않은 지시어에 인자가 붙어 있다면, 이는 무시되거나 오류로 처리될 수 있다.
5.2.1. Request Directives
요청 지시어를 정의한다. 이 지시어들은 권고 사항이다. 캐시가 이를 구현할 수 있지만, 필수 구현사항은 아니다.
5.2.1.1. max-age
max-age 요청 지시어는 클라이언트가 수용할 수 있는 응답의 최대 Age를 설정하는 데 사용된다.
클라이언트가 요청 헤더에 Cache-Control: max-age=60을 포함했다면, 이는 "나이가 60초 이하인 신선한 응답만 받고 싶다"는 의사를 캐시에 전달하는 것이다.
캐시는 자신의 저장소에 있는 응답의 현재 나이($current_age$)를 계산하여, 이 값이 클라이언트가 지정한 초(delta-seconds) 이하일 때만 그 응답을 즉시 내보낼 수 있다.
중요한 점은 별도의 max-stale 지시어가 함께 전달되지 않는 한, 클라이언트는 신선하지 않은(Stale) 응답을 받는 것을 원하지 않는다는 의사가 내포되어 있다는 것이다.
즉, max-age는 캐시된 데이터가 아무리 신선도 수명 내에 있더라도 클라이언트가 정한 기준보다 나이가 많다면 서버와 재검증을 거치도록 강제하는 역할을 한다.
max-age 지시어는 인자 값으로 반드시 토큰(Token) 형식을 사용해야 한다.
max-age=5와 같은 형태만 허용하며, max-age="5"와 같이 큰따옴표를 사용한 quoted-string 형식은 금지한다.
전송자는 어떠한 경우에도 따옴표 형태를 생성해서는 안 된다는 강한 제약(MUST NOT)이 부여되어 있다.
이는 시간 값을 나타내는 delta-seconds가 공백이나 특수문자를 포함하지 않는 단순한 숫자 형태이기 때문에, 파싱의 효율성과 일관성을 위해 가장 단순한 토큰 형식을 강제하는 것이다. 캐시는 이러한 클라이언트의 제안을 바탕으로 신선도 계산 결과를 조정하여 최적의 응답을 선택하게 된다.
5.2.1.2. max-stale
max-stale 요청 지시어는 클라이언트가 신선하지 않은(Stale) 응답을 어느 정도까지 허용할지를 정의한다.
기본적으로 캐시는 신선도 수명이 지난 응답을 클라이언트에게 보내지 않는 것을 원칙으로 하지만, 클라이언트가 max-stale 지시어를 사용하면 이미 만료된 응답이라도 기꺼이 수용하겠다는 의사를 캐시에 알릴 수 있다.
이는 네트워크 상태가 불안정하거나 서버의 응답 속도가 느릴 때, 최신 데이터보다는 빠른 응답을 선호하는 상황에서 유용하다.
이 지시어의 동작 방식은 인자의 유무에 따라 두 가지로 나뉜다.
- 인자 값이 있는 경우 (
max-stale=delta-seconds): 클라이언트는 신선도 수명이 만료된 후, 지정된 초(seconds)를 초과하지 않은 범위 내의 오래된 응답만 수용한다. 예를 들어max-stale=60이라면 만료된 지 1분 이내의 데이터는 허용한다는 뜻이다. - 인자 값이 없는 경우 (
max-stale): 클라이언트는 리소스가 얼마나 오래되었든 상관없이 저장된 모든 오래된 응답을 수용한다.
max-age와 마찬가지로 max-stale 역시 인자 값을 사용할 때 반드시 토큰(Token) 형식을 취해야 한다.
명세는 max-stale=10과 같은 형태만 인정하며, 큰따옴표를 사용한 max-stale="10" 형태의 생성은 엄격히 금지(MUST NOT)한다.
이는 HTTP 캐시 지시어에서 시간 단위의 수치 데이터를 전달할 때 일관되게 적용되는 규칙이다.
5.2.1.3. min-fresh
min-fresh 요청 지시어는 클라이언트가 응답의 향후 신선도를 보장받고 싶을 때 사용하는 지시어다.
min-fresh는 클라이언트가 현재 시점을 기준으로 해당 응답이 최소한 지정된 시간(초) 동안은 계속 신선한 상태(Fresh)를 유지하기를 원한다는 의사를 나타낸다.
즉, 응답을 받은 후에도 일정 시간 동안 서버와 다시 통신하지 않고 캐시를 안전하게 재사용할 수 있는 여유를 확보하려는 목적을 가진다.
이 지시어의 논리적 판단 기준은 다음과 같다.
freshness_lifetime >= current_age + min_fresh
예를 들어, 클라이언트가 min-fresh=60이라고 요청했다면, 캐시는 해당 응답이 현재 신선할 뿐만 아니라 앞으로도 적어도 60초간은 더 신선한 상태로 남아있을 데이터만 즉시 반환할 수 있다.
만약 응답이 지금은 신선하지만 30초 뒤에 만료될 예정이라면, 캐시는 이 응답이 클라이언트의 요구 사항을 충족하지 못한다고 판단하여 서버에 유효성 재검사를 요청해야 한다.
이 지시어 역시 다른 시간 기반 지시어들과 동일한 구문 규칙을 따른다.
반드시 min-fresh=20과 같이 숫자만 사용하는 토큰 형식을 사용해야 한다. min-fresh="20"과 같은 큰따옴표 형태는 허용되지 않으며, 전송자는 이를 생성해서는 안 된다.
결과적으로 min-fresh는 데이터가 만료되기 직전의 응답을 피하고 싶은 클라이언트에게 유용하다.
특히 짧은 시간 안에 동일한 데이터를 반복해서 사용해야 하는 로직을 수행할 때, 중간에 데이터가 만료되어 흐름이 끊기는 것을 방지하는 안전장치 역할을 한다.
5.2.1.4. no-cache
no-cache 요청 지시어는 캐시에 저장된 응답이 있더라도 원본 서버의 유효성 검사(Validation)를 반드시 거치라는 지시어다.
캐시에 저장된 데이터가 비록 신선도 계산상으로는 '신선한(Fresh)' 상태일지라도, 클라이언트가 서버로부터 해당 데이터가 여전히 최신인지 직접 확인받고 싶어 함을 의미한다.
따라서 캐시는 저장소의 응답을 그대로 내보내는 대신, 서버에 조건부 요청을 보내어 304 Not Modified 혹은 200 OK 응답을 받아야만 클라이언트의 요청에 답할 수 있다.
5.2.1.5. no-store
no-store 요청 지시어는 캐시 시스템에게 이 요청의 내용뿐만 아니라 이에 대한 응답의 어떤 부분도 저장해서는 안 된다는 강력한 명령을 전달한다.
이 지시어는 개인용 캐시(브라우저)와 공유 캐시(CDN, 프록시) 모두에 동일하게 적용된다.
"저장해서는 안 된다(MUST NOT store)"는 구체적으로 다음 두 가지 의무를 포함한다.
- 비휘발성 저장소 저장 금지: 캐시는 해당 정보를 하드 디스크와 같은 비휘발성 저장 장치에 의도적으로 기록해서는 안 된다.
- 휘발성 저장소 즉시 삭제: 캐시는 메모리와 같은 휘발성 저장소에 일시적으로 머문 정보에 대해서도, 메시지를 전달(Forwarding)한 후 가능한 한 신속하게 제거하기 위해 최선의 노력을 다해야 한다.
이 지시어의 목적은 민감한 정보가 부주의하게 캐시에 남아 다른 사용자나 공격자에게 노출되는 것을 방지하는 보안 및 개인정보 보호에 있다.
따라서 no-cache가 '재사용 전 확인'에 집중한다면, no-store는 '데이터 흔적 제거'에 집중하는 지시어라고 이해할 수 있다.
5.2.1.6. no-transform
no-transform 요청 지시어는 요청/응답 체인 사이에 있는 프록시나 게이트웨이와 같은 중간 장치들이 메시지 본문의 콘텐츠 형태를 임의로 변환하지 말 것을 요청할 때 사용한다.
HTTP 명세인 RFC 9110 섹션 7.7에서는 중간 장치가 전송 효율을 높이거나 수신 기기의 특성에 맞추기 위해 이미지의 포맷을 변경하거나 해상도를 낮추고, 또는 텍스트 데이터를 압축하는 등의 변환 작업을 수행할 수 있도록 허용하고 있다.
클라이언트가 이 지시어를 요청에 포함하면, 경로상의 모든 중간 장치는 서버로부터 받은 원래의 콘텐츠를 비트 단위까지 보존하여 그대로 전달해야 한다. 이는 데이터의 무결성이 매우 중요하거나, 특정 포맷으로 최적화된 데이터가 중간 장치의 개입으로 인해 훼손되는 것을 방지하고자 할 때 필수적인 안전장치가 된다. 주로 의료 영상 정보, 정밀한 과학적 데이터, 혹은 특정 소프트웨어 패키지처럼 단 하나의 비트 값 변경도 허용되지 않는 리소스를 다룰 때 이 지시어를 통해 데이터 변환을 명시적으로 거부할 수 있다.
5.2.1.7. only-if-cached
only-if-cached 요청 지시어는 클라이언트가 오직 캐시에 저장된 응답만을 받기를 원하며, 원 서버에 접속하여 데이터를 가져오는 과정은 원치 않는다는 의사를 나타낸다.
이 지시어는 주로 네트워크 연결이 불안정하거나 극도로 제한된 상황에서 클라이언트가 서버 대기 시간을 피하고 즉각적인 응답을 얻고자 할 때 사용한다.
만약 적합한 저장된 응답이 있다면 서버를 거치지 않고 즉시 그 데이터를 반환한다. 반면 캐시 저장소에 일치하는 데이터가 없거나 저장된 데이터가 신선하지 않아 재사용할 수 없는 상황이라면, 캐시는 클라이언트에게 504 Gateway Timeout 상태 코드를 응답해야 한다. 이는 클라이언트가 서버로의 추가적인 네트워크 탐색을 원하지 않는다는 명시적인 의사를 표현했기 때문에, 캐시 선에서 요청을 종결하고 결과가 없음을 알리는 표준적인 처리 방식이다.
5.2.2. Response Directives
서버가 응답에 포함하여 캐시 동작을 제어하는 응답 지시어들이다. 요청 지시어가 클라이언트의 희망 사항을 전달하는 권고 성격이었던 것과 달리, 응답 지시어는 캐시가 반드시 준수해야 하는 강제 규칙(MUST)이다.
5.2.2.1. max-age
max-age는 가장 널리 쓰이는 신선도 제어 수단으로, 해당 응답이 생성된 시점부터 몇 초 동안 신선한 상태로 간주될지를 지정한다.
서버가 max-age=delta-seconds를 설정하면, 캐시는 응답의 나이(Age)가 이 값을 초과하는 즉시 해당 응답을 신선하지 않은(Stale) 상태로 분류한다.
예를 들어 max-age=3600으로 설정된 응답은 서버에서 생성된 지 1시간이 지나면 만료된 것으로 간주되어, 이후 요청에 대해서는 원 서버와 유효성 재검증을 거쳐야만 사용할 수 있다.
이 지시어는 Expires 헤더보다 우선순위가 높으며, 정밀한 초 단위 제어를 가능하게 함으로써 캐시의 수명을 명확하게 규정한다.
구문 측면에서는 요청 지시어와 동일하게 숫자만 포함된 토큰 형식을 사용해야 한다.
max-age=5와 같은 형태는 올바르지만, 큰따옴표를 사용한 max-age="5" 형태는 명세상 엄격히 금지된다(MUST NOT).
이는 프로토콜 파싱의 일관성을 유지하고 불필요한 복잡성을 제거하기 위한 조치이다.
5.2.2.2. must-revalidate
must-revalidate 응답 지시어는 캐시된 응답이 신선하지 않은(Stale) 상태가 되었을 때의 재사용 규칙을 매우 엄격하게 규정한다.
일반적인 경우 캐시는 네트워크 단절이나 특정 설정에 따라 오래된 응답을 클라이언트에게 제공할 수 있는 유연성을 가지지만, 이 지시어가 포함된 응답에 대해서는 그러한 예외가 일절 허용되지 않는다.
이 지시어의 핵심은 응답이 만료되는 즉시 반드시(MUST NOT) 원 서버를 통한 유효성 검사를 거쳐야만 다시 사용할 수 있다는 점이다.
특히 캐시가 원 서버와 통신할 수 없는 연결 단절 상태인 경우, 캐시는 오래된 데이터를 내보내는 대신 반드시 에러 응답을 생성해야 한다.
이때 발생하는 상태 코드는 주로 504 Gateway Timeout이 사용된다.
이는 금융 거래와 같이 검증되지 않은 오래된 정보가 처리되었을 때 심각한 오류를 초래할 수 있는 서비스에서 데이터 무결성을 보장하기 위한 필수적인 장치다.
또한 must-revalidate는 인증 정보가 포함된 요청(Authorization header)에 대한 공유 캐시의 동작에도 영향을 준다.
본래 공유 캐시는 보안상의 이유로 인증된 요청의 응답을 함부로 재사용하지 않지만, 이 지시어가 있으면 만료 후 매번 서버의 검증을 받는다는 전제하에 해당 응답을 저장하고 재사용할 수 있는 권한을 얻게 된다(참고: 3.5. Storing Responses to Authenticated Requests).
따라서 서버 개발자는 리소스의 성격을 면밀히 파악해야 한다.
실시간 주식 시세, 수술 중인 환자의 생체 신호 데이터, 혹은 제한된 수량의 티켓 예매 정보처럼 낡은 정보가 전달되었을 때의 리스크가 서비스 중단(에러 발생)보다 크다면 must-revalidate를 사용하는 것이 적절하다.
반면 뉴스 기사나 블로그 포스트처럼 서버가 잠시 점검 중이더라도 조금 오래된 내용을 보여주는 것이 사용자 경험 측면에서 나은 일반적인 콘텐츠의 경우에는 이 지시어를 생략하여 캐시의 유연한 대응 기능을 유지하는 것이 바람직하다.
5.2.2.3. must-understand
must-understand 응답 지시어는 캐시가 해당 응답의 상태 코드(Status Code)에 정의된 요구 사항을 온전히 이해하고 준수할 때만 그 응답을 저장할 수 있도록 제한한다.
이는 HTTP 명세의 호환성과 안정성을 유지하기 위한 안전장치로서, 캐시가 자신이 모르는 새로운 상태 코드나 특수한 처리 로직이 필요한 코드를 임의로 캐싱하여 발생할 수 있는 부작용을 방지하는 역할을 한다.
일반적으로 캐시는 자신이 알지 못하는 상태 코드를 만나면 이를 기본적으로 캐싱 불가능한 것으로 취급하거나, 특정 조건하에 제한적으로 처리한다.
그러나 서버가 must-understand 지시어를 명시하면, 캐시는 해당 상태 코드의 의미와 관련 제약 사항을 명확히 구현하고 있는 경우에만 이를 저장소에 보관할 수 있다.
만약 캐시가 해당 상태 코드를 이해하지 못한다면, 설령 다른 지시어(예: max-age)가 캐싱이 가능함을 시사하더라도 이를 무시하고 응답을 저장해서는 안 된다.
이 지시어는 주로 표준화된 지 얼마 되지 않은 새로운 HTTP 상태 코드를 전송하거나, 특정 상태 코드에 대해 매우 엄격한 캐싱 규칙을 적용해야 할 때 유용하다. 이를 통해 서버는 자신의 의도와 다르게 동작할 수 있는 오래된 프록시나 캐시 계층으로부터 리소스를 보호하고, 최신 프로토콜을 정확히 이해하는 캐시들만 데이터를 효율적으로 관리하도록 통제할 수 있다.
5.2.2.4. no-cache
no-cache 응답 지시어는 크게 인자가 없는 기본 형태와 특정 헤더 필드 이름을 인자로 갖는 한정적 형태로 나뉜다.
이 지시어의 핵심은 이름과 달리 저장을 금지하는 것이 아니라, 저장된 데이터를 사용하기 전에 반드시 원 서버에 확인을 받도록 강제하는 데 있다.
인자가 없는 기본 형태의 no-cache는 해당 응답을 저장소에 보관할 수는 있지만, 원 서버와의 성공적인 유효성 재검증 과정을 거치지 않고는 다른 요청에 재사용해서는 안 된다는 강력한 지시를 담고 있다.
이는 4.3. Validation에서 정의한 유효성 검사 메커니즘을 반드시 통과해야 함을 의미하며, 설령 캐시가 오래된 응답을 보내도록 설정되어 있더라도 이 지시어가 있으면 반드시 원 서버에 접속하여 데이터의 최신 여부를 확인해야 한다.
반면 하나 이상의 헤더 필드 이름을 인자로 갖는 한정적 형태의 no-cache는 조금 더 세밀한 제어를 가능하게 한다.
이 지시어가 사용되면 캐시는 인자로 나열된 특정 헤더 필드들을 제외한 나머지 응답 부분은 재사용할 수 있다.
인자로 지정된 헤더 필드들은 후속 응답에서 제거되거나, 원 서버와 성공적인 재검증을 통해 업데이트 혹은 제거되어야만 클라이언트에게 전달될 수 있다.
이를 통해 서버는 응답의 본문이나 대부분의 헤더는 캐싱을 허용하면서도, 보안이나 개인정보와 관련된 특정 헤더 필드만은 매번 새롭게 처리하도록 강제할 수 있다.
구문 규칙에 따르면 이 지시어의 인자는 반드시 큰따옴표로 감싸진 문자열(quoted-string) 형식을 사용해야 한다. 인자가 하나뿐이라서 따옴표가 굳이 필요 없어 보이는 경우라도 토큰 형식을 생성해서는 안 된다는 권고(SHOULD NOT)가 명시되어 있다. 또한 인자로 지정되는 헤더 필드 이름은 대소문자를 구분하지 않으며, 이 명세에서 정의되지 않은 임의의 헤더 필드도 포함될 수 있다.
다만 실제 구현 환경에서는 한계가 존재한다.
명세의 주석에 언급된 것처럼, 인자가 있는 한정적 형태의 no-cache는 많은 캐시 구현체에서 인자가 없는 기본 형태와 동일하게 취급된다.
즉, 특정 헤더만 제외하고 재사용하는 정교한 처리가 널리 구현되어 있지 않으므로, 캐시는 대개 전체 응답에 대해 재검증을 시도하게 된다.
5.2.2.5. no-store
no-store 응답 지시어는 캐시가 해당 요청과 응답의 그 어떤 부분도 저장해서는 안 되며, 이후의 다른 요청을 처리하기 위해 해당 응답을 재사용해서도 안 된다는 가장 강력한 제약 사항을 규정한다.
이 지시어는 개인용 캐시(브라우저)와 공유 캐시(프록시, CDN) 모두에 예외 없이 적용된다.
여기서 저장하지 말아야 한다는 명령(MUST NOT store)은 구체적으로 두 가지 기술적 의무를 포함한다.
- 비휘발성 저장소 저장 금지: 캐시는 수신한 정보를 하드 디스크와 같은 비휘발성 저장 장치에 의도적으로 기록해서는 안 된다.
- 휘발성 저장소 즉시 삭제: 데이터를 전달(Forwarding)한 후에는 메모리와 같은 휘발성 저장소에 일시적으로 남아 있는 정보조차도 가능한 한 신속하게 제거해야 한다.
이는 데이터가 서버와 클라이언트 사이를 통과한 직후 캐시 시스템 내부에서 즉시 파기되어야 함을 의미한다.
그러나 이 지시어가 프라이버시를 보장하기 위한 완벽하거나 충분한 수단은 아니다.
악의적인 의도를 가진 캐시나 해킹된 캐시는 이 지시어를 무시하고 데이터를 몰래 저장할 수 있으며, 통신 네트워크 자체가 도청에 취약할 수도 있기 때문이다.
따라서 no-store는 정상적인 캐시 시스템에 대한 통제 수단일 뿐, 보안의 모든 문제를 해결하는 근본적인 장치는 아니다.
특이사항으로, 앞서 살펴본 must-understand 지시어와 함께 사용될 경우 특정 상황에서 no-store의 동작이 영향을 받을 수 있다.
캐싱 시스템에는 두 가지 큰 대원칙이 존재한다.
첫째는 프로토콜의 무결성이다. 즉, 캐시가 이해하지 못하는 상태 코드나 로직을 마음대로 처리해서는 안 된다는 원칙이다.
둘째는 데이터의 보안이다. 민감한 정보는 어디에도 남기지 말아야 한다는 원칙이다.
no-store는 데이터 보안을 위한 지시어다. 반면 must-understand는 프로토콜 무결성을 위한 지시어다. 이 두 지시어가 부딪힐 때 RFC 9111은 프로토콜의 무결성인 must-understand에 우선권을 부여한다.
만약 서버가 캐시가 한 번도 본 적 없는 새로운 상태 코드(예: 미래에 정의될 299 상태 코드)를 보내면서 no-store와 must-understand를 동시에 설정했다고 가정했을 때, 캐시는 먼저 상태 코드를 확인한다.
must-understand가 있으므로, 이 상태 코드의 의미와 제약 조건을 명확히 알고 있는지 확인한다.
캐시가 이 코드를 모른다면, 응답에 포함된 no-store라는 지시어조차도 온전히 신뢰할 수 없는 상태가 된다. 왜냐하면 해당 상태 코드의 정의 안에 no-store의 동작을 변형시키거나 무시하게 만드는 또 다른 규칙이 숨어있을지 모르기 때문이다.
결과적으로 캐시는 no-store 때문이 아니라, must-understand 규칙에 의해 이 응답을 저장소에 기록하지 않는다. 명세에서 override라는 표현을 쓴 이유는, 저장을 하지 않는 근본적인 원인이 데이터의 민감성(no-store) 이전에 프로토콜의 불확실성(must-understand)에 있기 때문이다.
이러한 설계는 미래의 확장성을 고려한 것이다.
만약 캐시가 상태 코드를 이해하지 못한 채 단순히 no-store만 보고 동작했다가, 나중에 그 상태 코드가 사실은 아주 특수한 형태의 공유 캐싱을 허용하는 코드였다는 것이 된다면 프로토콜 전체의 일관성이 깨질 수 있다.
따라서 캐시가 상태 코드를 이해하지 못하는 시점에서는 그 응답 안에 담긴 no-store를 포함한 그 어떤 지시어도 유효하게 처리될 기반이 없다고 간주하는 것이다.
5.2.2.6. no-transform
no-transform 응답 지시어는 서버가 클라이언트에게 응답을 보낼 때, 경로상에 존재하는 모든 중간 장치(프록시, 게이트웨이 등)가 메시지 본문의 내용을 어떠한 형태로도 변형하지 말 것을 강제하는 명령이다.
이는 해당 중간 장치가 캐시 기능을 구현하고 있는지 여부와 상관없이 반드시 준수해야 하는 의무 사항이다.
이 지시어는 RFC 9110 섹션 7.7에서 정의된 콘텐츠 변환(Transformation) 행위를 금지한다.
일반적으로 중간 장치들은 네트워크 대역폭을 절약하거나 수신 기기의 성능에 맞추기 위해 이미지의 포맷을 변환(예: PNG를 WebP로 변경)하거나 해상도를 조절하고, 텍스트나 바이너리 데이터를 재압축하는 등의 작업을 수행할 수 있다.
하지만 서버가 no-transform을 명시하면 이러한 최적화 작업이 일절 금지되며, 서버가 생성한 원래의 비트 스트림이 클라이언트에게 그대로 전달되어야 한다.
이 지시어가 응답에 사용되는 주요 목적은 데이터의 정밀도와 무결성을 보존하는 데 있다. 예를 들어, 아주 미세한 픽셀의 변화도 진단 결과에 영향을 줄 수 있는 의료용 방사선 사진이나, 단 1비트의 데이터 변형으로도 실행이 불가능해지는 소프트웨어 바이너리 파일, 혹은 특수한 인코딩 방식이 적용된 과학 데이터 등을 전송할 때 필수적이다. 이를 통해 서버는 네트워크 경로상의 자동화된 최적화 도구들로부터 리소스의 원본 상태를 안전하게 보호할 수 있다.
5.2.2.7. private
private 응답 지시어는 해당 응답이 특정 단일 사용자만을 위한 것이며, 공유 캐시에는 저장되어서는 안 된다는 것을 명시한다.
이 지시어는 인자가 없는 기본 형태(unqualified)와 특정 헤더 필드 목록을 인자로 갖는 한정적 형태(qualified)로 구분된다.
- 인자가 없는 기본 형태: CDN이나 프록시 서버와 같은 공유 캐시가 해당 응답을 저장하는 것을 엄격히 금지한다. 반면, 사용자의 브라우저와 같은 개인용 캐시(private cache)는 3. Storing Responses in Caches의 제약 조건을 만족한다면 해당 응답을 저장할 수 있다. 특히 이 지시어는 본래 경험적 캐싱(heuristic caching)이 불가능한 응답이라 할지라도 개인용 캐시에서는 저장할 수 있도록 허용하는 신호로도 작용한다.
- 한정적 형태: 응답 전체가 아닌 특정 부분에 대해서만 공유를 제한한다. 이 경우 공유 캐시는 인자로 나열된 특정 헤더 필드들을 제외한 나머지 응답 부분은 저장할 수 있다. 나열된 헤더 필드들은 오직 해당 요청을 보낸 단일 사용자에게만 유효한 정보로 간주되어 공유 저장소에서 누락되어야 한다.
필드 이름은 대소문자를 구분하지 않으며 표준에 정의되지 않은 임의의 헤더 이름도 사용할 수 있다.
구문 규칙에 따르면 인자 값은 반드시 큰따옴표로 감싸진 문자열(quoted-string) 형식을 사용해야 한다. 인자가 하나뿐이라 따옴표가 필요 없어 보이는 상황이라도 토큰 형식을 생성하지 않아야 한다는 권고가 적용된다.
다만 명세는 두 가지 중요한 한계점을 지적한다.
첫째, private이라는 단어의 사용은 오직 응답이 어디에 저장될 수 있는지를 제어할 뿐, 메시지 내용 자체의 프라이버시나 기밀성을 보장하는 기술적 수단이 아니라는 점이다.
둘째, 실무적으로는 많은 캐시 구현체가 인자가 있는 한정적 형태를 인자가 없는 기본 형태와 동일하게 취급한다. 즉, 특정 헤더만 제외하고 나머지를 공유 캐시에 저장하는 정교한 기능은 널리 보급되어 있지 않으므로 대부분의 공유 캐시는 이 지시어를 보는 즉시 전체 응답의 저장을 포기한다.
5.2.2.8. proxy-revalidate
proxy-revalidate 응답 지시어는 응답이 신선하지 않은(Stale) 상태가 되었을 때, 오직 **공유 캐시(Shared Cache)**에 대해서만 엄격한 재검증 의무를 부여한다.
이는 앞서 살펴본 must-revalidate와 유사한 논리로 작동하지만, 그 적용 범위가 개인 캐시가 아닌 공유 캐시 계층으로 한정된다는 점이 차이점이다.
이 지시어가 포함된 응답이 만료되면, 공유 캐시는 4.3. Validation에서 정의한 원 서버와의 성공적인 유효성 재검증 과정을 거치기 전까지 해당 응답을 다른 요청에 절대 재사용해서는 안 된다. 반면 개인용 캐시는 이 지시어의 영향을 받지 않으므로, 네트워크 단절 상황 등에서 만료된 데이터를 제공하도록 설정되어 있다면 클라이언트에게 오래된 응답을 그대로 보여줄 수도 있다.
또한 proxy-revalidate 지시어 자체가 해당 응답이 캐싱 가능하다는 것을 의미하지는 않는다는 점을 유의해야 한다.
예를 들어 이 지시어는 public 지시어와 함께 사용되어, 본래 공유 캐시가 저장할 수 없는 성격의 응답을 저장할 수 있게 허용함과 동시에, 만료 시 반드시 서버의 확인을 받도록 강제하는 방식으로 조합될 수 있다.
즉, 이 지시어는 응답의 저장 가능 여부가 아니라 저장된 이후 만료된 시점에서의 재사용 정책을 제어하는 수단이다.
5.2.2.9. public
public 응답 지시어는 해당 응답이 본래 캐싱이 금지될 만한 요소를 포함하고 있더라도, 3. Storing Responses in Caches의 제약 조건에 따라 캐시가 이를 명시적으로 저장할 수 있음을 나타낸다.
즉, 이 지시어는 해당 응답이 캐싱 가능하다는 사실을 외부로 명확히 공표하는 역할을 수행한다.
가장 대표적인 활용 사례는 Authorization 헤더가 포함된 요청에 대한 응답이다.
3.5. Storing Responses to Authenticated Requests에 따르면, 인증 정보가 포함된 요청의 응답은 보안을 위해 공유 캐시에 저장되지 않는 것이 기본 원칙이다.
그러나 서버가 응답에 public 지시어를 포함하면, 공유 캐시는 해당 응답이 여러 사용자에게 공유되어도 안전하다고 판단하여 이를 저장하고 재사용할 수 있는 권한을 얻게 된다.
명세에서는 이미 3. Storing Responses in Caches의 기준에 따라 캐싱이 가능한 응답의 경우, 굳이 public 지시어를 추가할 필요가 없음을 명시하고 있다.
즉, 일반적으로 public 한 성격을 가진 리소스에는 필수 사항이 아니다.
또한 public 지시어가 포함된 응답에 max-age나 Expires와 같은 명시적인 신선도 정보가 부족하더라도, 캐시는 4.2.2. Calculating Heuristic Freshness에 정의된 알고리즘을 사용하여 경험적 캐싱(Heuristic Caching)을 수행할 수 있다.
이는 서버가 캐싱을 적극적으로 권장하지만 구체적인 만료 시점을 지정하지 않았을 때 캐시가 자체적으로 수명을 판단할 수 있는 근거가 된다.
5.2.2.10. s-maxage
s-maxage 응답 지시어는 오직 공유 캐시를 제어하기 위한 지시어다.
이 지시어는 개인 캐시에는 아무런 영향을 주지 않으면서, 공유 캐시 계층에서만 독립적인 만료 정책을 적용할 때 사용한다.
s-maxage의 가장 큰 특징은 공유 캐시 내에서 max-age 지시어나 Expires 헤더가 정의한 값을 완전히 덮어쓴다는(Override) 점이다.
예를 들어 Cache-Control: max-age=3600, s-maxage=600과 같이 설정되어 있다면, 사용자의 브라우저는 해당 응답을 1시간(3600초) 동안 신선하다고 판단하지만, 중간에 위치한 CDN 캐시는 오직 10분(600초)만 신선한 상태로 간주한다. 이를 통해 서버 개발자는 최종 사용자의 브라우저에는 데이터를 오래 머물게 하면서도, 중간 유통망인 공유 캐시는 더 자주 업데이트되도록 정교하게 제어할 수 있다.
또한 s-maxage는 5.2.2.8. Proxy Revalidate의 proxy-revalidate 의미를 내포한다.
즉, s-maxage로 지정된 시간이 지나 응답이 만료(Stale)되면, 공유 캐시는 반드시 원 서버와 성공적인 유효성 재검증을 거치기 전까지 해당 응답을 재사용해서는 안 된다.
아울러 이 지시어는 Authorization 헤더가 포함된 요청의 응답을 공유 캐시가 저장하고 재사용할 수 있도록 허용하는 권한을 부여하며, 이 경우에도 지정된 시간 경과 후에는 반드시 서버의 확인을 받아야 한다.
구문 규칙은 다른 시간 기반 지시어들과 동일하게 숫자 형태의 토큰 형식을 강제한다.
s-maxage=10과 같은 표현은 허용되지만, 큰따옴표를 사용한 s-maxage="10" 형태의 생성은 엄격히 금지된다.
5.2.3. Extension Directives
HTTP 캐싱 시스템이 미래의 요구 사항에 맞춰 기능을 확장할 수 있도록 설계된 확장 지시어(Extension Directives) 메커니즘을 정의한다. 이 구조의 핵심은 새로운 기능이 추가되더라도 기존에 배포된 수많은 캐시 장치들이 오작동하지 않고 안전하게 동작할 수 있도록 보장하는 하위 호환성(Backward Compatibility)에 있다.
명세는 알 수 없는 캐시 지시어를 만났을 때 캐시가 반드시 이를 무시해야 한다고 규정한다. 확장은 크게 두 가지 방식으로 나뉜다.
- 정보성 확장(Informational extensions): 캐시의 동작 자체를 바꿀 필요가 없는 부가 정보를 전달한다.
- 동작 확장(Behavioral extensions): 기존 지시어의 동작 방식을 수정하거나 보완하는 역할을 한다. 동작 확장은 기존 지시어와 새 지시어를 동시에 제공하는 방식을 취한다. 이를 통해 새 지시어를 이해하는 최신 캐시는 수정된 규칙을 따르고, 이해하지 못하는 구형 캐시는 기존 지시어의 기본 동작을 따르게 되어 시스템 전체가 깨지지 않고 유지된다.
명세에서 제시하는 가상의 community="UCI" 예시는 이러한 동작 원리를 잘 보여준다.
서버가 Cache-Control: private, community="UCI"라고 응답하면, 이 확장 지시어를 모르는 캐시는 단순히 private 지시어만 보고 공유 캐시에 저장하지 않는다. 반면 community 지시어를 이해하는 캐시는 private의 범위를 UCI 커뮤니티 내부의 공유 캐시까지로 넓혀서 해석하고 데이터를 저장할 수 있다.
새로운 확장 지시어를 정의할 때는 다음과 같은 설계 원칙을 고려해야 한다.
- 동일한 지시어가 여러 번 나타날 때의 처리 방식
- 인자가 없는 지시어에 인자가 붙어 있거나, 인자가 필요한데 누락된 경우의 동작
- 해당 지시어가 요청 전용인지, 응답 전용인지, 혹은 양쪽 모두에서 사용 가능한지 여부
이러한 가이드라인은 HTTP 캐싱 프로토콜이 시간이 흐름에 따라 새로운 기술적 요구를 수용하면서도 전 세계적인 네트워크 인프라의 안정성을 해치지 않도록 돕는 토대가 된다.
5.2.4. Cache Directive Registry
HTTP 캐시 지시어의 이름 공간(Namespace)을 체계적으로 관리하기 위한 등록소(Registry) 운영 규칙을 정의한다. 이는 전 세계적으로 사용되는 HTTP 프로토콜에서 지시어 이름이 중복되거나 무분별하게 생성되어 발생할 수 있는 혼란을 방지하고, 모든 지시어가 명확한 기술적 근거를 갖도록 보장하는 데 목적이 있다.
모든 캐시 지시어는 IANA(Internet Assigned Numbers Authority)에서 관리하는 특정 페이지에 등록되어야 하며, 새로운 지시어를 이 이름 공간에 추가하기 위해서는 반드시 다음과 같은 항목들을 포함하여 등록 절차를 밟아야 한다.
- 캐시 지시어 이름(Cache Directive Name): 사용될 고유한 명칭
- 명세서 텍스트 포인터(Pointer to specification text): 해당 지시어의 동작 방식과 구문을 상세히 정의한 공식 문서 위치
특히 이 이름 공간에 새로운 값을 추가하기 위해서는 RFC 8126 섹션 4.8에서 규정한 'IETF Review' 과정을 반드시 거쳐야 한다. 이는 단순히 이름을 등록하는 것을 넘어, 인터넷 엔지니어링 태스크 포스(IETF) 내의 전문가들이 해당 지시어의 필요성, 보안 영향도, 기존 프로토콜과의 호환성 등을 면밀히 검토하고 승인하는 엄격한 절차가 뒷받침되어야 함을 의미한다.
이러한 중앙 집중식 관리와 엄격한 검토 절차 덕분에 개발자들은 IANA 등록소를 참조하여 현재 표준으로 인정받는 지시어들이 무엇인지, 그리고 각 지시어가 어떤 공식 명세에 기반하여 설계되었는지 신뢰할 수 있는 정보를 얻을 수 있다.
5.3. Expires
Expires 응답 헤더 필드는 해당 응답이 신선하지 않은 상태(Stale)로 간주되는 절대적인 날짜와 시간을 지정한다.
이는 HTTP/1.0 시대부터 존재해 온 전통적인 신선도 제어 방식이며, 4.2. Freshness에서 설명하는 신선도 모델의 핵심 구성 요소 중 하나다.
Expires 헤더에 지정된 시간은 해당 리소스가 그 시점에 반드시 변경되거나 삭제됨을 의미하지는 않는다.
다만 캐시가 서버에 재검증을 요청하지 않고 해당 데이터를 즉시 사용할 수 있는 유효 기간을 알려주는 역할을 할 뿐이다.
필드 값은 RFC 9110 섹션 5.6.7에서 정의한 HTTP-date 형식을 따라야 하며, Expires: Thu, 01 Dec 1994 16:00:00 GMT와 같이 절대적인 타임스탬프로 기술된다.
현대적인 캐싱 시스템에서의 우선순위는 매우 명확하다. 만약 응답에 Cache-Control 헤더의 max-age 지시어가 포함되어 있다면, 캐시는 반드시 Expires 헤더를 무시해야 한다.
마찬가지로 공유 캐시가 s-maxage 지시어를 발견한 경우에도 Expires는 무시된다.
이러한 설계는 상대적인 시간(초 단위)을 다루는 max-age 계열이 서버와 클라이언트 간의 시계 불일치 문제에 더 강건하기 때문에 도입된 규칙이며, Expires는 Cache-Control을 이해하지 못하는 구형 캐시를 위한 하위 호환성 용도로 남겨져 있다.
구문 처리와 관련하여, 캐시는 잘못된 날짜 형식이나 "0"과 같은 값을 수신하면 이를 이미 만료된 과거의 시간으로 해석해야 한다. 또한 시계(Clock)가 없는 원 서버는 과거의 고정된 시간(항상 만료됨)을 보내는 경우가 아니라면 Expires 헤더를 생성해서는 안 된다.
역사적으로 HTTP 명세는 Expires의 값을 미래로 1년 이상 설정하지 않도록 권장해 왔다. 현재는 1년 이상의 긴 수명을 금지하지는 않으나, 32비트 정수형 시간 표현의 오버플로 문제 등으로 인해 너무 큰 값을 설정하면 시스템 오류를 유발할 수 있다.
대다수의 캐시는 설정된 수명이 아무리 길더라도 그보다 훨씬 일찍 저장소에서 응답을 제거(Evict)할 수 있다는 점을 유의해야 한다.
5.4. Pragma
Pragma 요청 헤더 필드는 HTTP/1.1의 Cache-Control이 등장하기 이전인 HTTP/1.0 환경에서 클라이언트가 캐시되지 않은 응답을 요청하기 위해 사용했던 유산이다.
당시에는 현재의 표준적인 캐시 제어 수단이 없었기에 클라이언트는 Pragma: no-cache를 통해 중간 캐시가 저장된 데이터를 그대로 내보내지 말고 원 서버에 확인하도록 요청했다.
현대 인터넷 환경에서는 HTTP/1.1 이상을 지원하는 Cache-Control이 보편화되었으며 모든 주요 캐시 시스템이 이를 완벽히 구현하고 있다.
이에 따라 RFC 9111 명세는 Pragma 헤더를 공식적으로 지원 중단(Deprecated) 처리했다.
특히 주의해야 할 점은 응답(Response) 헤더로서의 Pragma: no-cache다.
명세의 주석에 따르면 응답에 포함된 Pragma: no-cache는 HTTP 표준에서 그 의미가 단 한 번도 공식적으로 정의된 적이 없다.
따라서 응답 헤더에 이 값을 포함하는 것은 Cache-Control: no-cache를 대체할 수 있는 신뢰할 수 있는 방법이 되지 못한다.
현대의 웹 서버와 클라이언트는 호환성을 유지하기 위해 여전히 이 헤더를 처리하기도 하지만, 새로운 시스템을 설계할 때는 오직 Cache-Control만을 사용해야 한다.
5.5. Warning
Warning 헤더 필드는 과거 메시지의 상태나 콘텐츠 변환에 관한 부수적인 정보를 전달하기 위해 사용되었다.
HTTP 상태 코드만으로는 온전히 표현할 수 없는 미묘한 경고 사항(예: 응답이 신선하지 않음, 콘텐츠가 중간 장치에 의해 변형됨 등)을 숫자로 된 경고 코드와 텍스트 형태로 담아내는 역할을 수행했다.
그러나 Warning 헤더를 공식적으로 폐기(Obsolete) 처리했다.
그 이유는 실제 운영 환경에서 이 헤더가 널리 생성되지 않았을뿐더러, 브라우저나 애플리케이션 등 최종 사용자에게 이 정보가 제대로 노출되거나 활용되는 경우가 드물었기 때문이다.
현대의 HTTP 통신에서는 Warning 헤더가 제공하던 정보들을 Age, Cache-Control, 혹은 본문의 메타데이터와 같은 다른 헤더 필드들을 조사함으로써 충분히 파악할 수 있다.
따라서 새로운 시스템을 구현하는 개발자나 운영자는 더 이상 Warning 헤더를 생성할 필요가 없으며, 수신측 역시 이 헤더의 존재 여부에 의존하여 로직을 설계하지 않는 것이 권장된다.
이는 프로토콜의 복잡성을 줄이고 실질적으로 활용되는 헤더들에 집중하기 위한 표준화 과정의 일환이다.
5.6. Relationship to Applications and Other Caches
본 명세의 핵심은 HTTP 캐시 표준이 데이터가 네트워크를 타고 들어온 이후, 즉 애플리케이션 내부에서 어떻게 사용되는지까지는 완격하게 강제하지 않는다는 점에 있다. HTTP 캐싱 명세(RFC 9111)와 웹 브라우저 같은 애플리케이션 내부 로직 사이의 관계는 네트워크 규약과 사용자 경험(UX) 사이의 타협점으로 이해할 수 있다.
가장 구체적인 예시는 브라우저의 뒤로 가기 버튼이다.
사용자가 특정 웹페이지를 방문한 뒤 다른 페이지로 이동했다가 다시 뒤로 가기를 눌렀을 때, 브라우저는 해당 페이지를 서버에서 다시 가져오거나 네트워크 캐시를 확인하지 않고 단순히 이전 화면 상태를 그대로 보여주는 경우가 많다.
이때 해당 리소스의 신선도 수명(max-age)이 이미 끝났더라도 브라우저는 히스토리 메커니즘을 우선시하여 만료된 데이터를 그대로 화면에 띄울 수 있다.
이는 HTTP 명세의 신선도 규칙을 어기는 것이 아니라, 애플리케이션이 사용자 편의를 위해 표준 캐시 레이어 위에서 별도의 히스토리 캐시를 운영하는 사례로 본다.
또 다른 예로 동일한 페이지 안에서 같은 이미지가 여러 번 쓰이는 상황을 들 수 있다.
만약 서버가 해당 이미지에 Cache-Control: no-store 지시어를 보냈다면, 이론적으로는 어떤 저장소에도 기록하지 말아야 한다.
하지만 브라우저가 하나의 페이지를 렌더링하는 짧은 순간 동안 그 이미지를 메모리에 잠시 들고 있다가 중복된 위치에 뿌려주는 것은 허용된다.
명세는 이를 동일한 요청과 직접적으로 연관된 범위 내의 재사용으로 보며, 콘텐츠 제작자가 예상하는 캐싱 의미론을 크게 해치지 않는 합리적인 수준으로 간주한다.
반면, 만약 브라우저가 no-store가 설정된 데이터를 사용자가 브라우저를 껐다 킨 후에도 기억하고 있다가 전혀 다른 사이트에서 재사용한다면, 이는 콘텐츠 제작자에게 큰 혼란을 주는 행위가 된다.
RFC 9111은 이러한 예기치 못한 동작을 방지하기 위해, 애플리케이션이 사용자에게 보이지 않는 곳에서 데이터를 캐싱할 때는 가급적 HTTP 캐시 지시어를 존중하도록 강력히 권고하고 있다.
결국 HTTP 캐싱 명세는 데이터가 유통되는 규칙을 정하는 것이고, 애플리케이션은 그 데이터를 받은 뒤에 자신의 목적(빠른 화면 전환 등)을 위해 표준 규칙보다 조금 더 유연하게 데이터를 다룰 수 있는 여지를 가진다는 뜻이다. 다만 그 유연함이 개발자가 의도한 보안 정책이나 데이터 갱신 정책을 완전히 무너뜨릴 정도로 과해서는 안 된다는 것이 이 섹션의 핵심 권고 사항이다.
7. Security Considerations
HTTP 캐싱에 특화된 보안 고려 사항을 다룬다. 캐시는 요청이 완료된 이후에도 데이터를 보관하기 때문에, 네트워크 통신이 종료된 시점보다 훨씬 나중에 정보가 유출될 수 있는 잠재적인 공격 표면(Attack Surface)을 제공한다. 따라서 명세는 캐시된 내용 자체를 보호해야 할 민감한 정보로 취급할 것을 강조한다.
특히 개인용 캐시(Private Cache)는 단일 사용자의 데이터만 보관하지만, 거꾸로 이를 분석하면 해당 사용자의 활동 내역을 재구성하는 도구로 악용될 수 있다. 이는 사용자의 사생활 침해로 이어질 수 있으므로, 브라우저와 같은 사용자 에이전트는 사용자가 특정 서버 혹은 전체 서버의 저장된 응답을 직접 삭제할 수 있는 제어 기능을 반드시 제공해야 한다.
캐시는 정보 제공자와 사용자 모두에게 유용한 도구이지만, 데이터가 영속적으로 남는다는 특성 때문에 보안 관점에서는 항상 주의 깊게 다뤄져야 한다.
7.1. Cache Poisoning
캐시 중독(Cache Poisoning) 공격의 위험성을 경고한다. 캐시 중독은 공격자가 구현상의 결함이나 권한 상승 등의 수단을 동원해 악성 응답을 캐시에 강제로 삽입하는 행위를 말한다. 캐시의 특성상 한 번 저장된 데이터는 여러 사용자에게 반복적으로 제공되므로, 공격자는 단 한 번의 성공으로 수많은 클라이언트에게 악성 콘텐츠를 유포할 수 있는 강력한 전파력을 얻게 된다.
이 공격은 특히 전 세계 수많은 사용자가 거쳐 가는 CDN이나 프록시와 같은 공유 캐시에서 매우 치명적이다. 공격자가 조작된 응답을 공유 캐시에 심는 데 성공하면, 해당 리소스를 요청하는 모든 사용자는 원 서버가 보낸 정상적인 데이터 대신 공격자가 의도한 악성 스크립트나 가짜 정보를 수신하게 된다. 이는 단순한 정보 유출을 넘어 사이트 전체의 신뢰도를 파괴하고 사용자 기기를 감염시키는 통로가 된다.
명세는 주요 공격 벡터 중 하나로 프록시와 사용자 에이전트 간의 메시지 파싱 방식 차이를 이용한 기법을 언급한다. RFC 9112(기존 RFC 7230 대체) 섹션 6.3 등에서 규정하는 메시지 전송 규칙을 구현하는 과정에서, 중간 장치들이 요청이나 응답을 서로 다르게 해석하는 취약점이 발생할 수 있다. 예를 들어, HTTP 요청 스머글링(Request Smuggling)과 같은 기법을 통해 캐시 서버가 특정 요청에 대한 응답을 엉뚱한 요청의 결과로 저장하게 만듦으로써 캐시를 오염시킨다.
따라서 캐시 시스템을 설계하고 운영할 때는 메시지 파싱의 엄격한 표준 준수가 필수적이며, 캐시 키(Cache Key) 구성 요소에 대한 철저한 검증이 요구된다.
7.2. Timing Attacks
캐시를 통한 정보 유출, 특히 타이밍 공격(Timing Attacks)의 위험성을 경고한다. 캐시의 주된 목적은 성능 최적화이지만, 이 과정에서 리소스가 이전에 요청되었는지 여부가 외부에 노출될 수 있다는 부작용이 존재한다.
예를 들어, 사용자가 특정 사이트(사이트 A)를 방문하여 브라우저에 캐시를 남긴 후, 전혀 다른 두 번째 사이트(사이트 B)로 이동했다고 가정한다. 이때 사이트 B는 사이트 A에서 사용되었을 법한 리소스를 로드해봄으로써 사용자의 방문 기록을 추측할 수 있다. 만약 해당 리소스가 매우 빠르게 로드된다면, 사이트 B는 사용자가 이전에 사이트 A 혹은 그 특정 페이지를 방문했었다고 판단할 수 있다. 이는 사용자의 사생활과 방문 경로가 제3자에게 노출되는 결과를 초래한다.
이러한 타이밍 공격을 완화하기 위한 기술적 수단으로 캐시 키(Cache Key)에 더 많은 정보를 추가하는 방식이 사용된다. 단순히 요청된 리소스의 URL만 캐시 키로 사용하는 것이 아니라, 해당 리소스를 요청한 참조 사이트(Referring Site)의 식별 정보를 함께 묶어 저장하는 방식이다. 이를 흔히 더블 키잉(Double Keying) 또는 **캐시 파티셔닝(Cache Partitioning)**이라고 부른다.
더블 키잉이 적용되면, 사이트 A에서 저장된 캐시는 사이트 B에서 요청할 때 동일한 URL이라 하더라도 캐시 키가 다르기 때문에(상위 도메인 정보가 다름) 재사용되지 않는다. 결과적으로 사이트 B는 사용자가 이전에 사이트 A를 방문했는지 여부를 로딩 속도 차이로 알아낼 수 없게 된다. 이는 캐시 효율성을 다소 희생하더라도 사용자의 프라이버시 보호를 우선시하는 현대 브라우저들의 일반적인 설계 방향이다.
7.3. Caching of Sensitive Information
캐싱 동작에 대한 오해나 구현상의 결함이 민감한 정보의 유출로 이어지는 보안 위험을 경고한다. 시스템 설계자가 캐시의 작동 방식을 정확히 파악하지 못한 상태에서 서비스를 배포하면, 본래 비공개로 유지되어야 할 인증 자격 증명이나 개인 정보가 캐시 서버에 저장되어 권한이 없는 제3자에게 노출될 수 있다.
가장 흔한 오해 중 하나는 Set-Cookie 응답 헤더가 포함되어 있으면 캐시가 자동으로 저장을 하지 않을 것이라고 생각하는 점이다.
그러나 RFC 9111 명세는 Set-Cookie 헤더 필드가 캐싱을 억제하지 않는다고 명확히 규정한다.
즉, 캐시 가능한 응답에 Set-Cookie 헤더가 포함되어 있다면, 캐시는 이를 저장했다가 이후 발생하는 다른 요청을 처리하는 데 재사용할 수 있다.
이러한 특성 때문에 특정 사용자의 로그인 세션 정보가 담긴 쿠키가 공유 캐시(CDN, 프록시 등)에 저장될 위험이 있다. 만약 공유 캐시가 이 응답을 저장한 뒤 다른 사용자의 요청에 대해 동일한 응답을 내보낸다면, 두 번째 사용자는 첫 번째 사용자의 세션 쿠키를 가로채게 되는 셈이다. 이는 서비스의 보안을 심각하게 위협하는 요소가 된다.
따라서 민감한 쿠키 정보를 포함하는 응답을 전송하는 서버는 반드시 적절한 Cache-Control 응답 헤더를 명시적으로 제공해야 한다.
명세는 서버가 이러한 응답의 캐싱을 제어하기 위해 private 지시어나 no-store 지시어를 포함할 것을 강력히 권고한다.
private 지시어를 사용하면 응답이 특정 사용자만을 위한 것임을 나타내어 공유 캐시에 저장되는 것을 방지할 수 있고, no-store를 사용하면 어떤 형태의 캐시 저장소에도 정보가 남지 않도록 강제할 수 있다. 결국 보안의 책임은 Set-Cookie의 기본 동작에 의존하는 것이 아니라, 서버가 캐시 지시어를 통해 명확한 지침을 내리는 데 있다.