사전 RFC 9111
표준

RFC 9111

gabury1고친 사람 github-actions[bot]

RFC 9111 은 웹 캐시가 받은 응답을 언제 저장하고 언제 다시 내줘도 되는지 정하는 표준 문서입니다. 브라우저나 프록시 같은 캐시가 이 규칙을 따릅니다. 서버는 응답에 헤더를 붙여 그 판단을 조정합니다.

쉽고 빠른 이해

RFC 9111 은 웹 캐시가 지킬 규칙을 모은 문서입니다. 브라우저가 한 번 받은 응답을 서버에 다시 묻지 않고 꺼내 쓰는 판단이 이 규칙을 따릅니다.

규칙의 초점은 막는 데 있습니다. 다시 쓰면 안 되는 응답을 저장하는 일과, 저장한 응답을 엉뚱한 요청에 내주는 일을 막습니다. 한 사람에게만 가야 할 응답이 여러 사람이 쓰는 캐시에 남는 것이 그런 사고입니다.

캐시가 도는 순서는 셋입니다.

  1. 응답이 오면 서버가 붙인 헤더를 보고 저장해도 되는지 정합니다
  2. 같은 요청이 다시 오면 저장한 응답이 아직 신선한지 봅니다
  3. 낡았으면 서버에 바뀌었는지만 묻습니다. 안 바뀌었다는 답이 오면 저장한 응답의 헤더만 고쳐 내줍니다

대가도 있습니다. 신선한 동안에는 서버 쪽 내용이 바뀌어도 캐시가 저장한 응답을 서버에 묻지 않고 내줄 수 있습니다. 얼마나 오래 신선하게 볼지는 대개 서버가 헤더로 정합니다. 서버가 정하지 않은 응답은 몇몇 경우에 한해 캐시가 어림해 정할 수 있습니다.

상세

RFC(Request for Comments)는 IETF(Internet Engineering Task Force, 인터넷 기술 표준을 만드는 단체)가 번호를 붙여 내는 문서입니다. RFC 9111 은 그중 「HTTP Caching」이라는 제목의 문서입니다. HTTP(HyperText Transfer Protocol) 캐시가 무엇인지 정합니다. 캐시의 동작을 조정하는 헤더 필드도 정의합니다.

이 문서가 맡는 것은 HTTP 의 한 부분입니다. 요청과 응답이 무엇을 뜻하는지는 RFC 9110 이 정합니다. 이 문서는 그중 응답을 저장하고 다시 쓰는 일만 떼어 맡습니다. 2022년 6월에 나왔습니다. 이전 판인 RFC 7234 를 대체합니다.

본문 괄호 속의 MUST · SHOULD · MAY 는 원문이 쓴 요구 강도 낱말입니다. MUST 는 반드시 지킬 것입니다. SHOULD 는 이유가 있으면 어길 수 있는 권고입니다. MAY 는 해도 되고 안 해도 되는 것입니다. 원문이 전부 대문자로 적었을 때만 이 뜻이 걸립니다. 자세한 것은 「출처 문서」 절이 다룹니다.

캐시

HTTP 캐시는 응답 메시지를 담아 두는 로컬 저장소입니다. 그 저장소에 응답을 넣고, 찾고, 지우는 일을 맡는 부분까지 합쳐 캐시라고 부릅니다. 웹 브라우저가 한 번 받은 응답을 담아 두는 저장소가 그 예입니다.

캐시를 두는 까닭은 다음에 올 같은 요청의 응답 시간과 네트워크 대역폭을 줄이는 것입니다. 앞서 받은 응답으로 지금 요청에 답하면 서버까지 다녀오지 않아도 됩니다.

클라이언트든 서버든 캐시를 둘 수 있습니다(MAY). 다만 터널로 동작할 때는 둘 수 없습니다. 터널은 두 연결 사이에서 바이트를 읽지 않고 건네주기만 하는 중계입니다.

이 문서가 막으려는 사고

캐시를 두는 것 자체는 HTTP 에서 전부 선택 사항입니다(OPTIONAL). 그런데 캐시를 둔다면 저장한 응답을 다시 쓰는 것이 기본 동작으로 여겨집니다. 다시 쓰지 말라는 요구나 설정이 없으면 다시 씁니다.

그래서 이 문서의 요구는 막는 쪽에 맞춰져 있습니다. 막는 것은 둘입니다.

  1. 다시 쓰면 안 되는 응답을 저장하는 일
  2. 저장한 응답을 맞지 않는 요청에 다시 쓰는 일

어떤 응답을 꼭 저장하고 꼭 다시 쓰라고 요구하지는 않습니다. 응답을 하나도 저장하지 않는 캐시도 규칙을 어긴 것이 아닙니다.

막는 선이 없으면 한 사용자만 봐야 할 응답이 여러 사용자가 쓰는 캐시에 남아 다른 사람에게 갈 수 있습니다. 응답을 만든 쪽은 캐시 규칙이 지켜지리라 기대하고 헤더를 붙입니다. 그 기대가 어긋나지 않게 하는 것이 이 선의 몫입니다.

공유 캐시와 사설 캐시

캐시는 누가 쓰느냐로 둘로 갈립니다.

공유 캐시는 둘 이상의 사용자가 다시 쓰도록 응답을 저장하는 캐시입니다. 대개 중개자 안에 들어 있지만 늘 그렇지는 않습니다. 중개자는 클라이언트와 서버 사이에서 요청을 넘겨주는 프록시 같은 프로그램입니다.

사설 캐시는 한 사용자 전용입니다. 흔히 사용자 에이전트의 한 부품으로 들어 있습니다. 사용자 에이전트는 웹 브라우저처럼 사용자를 대신해 요청을 보내는 프로그램입니다.

