Last-Modified
고친 사람 github-actions[bot]
Last-Modified 는 서버가 보내는 내용을 마지막으로 고친 때를 알려 줍니다. 받는 쪽은 이 시각을 간직합니다. 나중에 그 뒤로 바뀌었는지를 서버에 물을 수 있습니다. 바뀐 것이 없으면 같은 내용을 다시 받지 않아도 됩니다.
쉽고 빠른 이해
Last-Modified 는 서버가 응답에 적어 보내는 「마지막으로 고친 시각」입니다. hello.txt 파일을 받으면
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT 같은 줄이 함께 옵니다.
이 시각이 없으면 캐시는 고친 때가 아닌 시각으로 물어야 합니다. 응답을 만든 때나 응답을 받은 때를 대신 씁니다. 이 값이 있으면 고친 때를 그대로 대고 물을 수 있습니다. 안 바뀐 내용을 다시 받지 않아 쓸데없는 전송이 크게 줄어듭니다.
- 서버가 내용과 함께 마지막으로 고친 시각을 보냅니다
- 받는 쪽은 내용과 시각을 함께 저장합니다
- 다음 요청에 그 시각을 실어 보내면, 안 바뀌었을 때 서버는 본문 없이 짧게 답합니다
대가는 정밀도입니다. 시각은 초까지만 적습니다. 그래서 1초 안에 두 번 고치면 둘을 못 가릴 수 있습니다. 서버 시계를 고치면 시각이 뒤로 간 것처럼 보이기도 합니다. 그래서 정확히 같은 시각만 받아 주는 서버도 있습니다.
상세
Last-Modified 는 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜) 응답에 실리는 헤더 필드입니다. 값은 원 서버가 이 내용을 마지막으로 고쳤다고 믿는 시각입니다. 원 서버는 내용의 원본을 들고 있는 서버입니다. 중간에서 응답을 저장해 두었다가 내주는 캐시와 가르려고 붙인 이름입니다.
리소스는 URL(Uniform Resource Locator, 통합 자원 위치 지정자) 하나가 가리키는 대상입니다. 「이 내용」은 그 리소스에 대해 원 서버가 요청 하나에 골라 내보내는 한 벌입니다. 이것을 표현이라고 부릅니다. 원 서버는 요청 처리를 마치는 때에 이 시각을 정합니다. 그 시점에서 보아 표현이 마지막으로 고쳐진 때가 값이 됩니다.
표현은 대개 여러 조각이 합쳐진 결과입니다. 그래서 이 시각은 보통 그 조각들 가운데 가장 최근에 바뀐 때가 됩니다. 어느 조각을 어떻게 세어 값을 정할지는 서버 구현에 맡깁니다. 표준은 이것을 정하지 않습니다.
구현이 고르는 값의 보기는 이렇습니다. 파일을 내주는 서버라면 파일 시스템이 적어 둔 수정 시각이 곧 이 값일 수 있습니다. 데이터베이스 레코드를 내주는 서버라면 그 레코드를 마지막으로 갱신한 시각일 수 있습니다.
검증자로서의 쓰임
조건부 요청은 「이 조건이 맞을 때만 해 달라」고 덧붙여 보내는 요청입니다. 그 조건에 넣어 내용이 바뀌었는지 가늠하는 값을 검증자라고 부릅니다. HTTP 가 기본으로 정의하는 검증자는 두 가지입니다. 하나가 이 수정 시각입니다. 다른 하나는 ETag(entity tag, 엔티티 태그)라는 불투명한 문자열입니다. 속을 뜯어 뜻을 캐지 말고 같은지만 보라는 뜻에서 불투명하다고 합니다.
검증자는 강한 것과 약한 것으로 갈립니다. 강한 검증자는 내용이 조금이라도 바뀌면 값도 바뀝니다. 약한 검증자는 내용이 바뀌어도 값이 안 바뀔 수 있습니다. Last-Modified 가 어느 쪽인지는 경계 절이 판정합니다.
이 시각을 조건으로 싣는 요청 헤더는 셋입니다.
| 요청 헤더 | 붙이는 조건 |
|---|---|
| If-Modified-Since | 그 시각 뒤로 바뀌었으면 내용을 보내 달라 |
| If-Unmodified-Since | 그 시각 뒤로 안 바뀌었을 때만 요청을 실행해 달라 |
| If-Range | 그 판이 맞으면 Range 헤더로 청한 일부만, 아니면 전부 보내 달라 |
Range 는 내용의 일부만 달라는 요청 헤더입니다. 이 셋은 모두 요청에 실립니다. 그래서 조건을 따지는 쪽은 응답을 받는 쪽이 아니라 그 요청을 받는 원 서버나 중간 캐시입니다. ETag 를 조건으로 싣는 짝도 있습니다. If-None-Match 와 If-Match 입니다.
아래부터 괄호 안에 MUST 와 SHOULD 가 나옵니다. 요구의 강도를 적은 낱말입니다. MUST 는 어기면 표준을 어긴 것입니다. SHOULD 는 그럴 만한 이유가 있으면 달리 해도 되는 권고입니다.
원 서버는 마지막 수정 시각을 합리적이고 일관되게 정할 수 있는 표현이면 이 필드를 보내기를 권합니다(SHOULD). 조건부 요청과 캐시의 신선도 판단에 쓰여 쓸데없는 전송을 크게 줄일 수 있기 때문입니다. 그 덕에 서비스의 가용성과 확장성도 크게 높아질 수 있습니다.
이 값이 없는 응답을 든 캐시는 다른 데이터로 조건 값을 만들기도 합니다. 응답의 Date 나 그 응답을 받은 때를 쓰는 것입니다. 그렇게 만든 시각은 내용을 고친 때가 아닙니다. 받아 둔 Last-Modified 를 그대로 되돌려 보내는 쪽이 원 서버와 가장 잘 맞물립니다. 그 까닭은 맞물림 절이 봅니다.
값의 생김새
값은 HTTP-date 라는 꼴의 시각 하나입니다.
Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT
요일, 일·월·연, 시·분·초가 차례로 옵니다. 끝에는 GMT(Greenwich Mean Time, 그리니치 평균시)가 붙습니다. 초보다 잘게는 적지 않습니다.
HTTP-date 는 언제나 UTC(Coordinated Universal Time, 협정 세계시)의 한 순간을 가리킵니다. GMT 는 UTC 라는 이름보다 먼저 쓰던 이름입니다. 이 줄의 GMT 세 글자가 UTC 를 나타냅니다.
같은 시각을 적는 꼴은 셋이 정의돼 있습니다. 1995년 전에 서버들이 흔히 쓰던 세 꼴을 옛 구현과 맞추려고 모두 남긴 것입니다. 받는 쪽은 세 꼴을 모두 읽을 줄 알아야 합니다(MUST).
보내는 쪽은 한 꼴로만 적어야 합니다(MUST). 그 꼴의 이름이 IMF-fixdate 입니다. IMF(Internet Message Format, 인터넷 메시지 형식)의 날짜 표기에서 길이를 고정한 꼴입니다. 시간대도 하나로 묶었습니다. 나머지 두 옛 꼴의 실물은 예시 절에 있습니다. HTTP-date 는 대소문자를 가립니다. 캐시가 받는 쪽일 때는 이 규칙이 완화됩니다.
값을 만드는 규칙
Date 는 서버가 응답 메시지를 만들어 낸 때를 적는 헤더 필드입니다. 원 서버는 Last-Modified 값을 Date 를 만드는 때에 되도록 가깝게 얻기를 권합니다(SHOULD). 응답을 만드는 즈음에 내용이 바뀌어도 받는 쪽이 수정 시각을 바르게 가늠할 수 있게 하려는 것입니다.
이 규칙들에서 시계는 지금의 UTC 시각을 그럴듯하게 어림해 줄 수 있는 구현을 말합니다. 시계가 있는 원 서버는 Date 보다 늦은 Last-Modified 를 만들면 안 됩니다(MUST NOT).
구현이 들고 있는 메타데이터로 셈한 시각이 서버 시계로 미래일 수도 있습니다. 그러면 원 서버는 그 값을 Date 로 바꿔 넣어야 합니다(MUST). 미래의 수정 시각이 캐시의 검증을 그르치지 않게 하려는 규칙입니다.
시계가 없는 원 서버는 다른 시스템이 그 리소스에 매겨 준 값이 아니면 Last-Modified 를 만들면 안 됩니다(MUST NOT). 시계가 있나 없나가 먼저 갈리고 그다음이 값의 검사입니다.
flowchart TD
A{"원 서버에 시계가 있나"}
A -- 있다 --> B{"셈한 시각이 Date 보다 미래인가"}
B -- 그렇다 --> C["Date 로 바꿔 넣는다 · MUST"]
B -- 아니다 --> D["셈한 시각을 그대로 적는다"]
A -- 없다 --> E{"다른 시스템이 매긴 값인가"}
E -- 그렇다 --> F["그 값을 적는다"]
E -- 아니다 --> G["Last-Modified 를 만들지 않는다 · MUST NOT"]
출처 문서
정본은 RFC(Request for Comments) 9110 HTTP Semantics 입니다. IETF(Internet Engineering Task Force, 국제 인터넷 표준화 기구)가 2022년 6월에 냈습니다. Category 는 Standards Track 입니다. STD(Standard) 97 번이 붙었습니다. Last-Modified 의 정의는 §8.8.2 에 있습니다.
RFC 9110 은 조건부 요청을 다루던 RFC 7232 를 폐기했습니다. 같은 정의 문장이 RFC 7232 §2.2 에 있었습니다. 옛 절 번호를 인용하면 폐기된 문서를 드는 것입니다.
캐시 쪽 규칙은 RFC 9111 HTTP Caching 이 갖습니다. 신선도 짐작은 §4.2.2, 검증 요청은 §4.3.1 입니다.
대체 관계
| 문서 | 나온 때 | 성격 | 이 필드를 적은 절 | 대체 관계 |
|---|---|---|---|---|
| RFC 9110 HTTP Semantics | 2022년 6월 | Standards Track · STD 97 | §8.8.2 | RFC 7232 를 폐기 |
| RFC 7232 HTTP/1.1 Conditional Requests | 2014년 6월 | Standards Track | §2.2 | RFC 2616 을 폐기 |
| RFC 2616 | 미확인 | Standards Track | §14.29 | RFC 2068 을 폐기 |
| RFC 1945 HTTP/1.0 | 1996년 5월 | Informational | §10.10 | — |
RFC 1945 는 어떤 종류의 인터넷 표준도 정하지 않는다고 스스로 밝힌 문서입니다. 그래도 이 필드는 이미 거기 있었습니다. RFC 1945 는 이 필드의 뜻을 받는 쪽이 어떻게 해석할지로 정의했습니다. 가진 사본이 Last-Modified 시각보다 오래됐으면 낡은 것으로 보라는 것입니다.
RFC 1945 는 구현마다 값이 갈린다고도 적습니다. 파일이면 파일 시스템의 수정 시각일 수 있습니다. 데이터베이스 게이트웨이면 레코드의 마지막 갱신 시각일 수 있습니다. 상세 절의 두 보기가 여기서 왔습니다. RFC 2068 본문은 확인하지 못했습니다(미확인).
요구 강도
MAY 는 해도 되고 안 해도 되는 선택입니다. MUST NOT 과 SHOULD NOT 은 각각 MUST 와 SHOULD 의 반대입니다. 표의 「받는 쪽」은 메시지를 받는 쪽입니다. 「요청을 받는 쪽」은 그 요청을 처리하는 원 서버나 중간 캐시입니다. Last-Modified 를 둘러싼 요구는 이렇게 갈립니다.
| 누가 | 무엇을 | 강도 | 절 |
|---|---|---|---|
| 원 서버 | 마지막 수정 시각을 합리적이고 일관되게 정할 수 있으면 보낸다 | SHOULD | RFC 9110 §8.8.2.1 |
| 원 서버 | 값을 Date 를 만드는 때에 되도록 가깝게 얻는다 | SHOULD | RFC 9110 §8.8.2.1 |
| 시계가 있는 원 서버 | Date 보다 늦은 값을 만들지 않는다 | MUST NOT | RFC 9110 §8.8.2.1 |
| 시계가 있는 원 서버 | 미래로 셈해진 값은 Date 로 바꿔 넣는다 | MUST | RFC 9110 §8.8.2.1 |
| 시계가 없는 원 서버 | 다른 시스템이 매긴 값이 아니면 만들지 않는다 | MUST NOT | RFC 9110 §8.8.2.1 |
| 보내는 쪽 | HTTP-date 를 IMF-fixdate 꼴로만 적는다 | MUST | RFC 9110 §5.6.7 |
| 받는 쪽 | HTTP-date 의 세 꼴을 모두 받아들인다 | MUST | RFC 9110 §5.6.7 |
| 원 서버 | GET·HEAD 의 200 OK 응답에 쓸 수 있는 검증자 필드를 싣는다. 강한 ETag 와 Last-Modified 를 둘 다 싣는 쪽이 선호된다 | SHOULD | RFC 9110 §15.3.1 |
| 요청을 받는 쪽 | If-None-Match 가 함께 오면 If-Modified-Since 를 무시한다 | MUST | RFC 9110 §13.1.3 |
| 요청을 받는 쪽 | If-Modified-Since 의 시각을 원 서버 시계 기준으로 해석한다 | MUST | RFC 9110 §13.1.3 |
| 원 서버 | If-Modified-Since 가 거짓이면 메서드를 실행하지 않고 304 를 만든다 | SHOULD NOT · SHOULD | RFC 9110 §13.1.3 |
| 원 서버 | If-Unmodified-Since 가 거짓이면 메서드를 실행하지 않는다 | MUST NOT | RFC 9110 §13.1.4 |
| 클라이언트 | ETag 가 없고 그 날짜가 강한 검증자일 때가 아니면 If-Range 에 날짜를 넣지 않는다 | MUST NOT | RFC 9110 §13.1.5 |
| 캐시 | 저장한 응답에 명시된 만료 시각이 있으면 짐작으로 신선도를 정하지 않는다 | MUST NOT | RFC 9111 §4.2.2 |
| 캐시 | 검증하는 저장 응답에 ETag 가 있으면 ETag 를 보낸다 | MUST | RFC 9111 §4.3.1 |
| 캐시 | 일부 범위 요청이 아니고, 저장된 응답 하나를 검증하며, 그 응답에 Last-Modified 가 있으면 If-Modified-Since 로 보낸다 | SHOULD | RFC 9111 §4.3.1 |
표준이 정하지 않고 남긴 것
표현의 마지막 수정 시각을 어떻게 셀지는 RFC 9110 이 구현에 맡깁니다. §8.8.2.1 이 그것을 이 문서의 범위 밖이라고 적습니다.
캐시가 저장한 응답을 얼마 동안 묻지 않고 내줘도 되는지 짐작하는 알고리즘도 정하지 않습니다. RFC 9111 §4.2.2 는 짐작한 값이 넘지 말아야 할 한도만 겁니다. 그 한도는 내용을 고친 뒤 흐른 시간의 일부입니다. 그 일부를 10퍼센트로 두는 것은 「흔한 설정일 수 있다」는 보기로만 나옵니다. 계산은 맞물림 절의 「캐시의 신선도 짐작」이 봅니다.
Last-Modified 를 강한 검증자로 볼지도 마지막에는 재량으로 판단합니다. 옛 판인 RFC 7232 §2.2.2 는 그 판단에 임의의 한도를 걸어 두었습니다. 캐시 항목의 Last-Modified 가 Date 보다 적어도 60초 앞서야 강하다고 볼 수 있게 한 것입니다.
두 값은 서로 다른 시계에서 나왔을 수 있습니다. 응답을 만드는 중 조금 다른 때에 매겨졌을 수도 있습니다. 60초는 그 가능성을 막으려고 임의로 정한 한도였습니다. 60초가 짧다고 보면 더 큰 값을 써도 됐습니다(MAY). RFC 9110 부록 B.4 는 이 한도를 합리적인 재량으로 풀었다고 적습니다. 지금 조건이 무엇인지는 경계 절이 판정합니다. 그 판단의 근거는 §8.8.2.2 입니다.
등록과 예시의 출처
필드 이름은 IANA(Internet Assigned Numbers Authority, 인터넷 번호 할당 기관)의 HTTP Field Name Registry 에 status permanent 로 올라 있습니다. 가리키는 절은 RFC 9110 §8.8.2 입니다.
예시 절의 값은 전부 RFC 9110 본문에 있는 것입니다. hello.txt 왕복은 §3.9, 날짜 세 꼴은 §5.6.7, If-Modified-Since 줄은 §13.1.3 의 것입니다.
예시
hello.txt 를 GET 한 응답
www.example.com 의 /hello.txt 를 GET 으로 요청한 왕복 한 벌입니다. 먼저 클라이언트의 요청입니다.
GET /hello.txt HTTP/1.1
User-Agent: curl/7.64.1
Host: www.example.com
Accept-Language: en, mi
서버의 응답입니다.
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain
Hello World! My content includes a trailing CRLF.
Date 와 Last-Modified 가 한 응답에 나란히 실렸습니다. 응답을 만든 때는 7월 27일 12시 28분 53초입니다. 파일을 마지막으로 고친 때는 7월 22일 19시 15분 56초입니다. 고친 때가 응답보다 나흘하고 17시간쯤 앞섭니다.
ETag 도 함께 실렸습니다. 검증자 두 가지를 다 싣는 보기입니다. 이 응답을 저장한 캐시는 다음 검증 요청에 ETag 를 반드시 싣습니다(MUST). 일부 범위 요청이 아니라면 Last-Modified 도 If-Modified-Since 로 함께 싣기를 권합니다(SHOULD).
같은 시각을 적는 세 꼴
같은 순간을 세 꼴로 적으면 이렇습니다.
Sun, 06 Nov 1994 08:49:37 GMT
Sunday, 06-Nov-94 08:49:37 GMT
Sun Nov 6 08:49:37 1994
첫째가 IMF-fixdate 입니다. 보내는 쪽이 쓰는 꼴은 이것 하나입니다. 둘째는 옛 RFC 850 꼴입니다. 요일 이름을 다 적고 연도를 두 자리로 줄입니다.
셋째는 C 언어의 asctime() 함수가 찍던 꼴입니다. 한 자리 날짜 앞에 빈칸이 하나 더 들어갑니다. 시간대 표시가 없습니다. 이 꼴은 UTC 로 간주합니다.
이 값을 되돌려 싣는 요청 줄
If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT
이 줄에는 쓰임이 둘 있습니다. 하나는 캐시가 저장해 둔 응답을 검증하는 것입니다. 다른 하나는 최근에 바뀐 것만 훑도록 범위를 좁히는 것입니다.
캐시가 검증할 때 값으로는 대개 저장해 둔 응답의 Last-Modified 를 씁니다. 위 hello.txt 응답을
저장한 캐시라면 Wed, 22 Jul 2009 19:15:56 GMT 를 적는 것이 보통입니다.
맞물림
Last-Modified 혼자서는 시각 하나를 알릴 뿐입니다. 쓰임은 이 값을 받아 가는 짝 헤더들이 만듭니다. 이 절은 원 서버가 보낸 시각을 누가 받아 어느 요청에 싣는지, 서버가 그것으로 무엇을 정하는지를 봅니다.
If-Modified-Since 와 304 Not Modified
캐시가 저장해 둔 응답을 그대로 내줘도 되는지 못 믿게 되면 원 서버에 물어 확인합니다. 이때 받아 둔 Last-Modified 를 If-Modified-Since 에 적는 것이 보통입니다. 이 조건은 GET 과 HEAD 에만 걸립니다. 다른 메서드에 붙어 오면 요청을 받는 쪽은 무시해야 합니다(MUST).
sequenceDiagram
participant C as 캐시
participant S as 원 서버
C->>S: GET
S-->>C: 200 OK · 본문 · Last-Modified
Note over C: 본문과 시각을 함께 저장한다
C->>S: GET · If-Modified-Since 받아 둔 시각
S-->>C: 304 Not Modified · 본문 없음
Note over C,S: 그 뒤로 고쳤다면 200 OK 와 새 본문이 온다
원 서버는 표현의 수정 시각이 필드 값보다 이르거나 같으면 조건을 거짓으로 봅니다. 그러면 메서드를 실행하지 않기를 권합니다(SHOULD NOT). 대신 304 Not Modified 를 만들기를 권합니다(SHOULD).
304 는 「가진 사본을 200 OK 응답의 본문처럼 써라」는 뜻입니다. 본문을 다시 보내지 않으므로 오가는 양이 헤더 몇 줄로 줄어듭니다.
304 를 만드는 쪽은 200 OK 에 실었을 필드 가운데 Content-Location · Date · ETag · Vary · Cache-Control · Expires 를 반드시 싣습니다(MUST). 그 밖에 내용 자체를 설명하는 필드는 싣지 않기를 권합니다(SHOULD NOT). 보낼 양을 줄이는 것이 304 의 목적이기 때문입니다. 다만 저장해 둔 사본의 헤더를 고쳐 두는 데 쓸모가 있으면 예외입니다. ETag 가 없는 응답에서 Last-Modified 가 그런 경우일 수 있습니다.
받아 둔 값을 손대지 않고 되돌려 보내는 데는 까닭이 있습니다. 원 서버의 시계를 고치거나 내용을 백업에서 되살리면 Last-Modified 가 시간을 거슬러 간 것처럼 보일 수 있습니다. 그래서 정확히 같은 시각만 받아 주는 서버가 있습니다. 받아 둔 값을 되돌려 보내면 그런 서버와도, 시계가 잘 안 맞는 환경에서도 가장 잘 맞물립니다.
If-Unmodified-Since 와 412 Precondition Failed
이번에는 쓰기 쪽입니다. POST · PUT · DELETE 처럼 상태를 바꾸는 요청에 If-Unmodified-Since 를 붙이면 「그 시각 뒤로 안 바뀌었을 때만 실행해 달라」는 뜻이 됩니다. 여럿이 같은 리소스를 함께 고칠 때 남이 먼저 고친 내용을 모르고 덮어쓰는 일을 막는 데 가장 자주 씁니다. 리소스가 ETag 를 주지 않을 때의 수단입니다.
원 서버는 표현의 수정 시각이 필드 값보다 이르거나 같으면 조건을 참으로 봅니다. 거짓이면 메서드를 실행하면 안 됩니다(MUST NOT). 412 Precondition Failed 로 실패를 알려도 됩니다(MAY). 같은 변경이 이미 반영된 것으로 보이면 2xx 성공 응답을 돌려줘도 됩니다(MAY).
ETag 쪽 짝인 If-Match 가 함께 오면 If-Unmodified-Since 는 무시합니다(MUST). If-Match 를 더 정확한 대체로 보기 때문입니다.
If-Range 와 끊긴 내려받기
Range 헤더는 내용의 일부만 달라는 요청입니다. 받다 끊긴 파일의 뒷부분을 이어 받을 때 씁니다. If-Range 에 Last-Modified 시각을 함께 실으면 판이 맞을 때만 일부를 받습니다. 조건이 맞고 Range 를 그 표현에 쓸 수 있으면 206 Partial Content 로 청한 일부만 옵니다. 판이 다르면 서버는 Range 를 무시합니다. 그러고 새 표현을 처음부터 끝까지 보냅니다. 412 를 돌려주지 않는 것이 If-Unmodified-Since 와 다른 점입니다.
flowchart TD
A["If-Range 에 날짜가 실려 왔다"] --> B{"그 날짜가 강한 검증자인가"}
B -- 아니다 --> X["Range 를 무시하고 전부 보낸다 · 200 OK"]
B -- 그렇다 --> C{"Last-Modified 와 정확히 같은가"}
C -- 다르다 --> X
C -- 같다 --> D{"Range 를 그 표현에 쓸 수 있는가"}
D -- 아니다 --> X
D -- 그렇다 --> Y["청한 일부만 보낸다 · 206 Partial Content"]
비교는 정확히 일치입니다. If-Unmodified-Since 의 「이르거나 같다」와 다릅니다. 이어 붙일 앞부분과 뒷부분이 같은 판이어야 하므로 날짜가 강한 검증자여야 합니다. 클라이언트는 ETag 가 없고 그 날짜가 강할 때가 아니면 If-Range 에 날짜를 넣으면 안 됩니다(MUST NOT).
ETag 와 함께 실릴 때
원 서버는 GET·HEAD 의 200 OK 응답에 쓸 수 있는 검증자 필드를 싣기를 권합니다(SHOULD). 그중에서도 강한 ETag 와 Last-Modified 를 둘 다 싣는 쪽이 선호됩니다. 두 검증자를 다 받은 캐시는 검증 요청에 ETag 를 반드시 보냅니다(MUST). 그래도 대개 Last-Modified 를 If-Modified-Since 로 함께 보냅니다. ETag 조건을 모르는 옛 중간 장치도 알맞게 답하게 하려는 것입니다.
한 요청에 조건 헤더가 여럿이면 원 서버와 캐시는 정해진 순서로 따져야 합니다(MUST). 날짜 쪽은 언제나 같은 일을 하는 ETag 쪽 뒤에 섭니다. ETag 가 날짜 검증자보다 정확하다고 보기 때문입니다.
| 순서 | 따지는 헤더 | 거짓이면 |
|---|---|---|
| 1 | If-Match · 원 서버가 받았을 때 | 412 |
| 2 | If-Unmodified-Since · 원 서버가 받았고 If-Match 가 없을 때 | 412 |
| 3 | If-None-Match | GET·HEAD 면 304, 나머지는 412 |
| 4 | If-Modified-Since · GET·HEAD 이고 If-None-Match 가 없을 때 | 304 |
| 5 | If-Range · GET 이고 Range 가 있을 때 | Range 를 무시하고 200 OK |
캐시의 신선도 짐작
신선도는 캐시가 저장한 응답을 원 서버에 묻지 않고 내줘도 되는 동안입니다. 원 서버가 만료 시각을 늘 주지는 않습니다. 그럴 때 캐시는 Last-Modified 같은 다른 필드로 그럴듯한 만료 시각을 짐작해도 됩니다(MAY).
저장한 응답에 명시된 만료 시각이 있으면 짐작해서는 안 됩니다(MUST NOT). 짐작을 쓸 수 있는 응답은 둘뿐입니다. 하나는 짐작으로 캐시해도 되는 상태 코드의 응답입니다. 다른 하나는 Cache-Control 의 public 지시어처럼 캐시해도 된다고 명시한 응답입니다.
Last-Modified 가 있으면 캐시는 그 시각 이후 흐른 시간의 일부를 넘지 않게 잡기를 권합니다. 그 비율의 흔한 설정은 10퍼센트일 수 있습니다. 예시 절의 hello.txt 응답은 그 응답의 Date 기준으로 고친 지 나흘하고 17시간쯤 지났습니다. 10퍼센트면 열한 시간 남짓입니다.
그동안 원 서버가 내용을 고쳐도 캐시는 묻지 않으므로 모릅니다. 오래 안 바뀐 내용은 앞으로도 한동안 안 바뀔 것이라는 어림에 기대는 것입니다.
경계
Last-Modified 는 초 단위 시각입니다. 그러면 언제나 약한 검증자일까요. 아닙니다. 요청에 실려 검증자로 쓰일 때 기본은 약한 것으로 봅니다. 하지만 강하다고 추론할 수 있는 세 경우에는 강한 검증자입니다.
약하다고 보는 까닭
강한 검증자는 GET 의 200 OK 응답 본문에서 보일 변화가 생길 때마다 값이 바뀝니다. 약한 검증자는 변화마다 값이 바뀌지는 않을 수 있습니다. 값을 셈하는 방식에 한계가 있으면 그럴 수 있습니다. 시계의 해상도가 그런 한계의 보기입니다.
수정 시각을 1초 단위로만 정한다면 약한 검증자일 수 있습니다. 1초 안에 표현이 두 번 바뀌고 그 사이에 누가 받아 갈 수 있으면 그렇습니다.
sequenceDiagram
participant S as 원 서버
participant C as 캐시
Note over S: 한 초 안에서 내용을 처음 고친다
C->>S: GET
S-->>C: 첫 판 · Date 와 Last-Modified 가 같은 초
Note over S: 같은 초 안에서 한 번 더 고친다
C->>S: GET · If-Modified-Since 그 초
S-->>C: 304 · 수정 시각이 그 초보다 늦지 않다
Note over C: 첫 판을 계속 내준다
두 번째 고침은 캐시에 닿지 않습니다. 그래서 약한 검증자는 앞서 받은 데이터와 똑같을 필요가 없는 쓰임에만 씁니다. 캐시 항목을 검증하거나 최근에 바뀐 것만 훑는 쓰임이 그렇습니다. 일부 범위를 이어 받거나 덮어쓰기를 막는 데는 강한 검증자가 필요합니다.
강하다고 볼 수 있는 세 경우
| 누가 비교하나 | 강하다고 볼 조건 |
|---|---|
| 원 서버가 표현의 지금 검증자와 비교할 때 | 그 시각이 덮는 1초 동안 표현이 두 번 바뀌지 않았음을 원 서버가 확실히 안다 |
| 클라이언트가 그 표현의 캐시 항목을 들고 If-Modified-Since · If-Unmodified-Since · If-Range 에 실으려 할 때 | 캐시 항목의 Date 가 Last-Modified 보다 1초 이상 뒤다. 그리고 둘이 같은 시계에서 나왔다고 믿을 이유가 있거나, 두 값의 차이가 커서 시계 맞춤 문제가 끼어들 것 같지 않다 |
| 중간 캐시가 자기 캐시 항목에 저장한 검증자와 비교할 때 | 둘째 줄과 같은 Date 조건이다 |
둘째 줄과 셋째 줄의 Date 조건이 왜 통하는지를 봅니다. 같은 초 안에 원 서버가 서로 다른 응답 둘을 같은 Last-Modified 로 보냈다면, 적어도 하나는 Date 가 Last-Modified 와 같습니다. 그 초가 다 지나기 전에 나간 응답이기 때문입니다. 거꾸로 Date 가 1초 이상 뒤인 응답은 그 초가 다 지난 뒤에 나간 것입니다. 같은 초 안의 두 판 사이에 끼어 나간 응답이 아닙니다.
flowchart TD
A["같은 초 안에 두 번 고쳤다"] --> B["그 초 안에 나간 응답 · Date 가 Last-Modified 와 같다"]
A --> C["그 초가 다 지난 뒤에 나간 응답 · Date 가 Last-Modified 보다 1초 이상 뒤"]
B --> D["두 판 사이에 끼어 나갔을 수 있다 · 약하다"]
C --> E["두 판이 다 반영된 뒤에 나갔다 · 강하다고 볼 수 있다"]
예시 절의 hello.txt 응답은 Date 가 Last-Modified 보다 나흘 넘게 뒤입니다. 이 응답을 저장한 클라이언트는 둘이 같은 시계에서 나왔다고 믿을 이유가 있으면 이 시각을 강한 검증자로 볼 수 있습니다. 나흘의 차이면 시계 맞춤 문제가 끼어들 것 같지 않다고 보아도 마찬가지입니다.
이 판정은 If-Range 에서 갈림길이 됩니다. ETag 가 없는 표현을 이어 받으려면 날짜가 강해야 합니다. 약하다고 판정하면 If-Range 에 날짜를 실을 수 없습니다.
관련 항목
이 시각과 함께 응답에 실리는 필드
ETag · Date · Expires · Cache-Control · Age · Vary · Content-Type
이 시각을 조건으로 적어 보내는 요청 헤더
If-Modified-Since · If-Unmodified-Since · If-Range · If-None-Match · If-Match · Range
조건을 따진 결과로 돌아오는 상태 코드
304 Not Modified · 412 Precondition Failed · 206 Partial Content · 200 OK · 상태 코드
이 값의 성질을 가르는 검증자 분류
검증자 · 약한 검증자 · 강한 검증자 · 엔티티 태그 · 불투명 검증자
이 값을 적는 날짜 형식
HTTP-date · IMF-fixdate · asctime · GMT · UTC · 타임스탬프
이 값을 만들 때 기대는 시계
시스템 시계 · 벽시계 · 시계 동기화 · NTP · 시간과 시계
이 값을 읽어 판단하는 캐시 개념
조건부 요청 · 재검증 · 신선도 · 휴리스틱 신선도 · 캐시 키 · HTTP 캐시 · 브라우저 캐시 · 공유 캐시 · CDN
이 시각으로 막는 동시 수정 문제
갱신 손실 · 낙관적 잠금 · 경쟁 상태 · Last Write Wins
이 값을 주고받는 서버와 중간 장치
오리진 서버 · 프록시 · 리버스 프록시 · 웹 서버 · Apache HTTP Server
이 필드를 정의하는 표준 문서
RFC 9110 · RFC 9111 · RFC 7232 · RFC 2616 · RFC 2068 · RFC 1945 · RFC 2119 · IETF · IANA
이것이 속하는 상위 분류
다른 이름: Last-Modified 헤더 · Last-Modified 필드 · 마지막 수정 시각