둘을 가르는 까닭은 한 사람에게만 가야 할 응답이 있기 때문입니다. 그래서 공유 캐시에만 걸리는 제한이 있습니다. 공유 캐시에만 통하는 지시어도 있습니다. 지시어는 요청이나 응답의 Cache-Control 헤더에 적어 캐시에 내리는 지시입니다. 두 캐시가 지시어 앞에서 어떻게 갈리는지는 「캐시를 조정하는 헤더 필드」 소절 끝의 표에 모읍니다.

캐시 키

캐시가 저장해 둔 응답 가운데 하나를 고를 때 쓰는 정보가 캐시 키입니다. 최소한 요청 메서드와 대상 URI(Uniform Resource Identifier, 자원을 가리키는 주소)로 이루어집니다. 메서드는 그 응답을 어떤 요청에 다시 써도 되는지를 정합니다.

지금 흔히 쓰는 HTTP 캐시 가운데 많은 수는 GET 응답만 저장합니다. 그래서 URI 만 캐시 키로 씁니다.

같은 URI 라도 요청 헤더에 따라 응답이 갈릴 수 있습니다. 요청 헤더를 보고 여러 응답 가운데 하나를 골라 주는 콘텐츠 협상이 그런 경우입니다. Accept-Language 요청 헤더를 보고 한국어 페이지와 영어 페이지를 나눠 주는 서버가 그 예입니다. 서버는 응답의 Vary 헤더에 응답을 가른 요청 헤더의 이름을 적습니다. 이 예라면 Vary: Accept-Language 입니다. 캐시는 그 요청 헤더들까지 캐시 키에 넣어 응답을 구별합니다.

신선함과 낡음

캐시가 저장 응답을 서버에 묻지 않고 내줄지 정하는 기준이 신선도입니다. 그 판단에 쓰는 낱말을 먼저 풉니다.

오리진 서버는 그 자원을 가진 서버입니다. 응답을 처음 만드는 쪽입니다. 이 뒤로 그냥 「서버」라고 쓰면 오리진 서버를 뜻합니다.

검증은 저장 응답을 이 요청에 아직 써도 되는지 서버에 확인하는 일입니다. 낡은 응답을 다시 쓰려면 대개 이 확인을 거칩니다. 캐시는 조건부 요청으로 묻습니다. 조건부 요청은 전제 조건 헤더를 붙인 요청입니다.

낱말 뜻
나이 서버가 응답을 만든 때, 또는 서버에 검증해 성공한 때부터 흐른 시간
신선도 수명 서버가 응답을 만든 때부터 만료 시각까지의 길이
신선하다 나이가 아직 신선도 수명을 넘지 않았다
낡았다 나이가 신선도 수명을 넘었다

신선한 응답은 서버에 묻지 않고 다시 쓸 수 있습니다. 그만큼 지연과 네트워크 부담이 줄어듭니다. 낡은 응답도 다시 쓸 수 있는 경우가 있습니다. 검증으로 다시 신선해졌을 때와, 서버에 닿을 수 없을 때입니다.

서버에 닿을 수 없는 상태를 원문은 끊긴 상태(disconnected)라고 부릅니다. 캐시가 서버에 연락할 수 없거나, 요청을 넘길 다른 길을 못 찾는 상태입니다. 끊긴 캐시는 몇몇 경우에 낡은 응답을 내줄 수 있습니다.

만료 시각을 정하는 주된 방법은 서버가 미래의 만료 시각을 주는 것입니다. Expires 헤더나 max-age 지시어로 줍니다. 서버는 대개 그 시각까지 내용이 뜻있게 바뀌지 않으리라 보고 만료 시각을 잡습니다.

그 짐작이 빗나가 만료 전에 내용이 바뀌어도, 신선한 동안 캐시는 서버에 묻지 않고 옛 응답을 내줄 수 있습니다. 서버에 묻지 않아 빨라지는 대신 치르는 값입니다.

서버가 만료 시각을 주지 않는 응답도 있습니다. 그런 응답에는 정해진 경우에 한해 캐시가 만료 시각을 어림해 정할 수 있습니다. 이것을 휴리스틱 만료 시각이라고 부릅니다. 정해진 경우는 둘입니다(§4.2.2).

  1. 상태 코드가 「휴리스틱으로 캐시할 수 있다(heuristically cacheable)」고 정의된 응답. 어느 코드가 그런지는 RFC 9110 §15.1 같은 절이 정한다
  2. public 지시어처럼 명시해 캐시할 수 있다고 표시된 응답

둘 다 명시 만료 시각이 없는 응답일 때 이야기입니다. 명시 만료 시각이 있으면 휴리스틱을 쓰면 안 됩니다(MUST NOT).

캐시를 조정하는 헤더 필드

이 문서는 다섯 필드를 정의합니다. 서버와 클라이언트는 이 필드로 캐시의 판단을 조정합니다.

필드 하는 일
Cache-Control 요청과 응답이 지나는 경로상의 캐시에 지시어를 나열해 전한다
Age 응답이 서버에서 만들어지거나 검증된 뒤 흐른 시간. 보내는 쪽이 어림해 초로 적는다
Expires 이 시각이 지나면 응답을 낡은 것으로 보라는 날짜와 시각
Pragma HTTP/1.0 캐시를 위한 요청 헤더. 이 문서가 쓰지 않을 것으로 돌렸다(deprecated)
Warning 상태 코드에 안 드러나는 정보를 싣던 헤더. 이 문서가 폐기했다(obsoleted)

지시어 이름은 대소문자를 가리지 않습니다. 요청에 붙는 지시어와 응답에 붙는 지시어는 따로 놉니다. 요청에 붙었다고 응답에도 붙는 것이 아닙니다.

캐시는 응답 지시어를 따라야 합니다(MUST). 프록시는 캐시를 두든 안 두든 지시어를 다음 메시지로 넘겨야 합니다(MUST). 지시어를 특정 캐시 하나에만 겨냥할 방법은 없습니다.

이 문서 밖에서 정의한 지시어도 Cache-Control 에 실을 수 있습니다. 원문은 이를 캐시 확장이라 부릅니다. 다루는 절은 §5.2.3 입니다. RFC 5861 이 정의한 지시어가 그런 확장입니다.

이 문서가 정의한 응답 지시어 가운데 자주 만나는 여덟을 표로 모읍니다.

지시어 뜻
max-age=초 나이가 이 초를 넘으면 낡은 것으로 본다
s-maxage=초 공유 캐시에서만 max-age 와 Expires 를 누르고 이 값을 쓴다
no-store 요청도 응답도 어느 부분도 저장하지 않고, 다른 요청에 쓰지 않는다(MUST NOT). 사설·공유 캐시 모두에 걸린다
no-cache 인자 없이 쓰면, 저장은 해도 되지만 서버에 검증해 성공하기 전에는 다른 요청에 쓰지 않는다(MUST NOT)
must-revalidate 낡은 뒤에는 검증에 성공하기 전까지 다시 쓰지 않는다(MUST NOT). 서버에 닿지 못해도 마찬가지다
private 인자 없이 쓰면, 공유 캐시는 저장하지 않는다(MUST NOT). 사설 캐시는 저장해도 된다(MAY). 필드 이름 목록을 인자로 받을 수도 있다
public 보통은 저장이 막힐 응답도 저장 조건 안에서 저장해도 된다(MAY). 예를 들어 인증 정보를 실은 요청의 응답을 공유 캐시가 다시 쓰게 허락한다
must-understand 응답의 상태 코드를 이해하고 그 요구를 따르는 캐시만 저장하게 한다

no-cache 는 이름과 뜻이 어긋나 자주 헷갈립니다. 캐시를 안 쓴다는 뜻이 아니라 쓸 때마다 확인하라는 뜻입니다. 저장 자체를 막는 것은 no-store 입니다.

no-store 의 「저장하지 않는다」는 디스크 같은 비휘발성 저장소에 일부러 남기지 않는다는 뜻입니다. 메모리 같은 휘발성 저장소에 잠깐 있던 것은 응답을 넘긴 뒤 되도록 빨리 지우려고 애써야 합니다(MUST).

must-understand 를 붙인 응답에는 no-store 도 함께 붙이기를 권합니다(SHOULD). 그리고 must-understand 를 구현한 캐시가 그 상태 코드의 캐시 규칙을 이해하면, 함께 붙은 no-store 를 무시하기를 권합니다(SHOULD).

두 권고를 겹쳐 보면 no-store 가 누구에게 걸리는지 드러납니다. must-understand 를 구현하지 않은 캐시는 이 지시어를 모릅니다. 그래서 no-store 를 따라 저장하지 않습니다. 구현한 캐시도 그 상태 코드를 이해하지 못하면 no-store 를 따릅니다. 상태 코드를 이해하는 캐시만 no-store 를 넘어 저장합니다. no-store 덕에 「이해하는 캐시만 저장한다」는 뜻이 이 지시어를 모르는 캐시에도 걸립니다.

공유 캐시와 사설 캐시는 다섯 군데에서 갈립니다. 그중 셋은 지시어를 받아들이는 방식입니다.

공유 캐시 사설 캐시
누가 쓰나 여러 사용자 한 사용자
흔히 들어 있는 곳 프록시 같은 중개자 웹 브라우저 같은 사용자 에이전트
인자 없는 private 이 붙은 응답 저장하지 않는다 저장해도 된다
s-maxage 따른다 쓰지 않는다
인증 정보를 실은 요청의 응답 public · s-maxage · must-revalidate 가 있을 때만 저장한다 이 제한이 안 걸린다

출처 문서

RFC 9111 의 번호·판·상태를 문서 머리에서 읽습니다. 요구 강도가 어느 절에서 갈리는지, RFC 7234 에서 무엇이 바뀌었는지, 무엇을 구현에 맡겼는지도 함께 모읍니다. 정본은 rfc-editor.org 가 내는 원문입니다. 문서 머리가 번호와 상태를 함께 적습니다.

Internet Engineering Task Force (IETF)                  R. Fielding, Ed.
Request for Comments: 9111                                         Adobe
STD: 98                                               M. Nottingham, Ed.
Obsoletes: 7234                                                   Fastly
Category: Standards Track                                J. Reschke, Ed.
ISSN: 2070-1721                                               greenbytes
                                                               June 2022


                              HTTP Caching

번호는 9111, 발행은 2022년 6월입니다. 분류는 Standards Track 입니다. 정식 표준화 절차를 밟는 문서라는 뜻입니다. STD(Standard) 98 이라는 표준 번호도 달았습니다. STD 번호는 인터넷 표준 단계까지 오른 RFC 가 받는 번호입니다. 편집자는 R. Fielding · M. Nottingham · J. Reschke 세 사람입니다.

요구 강도

이 문서의 MUST · MUST NOT · SHOULD · SHOULD NOT · MAY · OPTIONAL 같은 낱말은 BCP(Best Current Practice) 14 가 정한 뜻으로 읽습니다. BCP 14 는 RFC 2119 와 RFC 8174 두 편입니다(§1.1). 조건이 하나 붙습니다. 전부 대문자로 적혔을 때만 그 뜻입니다. 소문자로 쓴 같은 낱말은 요구 강도가 아닙니다.

낱말 뜻
MUST · MUST NOT 반드시 지킨다. 어기면 이 표준을 안 지킨 캐시다
SHOULD · SHOULD NOT 따라야 하지만, 이유가 있으면 어길 수 있다
MAY · OPTIONAL 해도 되고 안 해도 된다

원문에는 소문자 should 로 적은 권고도 있습니다. §4.2.1 의 같은 지시어가 두 번 왔을 때의 처리가 그 예입니다. 이런 문장은 요구 강도 낱말이 아니라서 아래 표에 넣지 않았습니다.

강도가 갈리는 대표 문장을 절과 함께 모읍니다.

강도 문장 절
OPTIONAL 캐싱 자체가 선택이다 §2
MUST NOT 저장 조건을 다 채우지 못한 응답은 저장하지 않는다 §3
MUST NOT no-store 응답은 저장하지 않고 다른 요청에 쓰지 않는다 §5.2.2.5
MUST NOT 끊긴 상태(서버에 닿을 수 없는 상태)가 아니고 클라이언트·서버의 허락도 없으면 낡은 응답을 내주지 않는다 §4.2.4
MUST 검증 없이 저장 응답을 내줄 때 Age 를 새로 적는다 §4
MUST 안전하지 않은 메서드(PUT · POST · DELETE 처럼 서버 상태를 바꿀 수 있는 메서드)에 오류가 아닌 응답이 오면 대상 URI 의 저장 응답을 무효화한다 §4.4
MUST 검증 요청을 만들 때 저장 응답에 엔티티 태그(응답의 버전을 가리키는 ETag 값)가 있었으면 그것을 싣는다 §4.3.1
SHOULD 범위 요청이 아니고, 저장 응답 하나를 검증하며, 그 응답에 Last-Modified 가 있으면 그 값을 If-Modified-Since 로 싣는다 §4.3.1
SHOULD must-revalidate 때문에 낡은 응답 대신 오류를 낼 때, 다른 오류 코드가 더 맞지 않으면 상태 코드를 504 로 한다 §5.2.2.2
MAY 명시 만료 시각이 없는 응답에, 정해진 경우에 한해 캐시가 휴리스틱으로 만료 시각을 정한다 §4.2.2

폐기 관계

RFC 9111 은 RFC 7234 를 폐기했습니다. 7234 의 절 번호를 인용하면 폐기된 판을 근거로 드는 것입니다. 바뀐 것은 부록 B(Changes from RFC 7234)가 모아 둡니다.

바뀐 것 절
같은 지시어가 두 번 오거나 지시어끼리 부딪칠 때의 처리를 분명히 했다 §4.2.1
Location · Content-Location 의 URI 무효화는 이제 요구가 아니라 허용이다 §4.4
그 무효화는 오리진(스킴·호스트·포트의 묶음)이 다르면 금지된다. 예전 기준은 호스트였다 §4.4
잘못된 Age 값과 여러 개의 Age 값 처리를 분명히 했다 §5.1
이 문서가 정의한 지시어 일부에서 값을 따옴표 문자열로 만드는 것을 더 강하게 막았다. 상호운용 문제를 일으켰기 때문이다 §5.2
public · private 이 어떤 조건에서든 응답을 다시 쓰게 만드는 것은 아니라고 분명히 했다 §5.2.2
must-understand 지시어를 새로 들였다. 이 지시어가 없으면 캐시가 새 상태 코드의 뜻을 이해할 필요가 없다 §5.2.2.3
Warning 헤더를 폐기했다. 캐시나 중개자가 이 헤더를 붙이는 일이 드물었다 §5.5

Pragma 는 폐기가 아니라 쓰지 않을 것으로 돌린 필드입니다(§5.4). Cache-Control 지원이 널리 퍼졌기 때문입니다. 응답의 Pragma: no-cache 는 뜻이 정해진 적이 없어 Cache-Control: no-cache 를 믿고 대신할 수 없습니다.

구현에 맡긴 대목

이 문서가 정하지 않고 캐시 구현에 넘긴 대목이 있습니다. 실무에서 캐시마다 동작이 갈리는 곳이 여기입니다.

대목 이 문서가 정한 범위 절
휴리스틱 만료 시각 계산법 쓸 수 있는 응답만 정한다(「상세」의 두 경우). 구체적인 알고리즘은 안 정하고 결과가 넘으면 안 되는 한도만 건다. Last-Modified 이후 흐른 시간의 일부 이하를 쓰기를 권한다. 흔한 설정값으로 10% 를 든다 §4.2.2
함께 무효화할 다른 URI Location · Content-Location 은 후보다. 다른 URI 를 찾는 방법은 정하지 않는다 §4.4
검증자(응답의 버전을 가리키는 값)도 만료 시각도 없는 응답 대개 저장해 봐야 쓸모가 없어 저장 안 하는 캐시가 있다. 그래도 저장을 막지는 않는다 §3
캐시 확장 저장·재사용 조건을 캐시 확장이 덮어쓸 수 있다 §3 · §4 · §5.2.3

예시

RFC 9111 원문이 직접 든 값을 헤더 셋(Expires · Cache-Control · Age)으로 나눠 풉니다. 값마다 그 값이 캐시에 무엇을 정하는지를 붙입니다.

Expires

Expires: Thu, 01 Dec 1994 16:00:00 GMT

이 응답은 1994년 12월 1일 16시 0분 0초(그리니치 표준시)가 지나면 낡은 것으로 봅니다. 값은 HTTP 가 정한 날짜 형식(HTTP-date)입니다.

날짜 형식이 잘못된 값은 이미 지난 시각으로 읽어야 합니다(MUST). 특히 값 0 이 그렇습니다. Expires: 0 이 붙은 응답은 받는 순간부터 만료된 것입니다.

같은 응답에 Cache-Control 의 max-age 가 있으면 받는 쪽은 Expires 를 무시해야 합니다(MUST). s-maxage 가 있으면 공유 캐시가 Expires 를 무시해야 합니다(MUST). 이럴 때 Expires 는 Cache-Control 을 아직 구현하지 않은 수신자를 위해 남겨 둔 값입니다.

Expires 가 있다고 원래 자원이 그 시각에 바뀌거나 사라진다는 뜻은 아닙니다.

Cache-Control

Cache-Control 의 문법은 두 줄입니다.

Cache-Control   = #cache-directive
cache-directive = token [ "=" ( token / quoted-string ) ]

# 은 쉼표로 이은 목록이라는 표시입니다. 지시어는 이름 하나로 시작합니다. = 뒤에 값을 붙일 수 있습니다. 값은 토큰이나 따옴표로 감싼 문자열입니다. 인자를 정해 둔 지시어가 아니면 값을 붙일 수 없습니다.

원문 §4 에 나오는 값 셋입니다.

Cache-Control: max-age=0
Cache-Control: no-cache
Cache-Control: no-store

앞의 둘은 요청에 붙는 값입니다. 쓸 만한 저장 응답이 여럿이라 어느 것을 쓸지 모호할 때, 캐시는 요청에 이 값을 실어 서버로 넘길 수 있습니다. 서버의 답으로 어느 응답을 쓸지 가리는 것입니다.

요청에 붙은 max-age 는 응답의 max-age 와 뜻이 다릅니다. 클라이언트가 그 요청에 쓸 응답의 신선도 셈에 한도를 제안하는 지시어입니다(§4.2). max-age=0 은 그 한도를 0초로 잡은 값입니다. 캐시가 이 제안을 꼭 따를 필요는 없습니다. 요청 쪽 no-cache 의 정의(§5.2.1)는 자료에 없습니다(미확인).

no-store 는 응답에 붙어 저장을 막습니다. 이 값이 붙은 응답은 사설 캐시도 공유 캐시도 저장하지 않습니다.

Age

Age = delta-seconds

값은 0 이상의 정수입니다. 단위는 초입니다. 보내는 쪽이 어림한 응답의 나이입니다. 검증 없이 저장 응답을 내주는 캐시는 이 필드를 새로 적어야 합니다(MUST). 응답에 이미 Age 가 있었으면 그 값을 자기가 센 현재 나이로 바꿉니다. 현재 나이는 「동작」 절의 「나이 세기」가 셈하는 current_age 입니다.

동작

요청 하나를 받은 캐시 한 대가 무엇을 차례로 따지는지 좇습니다. 대상은 캐시 한 대 안의 판단입니다. 순서는 저장 → 재사용 → 신선도 셈 → 검증 → 무효화입니다.

flowchart TD
    A["요청이 도착한다"] --> B{"캐시 키가 맞는<br/>저장 응답이 있나"}
    B -->|없다| C["서버의 응답으로 답하고<br/>저장할지 따진다"]
    B -->|있다| D{"신선한가"}
    D -->|신선하다| E["Age 를 적어 내준다"]
    D -->|낡았다| F["조건부 요청으로 검증한다"]
    F -->|304| G["저장 응답의 헤더를 고쳐 내준다"]
    F -->|새 응답| C

그림의 갈림은 셋입니다. 캐시 키가 맞는 저장 응답이 있나, 그 응답이 신선한가, 검증에 서버가 무엇으로 답하나입니다. 저장 응답이 없거나 서버가 새 응답을 보내면 그 응답으로 답합니다. 저장할지는 그 뒤에 따집니다.

저장할지 정하기

캐시는 아래 조건을 전부 채운 응답만 저장할 수 있습니다. 하나라도 빠지면 저장하면 안 됩니다(MUST NOT).

  1. 캐시가 요청 메서드를 이해한다
  2. 상태 코드가 최종 상태 코드다. 1xx 처럼 중간에 오는 정보 응답이 아니라는 뜻이다
  3. 상태 코드가 206 이나 304 이거나 응답에 must-understand 가 붙었으면, 캐시가 그 상태 코드를 이해한다
  4. 응답에 no-store 가 없다
  5. 공유 캐시라면, private 이 없거나, 있더라도 공유 캐시가 응답을 고쳐서 저장하도록 허락하는 private 이다
  6. 공유 캐시라면, 요청에 Authorization 헤더가 없거나 공유 캐시 저장을 명시해 허락하는 응답 지시어가 있다
  7. 응답에 저장을 받쳐 주는 표지가 하나 이상 있다

「이해한다」는 그 메서드나 상태 코드를 알아보고, 거기 걸린 캐시 동작을 전부 구현했다는 뜻입니다.

5번은 private 의 두 꼴을 가릅니다. 인자 없이 쓴 private 은 공유 캐시 저장을 막습니다. 그러니 허락하는 쪽은 필드 이름 목록을 인자로 단 private 입니다(§5.2.2.7). 그 꼴이 응답의 무엇을 고치게 하는지는 자료에 없습니다(미확인).

Authorization 은 요청에 인증 정보를 싣는 헤더입니다. 6번의 「명시해 허락하는 지시어」는 must-revalidate · public · s-maxage 셋입니다.

7번의 표지는 아래 가운데 하나입니다.

표지 조건
public 없음
private 공유 캐시가 아닐 때
Expires 없음
max-age 없음
s-maxage 공유 캐시일 때
저장을 허락하는 캐시 확장 없음
휴리스틱으로 캐시할 수 있다고 정의된 상태 코드 없음

마지막 행의 상태 코드는 「상세」에서 본 휴리스틱 대상 코드와 같습니다. 원문은 이 코드들을 예전 판에서 「기본으로 캐시할 수 있다(cacheable by default)」고 불렀다고 적습니다.

가장 흔히 저장되는 것은 GET 요청에 온 200 OK 응답입니다. 리다이렉트, 404 Not Found 같은 부정 결과, 206 Partial Content 같은 부분 결과도 저장할 수 있습니다. GET 이 아닌 메서드의 응답도 그 메서드의 정의가 캐싱을 허락하고 캐시 키로 쓸 것을 정해 두었으면 저장할 수 있습니다.

다시 쓸지 정하기

요청이 오면 캐시는 저장 응답을 아래 조건을 전부 채울 때만 다시 씁니다. 하나라도 빠지면 다시 쓰면 안 됩니다(MUST NOT).

  1. 요청의 대상 URI 와 저장 응답의 대상 URI 가 같다
  2. 저장 응답에 딸린 요청 메서드가 이번 요청에 써도 되는 메서드다
  3. 저장 응답의 Vary 가 지목한 요청 헤더가 있으면, 그 값이 이번 요청과 맞는다
  4. 저장 응답에 no-cache 가 없다. 있으면 검증에 성공해야 한다
  5. 저장 응답이 신선하거나, 낡은 채로 내줘도 되거나, 검증에 성공했다

이 조건 밖에서도 반드시 지킬 것이 넷 있습니다.

경우 캐시가 할 일
검증 없이 저장 응답을 내줄 때 Age 를 현재 나이로 새로 적는다(MUST)
쓸 만한 저장 응답이 여럿일 때 Date 헤더(서버가 응답을 만든 시각)로 보아 가장 최근 것을 쓴다(MUST)
캐시에 시계가 없을 때 쓸 때마다 저장 응답을 검증한다(MUST)
안전하지 않은 메서드의 요청일 때 서버로 넘기고 응답을 받기 전에는 답을 만들지 않는다(MUST)

안전하지 않은 메서드는 PUT · POST · DELETE 처럼 서버의 상태를 바꿀 수 있는 메서드입니다.

신선도 수명 정하기

신선도 수명은 응답의 헤더에서 읽습니다. 캐시는 아래 순서로 따져 처음 걸리는 것을 씁니다.

flowchart TD
    A{"공유 캐시이고<br/>s-maxage 가 있나"} -->|있다| S["s-maxage 값"]
    A -->|아니다| B{"max-age 가 있나"}
    B -->|있다| M["max-age 값"]
    B -->|없다| C{"Expires 가 있나"}
    C -->|있다| E["Expires 에서 Date 를 뺀 값"]
    C -->|없다| H["명시 만료 시각 없음<br/>휴리스틱을 쓸 수도 있다"]

Date 는 서버가 응답을 만든 시각을 적는 헤더입니다. Date 가 없으면 메시지를 받은 시각을 씁니다. 가능한 한 서버의 시계를 쓰려는 순서입니다. 그래야 캐시와 서버의 시계가 어긋난 영향이 줄어듭니다.

같은 지시어가 두 번 오면 첫 번째 것을 쓰거나 응답을 낡은 것으로 보라고 권합니다. max-age 와 no-cache 처럼 지시어끼리 부딪치면 가장 엄한 쪽을 따르라고 권합니다. 두 권고는 원문이 소문자 should 로 적었습니다. 요구 강도 낱말이 아닌 권고입니다. 정수가 아닌 max-age 처럼 신선도 정보가 잘못된 응답은 낡은 것으로 보기를 권합니다(encouraged).

명시 만료 시각이 없을 때 휴리스틱을 쓰는 것은 해도 되는 일입니다(MAY). 쓸 수 있는 응답은 「상세」에서 본 두 경우뿐입니다. 상태 코드가 휴리스틱으로 캐시할 수 있다고 정의된 응답과, public 처럼 명시해 캐시할 수 있다고 표시된 응답입니다. 명시 만료 시각이 있으면 휴리스틱을 쓰면 안 됩니다(MUST NOT).

서버가 일부러 지난 시각을 만료 시각으로 주면, 응답은 처음부터 낡은 것이 됩니다. 표준을 지키는 캐시는 보통 그 응답을 다시 쓰기 전에 검증합니다.

나이 세기

나이는 캐시가 스스로 셉니다. 셈에 쓰는 값은 다섯입니다.

값 뜻
age_value 받은 응답의 Age 헤더 값. 없으면 0
date_value 받은 응답의 Date 헤더 값
request_time 이 응답을 부른 요청을 보낸 때의 시계
response_time 응답을 받은 때의 시계
now 지금 시계

네 시점이 시계 위에 어떻게 놓이는지 그리면 이렇게 됩니다.

sequenceDiagram
    participant 캐시
    participant 서버
    Note over 캐시: request_time 요청을 보낸다
    캐시->>서버: 요청
    Note over 서버: 응답을 만든다 · Date 를 적는다
    서버->>캐시: 응답 · 앞선 캐시를 지났으면 Age 도 붙는다
    Note over 캐시: response_time 응답을 받는다
    Note over 캐시,서버: response_delay 는 request_time 부터 response_time 까지
    Note over 캐시: 캐시 안에 머문다 · resident_time
    Note over 캐시: now 다시 내줄지 셈한다

받을 때의 나이를 셈하는 길은 둘입니다.

첫째는 Date 와 받은 시각의 차이(apparent_age)입니다. 캐시 시계가 서버 시계와 잘 맞을 때 쓸 만합니다. 셈이 음수로 나오면 0 으로 바꿉니다.

둘째는 앞선 캐시가 적어 보낸 Age 에 요청을 보낸 뒤 응답이 오기까지 걸린 시간(response_delay)을 더한 값(corrected_age_value)입니다. 응답이 지나온 경로의 캐시가 전부 HTTP/1.1 이상일 때 맞습니다. 캐시는 이 값을 응답을 받은 때가 아니라 요청을 보낸 때를 기준으로 읽어야 합니다(MUST).

corrected_age_value 만 받을 때의 나이(corrected_initial_age)로 써도 됩니다(MAY). Age 를 제대로 못 적는 아주 옛 캐시가 섞여 있을 수 있으면, 두 값 가운데 큰 것을 써서 더 보수적으로 셀 수 있습니다. 그 위에 응답을 받은 뒤 캐시 안에 머문 시간(resident_time)을 더하면 현재 나이(current_age)가 됩니다.

원문의 계산 블록이 이 셈을 한데 적습니다.

apparent_age = max(0, response_time - date_value);

response_delay = response_time - request_time;
corrected_age_value = age_value + response_delay;

corrected_initial_age = max(apparent_age, corrected_age_value);

resident_time = now - response_time;
current_age = corrected_initial_age + resident_time;

신선한지는 마지막에 한 줄로 가립니다.

response_is_fresh = (freshness_lifetime > current_age)

신선도 수명이 현재 나이보다 크면 신선합니다. 그렇지 않으면 낡은 것입니다.

낡은 응답을 내줄 때

낡은 응답을 검증 없이 내주는 일은 기본으로 막혀 있습니다. 둘 가운데 하나에 걸리면 내주면 안 됩니다(MUST NOT).

  1. 응답의 지시어가 막는다. 원문이 드는 지시어는 no-cache · must-revalidate 와, 걸리는 s-maxage · proxy-revalidate 다
  2. 캐시가 끊긴 상태가 아니고, 클라이언트나 서버가 명시해 허락하지도 않았다

1번의 「걸리는」은 그 캐시에 적용되는 지시어라는 뜻입니다. s-maxage 는 공유 캐시에만 걸리므로, 공유 캐시에서만 이 금지에 들어갑니다. proxy-revalidate 는 원문이 같은 줄에 드는 응답 지시어입니다. 그 정의는 자료에 없습니다(미확인).

2번의 끊긴 상태는 「상세」에서 본 대로 캐시가 서버에 닿을 수 없는 상태입니다. 명시한 허락의 예는 셋입니다. 요청의 max-stale 지시어, RFC 5861 이 정의한 확장 지시어, 별도 계약에 따른 설정입니다.

두 금지를 겹치면 갈림이 이렇게 됩니다.

flowchart TD
    A["낡은 저장 응답"] --> B{"응답의 지시어가 막나"}
    B -->|막는다| V["검증 없이는 내주지 않는다"]
    B -->|안 막는다| C{"끊긴 상태이거나<br/>명시한 허락이 있나"}
    C -->|그렇다| S["낡은 채 내줄 수 있다"]
    C -->|아니다| V

must-revalidate 는 끊긴 상태에서도 풀리지 않습니다. 이때 캐시는 낡은 응답 대신 오류 응답을 만들어야 합니다(MUST). 상태 코드는 다른 코드가 더 맞지 않으면 504 Gateway Timeout 으로 하기를 권합니다(SHOULD).

검증

검증은 저장 응답을 내줄 수 없을 때 서버에 묻는 절차입니다. 응답이 낡았거나, 여럿 가운데 하나를 고를 수 없을 때가 그렇습니다. 캐시는 넘기는 요청을 조건부 요청으로 만들어 묻습니다.

조건부 요청은 요청에 전제 조건 헤더를 붙인 요청입니다. 서버는 그 조건을 시험해 보고 답을 고릅니다. 캐시는 저장 응답이 가진 검증자를 이 헤더에 실어 보냅니다. 검증자는 응답의 버전을 가리키는 값입니다.

저장 응답에 있던 검증자 실어 보내는 헤더 강도
엔티티 태그(ETag 값) If-None-Match 등 반드시 싣는다(MUST)
Last-Modified 시각 If-Modified-Since 싣기를 권한다(SHOULD). 범위 요청(응답의 일부만 달라는 요청)이 아니고 저장 응답 하나를 검증할 때

서버가 304 로 답하는 왕복에서 선 위로 오가는 것은 검증자를 실은 요청과 본문 없는 304 입니다.

sequenceDiagram
    participant 클라이언트
    participant 캐시
    participant 서버
    클라이언트->>캐시: 요청
    Note over 캐시: 저장 응답이 낡았다
    캐시->>서버: 조건부 요청 · If-None-Match 에 엔티티 태그
    서버->>캐시: 304 Not Modified · 본문 없음
    Note over 캐시: 304 의 헤더로 저장 응답의 헤더를 고친다
    캐시->>클라이언트: 고친 저장 응답

서버의 답에 따라 캐시가 하는 일이 갈립니다.

받은 응답 캐시가 하는 일
304 Not Modified 저장 응답을 고쳐 다시 쓴다. 304 가 실어 온 헤더로 저장 응답의 헤더를 갱신해야 한다(MUST)
본문이 있는 전체 응답 저장 응답은 하나도 맞지 않는다는 뜻이다. 이 새 응답으로 답해야 한다(MUST). 저장 조건을 채우면 저장해도 된다(MAY)
5xx 서버 오류 오류를 클라이언트에 넘기거나, 서버가 답하지 않은 것처럼 다룬다. 뒤쪽이면 낡은 응답 규칙 안에서 저장 응답을 내주거나 검증을 다시 시도한다

304 를 받으면 본문을 다시 받지 않아도 됩니다. 응답이 안 바뀌었을 때 낭비를 줄이는 것이 검증의 쓸모입니다.

무효화

무효화는 저장 응답을 지우거나, 다음에 쓰기 전에 반드시 검증하라고 표시하는 일입니다. PUT · POST · DELETE 같은 안전하지 않은 메서드는 서버의 상태를 바꿀 수 있습니다. 그래서 요청이 지나는 중간 캐시가 저장 응답을 무효화해서 내용을 맞춰야 합니다.

안전하지 않은 메서드의 요청에 오류가 아닌 응답이 오면, 캐시는 그 대상 URI 를 무효화해야 합니다(MUST). 안전한지 모르는 메서드도 같습니다. 오류가 아닌 응답은 2xx 성공이나 3xx 리다이렉트 응답입니다.

다른 URI 를 함께 무효화해도 됩니다(MAY). 응답의 Location · Content-Location 헤더에 적힌 URI 가 후보입니다. 무효화할 URI 의 오리진이 대상 URI 의 오리진과 다르면 무효화하면 안 됩니다(MUST NOT). 서비스 거부 공격을 막는 데 도움이 되는 제한입니다. 오리진은 스킴·호스트·포트의 묶음입니다.

이 무효화는 요청이 지나간 캐시에서만 일어납니다. 다른 경로에 있는 캐시는 그 요청을 못 봤으니 옛 응답을 계속 가집니다. 모든 캐시를 한꺼번에 무효화하는 방법은 이 문서에 없습니다.

경계

브라우저에서 뒤로 가기를 눌렀더니 이미 만료된 페이지가 다시 보입니다. 이 브라우저는 RFC 9111 을 어긴 것인가.

아닙니다. 뒤로 가기 같은 기록 기능은 HTTP 캐시가 아닙니다. 웹 브라우저 같은 애플리케이션이 HTTP 위에 따로 둔 기능입니다. 이 문서의 요구는 응답을 HTTP 캐시에서 꺼낸 뒤 애플리케이션이 그 데이터를 어떻게 쓰는지에는 꼭 걸리지 않습니다. 그래서 기록 기능은 만료된 응답이라도 앞서 받은 것을 다시 보여 줄 수 있습니다.

신선도도 같은 선에서 끊깁니다. 신선도는 캐시 동작에만 쓰는 개념입니다. 화면을 새로 그리라거나 자원을 다시 불러오라고 사용자 에이전트에 시키는 장치가 아닙니다.

한 페이지를 보는 동안 이미지 같은 자산을 따로 캐시하는 브라우저도 있습니다. 이런 캐시는 HTTP 캐시 의미를 따를 수도 있고 안 따를 수도 있습니다.

반대쪽도 열려 있습니다. 기록 기능이 HTTP 캐시 규칙을 따르는 것은 막지 않습니다. 이 화면이 낡았다고 알려 주거나, Cache-Control: no-store 를 따르는 기록 기능도 이 문서와 어긋나지 않습니다.

사용자가 알아채거나 손쉽게 조절할 수 없게 데이터를 캐시하는 애플리케이션에는 강하게 권하는 것이 있습니다(strongly encouraged). HTTP 캐시 지시어에 맞춰 동작을 정해 두라는 것입니다. 캐시 의미가 지켜지리라 기대하는 콘텐츠 작성자를 놀라게 하지 않으려는 권고입니다.

원문이 드는 예가 no-store 응답입니다. HTTP 위에 둔 애플리케이션 캐시가 그 응답을, 그것을 가져온 요청과 직접 관련된 요청에 다시 쓰는 것은 그럴 만할 수 있다고 봅니다. 같은 페이지를 불러오는 동안 생긴 요청이 그런 요청입니다. 그 응답과 아무 관련 없는 요청에까지 다시 쓰면 사용자와 작성자를 놀라게 하고 헷갈리게 할 것이라고 봅니다.

정리하면 선은 「HTTP 캐시에서 꺼낸 뒤」입니다. 그 앞의 저장과 재사용은 이 문서가 정합니다. 그 뒤의 쓰임은 애플리케이션이 정합니다. 검증에 쓰는 조건부 요청의 뜻도 이 문서 밖입니다. 그건 RFC 9110 이 정합니다.

관련 항목

이 문서와 함께 HTTP 를 정의하는 표준 문서

RFC 9110 · RFC 9112 · RFC 9113 · RFC 9114

이 문서의 요구 강도 낱말을 정한 문서

RFC 2119 · RFC 8174 · BCP 14

이 문서가 대체했거나 이 문서를 넓히는 문서

RFC 7234 · RFC 5861 · RFC 8246 · RFC 9213

이 문서가 정의하는 헤더 필드

Cache-Control · Age · Expires · Pragma · Warning

Cache-Control 에 들어가는 지시어

no-store · no-cache · max-age · s-maxage · private · public · must-revalidate · proxy-revalidate · max-stale · min-fresh · only-if-cached · no-transform · must-understand · stale-while-revalidate · stale-if-error · immutable · 캐시 확장

캐시가 응답을 다시 쓸지 가리는 개념

신선도 · 신선도 수명 · 나이 · 휴리스틱 신선도 · 캐시 키 · Vary · 콘텐츠 협상 · 검증자 · 재검증 · 조건부 요청 · 무효화 · 낡은 데이터 · 끊긴 상태

검증과 신선도 셈에서 오가는 헤더와 상태 코드

ETag · Last-Modified · If-None-Match · If-Modified-Since · Date · 304 Not Modified · 504 Gateway Timeout

저장과 무효화를 가르는 메시지 요소

GET · POST · PUT · DELETE · 안전한 메서드 · Authorization 헤더 · Accept-Language · 범위 요청 · Location · Content-Location · 오리진

이 규칙을 따르는 캐시의 종류

HTTP 캐시 · 사설 캐시 · 공유 캐시 · 브라우저 캐시 · 프록시 · 리버스 프록시 · CDN

캐시 앞뒤에 서는 HTTP 참여자

오리진 서버 · 사용자 에이전트 · 중개자 · 터널 · 클라이언트

HTTP 캐시에서 나는 보안 문제

캐시 포이즈닝 · 타이밍 공격 · 웹 캐시 속임 · 서비스 거부 공격

이것이 속하는 상위 분류

HTTP · 캐싱 · IETF · RFC · 인터넷 표준

다른 이름: HTTP Caching · STD 98