stale-while-revalidate
고친 사람 github-actions[bot]
stale-while-revalidate 는 캐시가 기한이 막 지난 응답을 기다림 없이 먼저 내주도록 서버가 허락하는 지시어입니다. 캐시는 그 응답을 내주는 동안 뒤에서 서버에 새 값을 확인합니다. 사용자는 기다리지 않는 대신 잠깐 옛 값을 받을 수 있습니다.
쉽고 빠른 이해
기한이 지난 응답을 일단 내주게 하는 설정입니다. 새로 고치는 일은 뒤로 미룹니다.
응답에 max-age=600, stale-while-revalidate=30 이 붙어 있으면 캐시는 10분 동안 그 응답을 그냥 씁니다.
10분이 지난 뒤 30초 동안은 옛 응답을 바로 내줍니다. 그와 함께 뒤에서 서버에 새 값을 물어봅니다.
이 설정이 없으면 기한이 지난 직후에 온 요청이 손해를 봅니다. 캐시가 서버의 답을 받을 때까지 그 요청은 기다려야 합니다.
- 기한 안의 요청에는 저장해 둔 응답을 그냥 내줍니다
- 기한이 지났지만 30초 안이면 저장해 둔 응답을 바로 내줍니다. 서버 확인은 뒤에서 합니다
- 확인이 끝나면 새 응답으로 바꿔 둡니다. 30초가 다 지나면 다음 요청은 다시 기다립니다
대가는 옛 값입니다. 허락한 시간만큼 사용자가 바뀌기 전의 값을 볼 수 있습니다. 그 30초 안에 요청이 한 번도 안 오면 뒤에서 확인할 계기가 없어 효과도 없습니다.
상세
stale-while-revalidate 는 HTTP(HyperText Transfer Protocol) 응답의 Cache-Control 헤더에 적는 확장 지시어입니다. Cache-Control 은 캐시에게 줄 지시를 적어 보내는 헤더입니다. 그 안에 쉼표로 늘어놓는 항목 하나하나가 지시어입니다. HTTP 캐시의 본 규격 밖에서 더한 지시어라서 확장 지시어라고 부릅니다.
값은 초 단위의 0 이상 정수 하나입니다. 응답이 써도 되는 기한을 넘긴 뒤에도 그 초 동안은 캐시가 그 응답을 내줘도 된다는 허락입니다.
응답 헤더가 Cache-Control: max-age=600, stale-while-revalidate=30 이면 캐시는 그 응답을 600초 동안 그냥 씁니다.
600초가 지난 뒤에도 30초 동안은 그 응답을 더 내줄 수 있습니다.
허락을 내는 쪽은 오리진 서버입니다. 오리진 서버는 자원을 가지고 응답을 처음 만드는 서버입니다. 허락을 쓰는 쪽은 그 앞에서 응답을 저장해 두는 캐시입니다. 브라우저 캐시도 프록시도 그런 캐시입니다. 프록시는 클라이언트와 서버 사이에서 요청을 대신 전하는 서버입니다.
식당에 빗대 봅니다. 반찬이 조금 식었어도 손님에게 먼저 내놓습니다. 손님은 서서 기다리지 않습니다. 주방에서는 그사이 새 반찬을 데웁니다. 다음 손님부터 새 반찬을 받습니다.
신선한 응답과 낡은 응답
이 소절은 앞에서 「기한」이라 부른 것을 풉니다. 캐시는 저장한 응답마다 나이를 재서 기한을 따집니다.
나이는 오리진 서버가 응답을 만든 뒤로 흐른 시간입니다. 오리진 서버에 확인을 받았다면 그 확인 뒤로 흐른 시간입니다. 확인에 성공한 응답은 나이를 처음부터 다시 셉니다.
신선도 수명은 응답이 만들어진 때부터 만료 시각까지의 길이입니다. 서버는 max-age=600 처럼 이 길이를 초로
적어 보냅니다. 앞에서 기한이라 부른 것이 이 신선도 수명입니다.
나이가 신선도 수명을 넘지 않은 응답은 신선합니다. 캐시는 신선한 응답을 오리진 서버에 묻지 않고 내줄 수 있습니다. 나이가 신선도 수명을 넘은 응답은 낡은 응답입니다.
낡은 응답을 확인하는 재검증
낡은 응답을 버리고 처음부터 다시 받을 필요는 없을 수 있습니다. 그사이 오리진 서버의 값이 안 바뀌었을 수도 있어서입니다. 그럴 때 캐시는 「내가 가진 이 응답을 아직 써도 되나」를 오리진 서버에 묻습니다. 이 확인을 재검증이라고 부릅니다.
캐시는 이 질문을 조건부 요청으로 보냅니다. 조건부 요청은 캐시가 가진 응답의 버전을 실어 보내는 요청입니다. 버전을 가리키는 값으로는 ETag(entity tag) 헤더가 대표적입니다. 내용이 바뀌면 ETag 값도 바뀝니다.
오리진 서버는 둘 중 하나로 답합니다. 가진 응답을 계속 써도 되면 본문 없이 304 Not Modified 를 보냅니다. 그러면 캐시는 저장한 응답의 정보를 고쳐 적습니다. 그리고 그 응답을 다시 씁니다. 써서는 안 되면 서버가 본문을 담은 새 응답을 보냅니다.
창 안에서 캐시가 하는 일
stale-while-revalidate 가 붙은 응답은 신선도 수명이 끝난 뒤 지시어 값만큼 시간을 더 받습니다. 이 문서는 이 시간을 창이라 부릅니다. 앞 예의 값이면 600초부터 630초까지가 창입니다.
창 안의 규칙은 강도 낱말로 적혀 있습니다. 낱말마다 무게가 다릅니다.
| 낱말 | 뜻 |
|---|---|
| MUST | 반드시 지켜야 하는 요구. 어기면 규칙 위반입니다 |
| MUST NOT | 반드시 하지 말아야 하는 금지 |
| SHOULD | 권고. 이유가 있으면 어겨도 됩니다 |
| SHOULD NOT | 하지 않기를 권하는 것. 이유가 있으면 해도 됩니다 |
| MAY | 해도 되고 안 해도 되는 허락 |
창 안에서 캐시는 낡은 응답을 내줘도 됩니다(MAY). 그렇게 낡은 응답을 내준 캐시는 재검증을 시도하는 것이 좋습니다(SHOULD). 이 재검증은 요청을 막아 두지 않고 뒤에서 합니다. 이것을 백그라운드 재검증이라고 부릅니다.
창이 다 지나도록 재검증이 안 됐으면 그 응답을 더는 낡은 채로 내주지 않기를 권합니다(SHOULD NOT). 이 권고에는 「다른 정보가 없다면」이라는 단서가 붙습니다. 곧 이 권고는 다른 근거가 없을 때의 기본입니다. 낡은 응답을 허락하는 길이 이 지시어 하나만은 아닙니다(출처 문서 절의 「HTTP 캐시 본 규격과의 분담」).
응답이 지나는 세 구간
stale-while-revalidate 가 붙은 응답은 나이에 따라 세 구간을 차례로 지납니다. 아래 그림은
Cache-Control: max-age=600, stale-while-revalidate=30 이 붙은 응답의 구간입니다.
stateDiagram-v2
direction TB
신선: 신선 · 0초부터 600초 전까지
창: 창 · 600초부터 630초 전까지
창밖: 창 밖 · 630초부터
[*] --> 신선
신선 --> 창: 신선도 수명이 끝난다
창 --> 신선: 백그라운드 재검증이 성공한다
창 --> 창밖: 요청이 없거나 재검증이 결론 나지 않은 채 30초가 지난다
첫 구간에서는 응답을 그냥 내줍니다. 창에서는 낡은 응답을 바로 내줍니다. 재검증은 백그라운드로 보냅니다. 재검증이 성공하면 나이를 다시 세므로 응답은 신선해집니다.
창 안에 요청이 없었으면 30초 뒤 이 지시어는 더 작동하지 않습니다. 재검증이 결론 나지 않았을 때도 같습니다. 셋째 구간의 응답은 창의 허락도 끝난 낡은 응답입니다. 이 지시어가 없는 보통의 낡은 응답과 같습니다. 다음 요청은 막혀서 보통 요청처럼 처리됩니다. 재검증이 끝날 때까지 기다린다는 뜻입니다.
같은 응답에 요청이 오면 캐시가 하는 일은 응답의 나이에 따라 갈립니다.
| 응답의 나이 | 구간 | 캐시가 하는 일 |
|---|---|---|
| 300초 | 신선 | 바로 내준다 |
| 615초 | 창 | 바로 내준다. 재검증은 백그라운드로 보낸다 |
| 640초 | 창 밖 | 재검증이 끝날 때까지 요청을 기다리게 한다 |
창 안에 요청이 들어왔을 때
창 안에서 요청 하나가 어떻게 흐르는지를 보겠습니다. 사용자는 응답을 먼저 받습니다. 캐시와 오리진 서버 사이의 확인은 그 뒤에 진행됩니다.
sequenceDiagram
participant 사용자
participant 캐시
participant 서버 as 오리진 서버
사용자->>캐시: 요청
Note over 캐시: 나이 615초 · 창 안
캐시-->>사용자: 저장한 낡은 응답을 바로 준다
캐시->>서버: 조건부 요청으로 백그라운드 재검증
서버-->>캐시: 304 Not Modified 또는 새 응답
Note over 캐시: 저장한 응답을 갱신한다
그림에서 사용자 쪽 화살표가 오리진 서버 쪽 화살표보다 먼저 끝납니다. 이 지시어가 주는 것은 이 순서입니다. 사용자가 받은 응답은 확인 전의 값입니다. 서버의 값이 그사이 바뀌었다면 이 사용자는 옛 값을 봅니다.
창 안에 요청이 없을 때
이 소절은 창 안에 요청이 한 번도 안 오는 응답을 봅니다. 백그라운드 재검증은 창 안에 들어온 요청을 계기로 시작합니다. 요청이 없으면 재검증을 시작할 계기도 없습니다.
계기를 들어온 요청에 묶는 까닭은 증폭 공격입니다. 캐시가 사용자 요청 없이 제 판단으로 요청을 만들어 보내면 증폭 공격의 통로가 될 수 있습니다. 증폭 공격은 적은 요청으로 서버에 훨씬 많은 요청이 쏟아지게 만드는 공격입니다. 다른 일부 프리페치(미리 가져오기)나 자동 새로 고침 장치에서 이런 일을 볼 수 있습니다.
이 묶음은 MUST 나 SHOULD 가 아닌 제안입니다. 제안대로 묶은 캐시에서 백그라운드 재검증은 응답이 낡은 뒤, 창이 끝나기 전에 요청이 들어왔을 때만 일어납니다.
창의 길이와 그사이 요청이 올 가능성은, 모든 요청이 기다림 없이 나갈 가능성이 얼마인지를 정합니다. 창이 너무 짧거나 요청이 드물면 일부 요청이 창 밖으로 떨어집니다. 그 요청은 오리진 서버가 저장한 응답을 확인해 줄 때까지 기다립니다.
HTTP 밖에서 쓰는 같은 이름
같은 이름이 HTTP 헤더를 떠나 캐시 전략 하나를 가리킬 때도 있습니다. 화면에 데이터를 띄우는 라이브러리나 애플리케이션 안의 캐시가 이 이름을 씁니다. 뜻은 같습니다. 저장해 둔 값을 먼저 보여 주고 뒤에서 새 값을 가져와 바꿔 넣습니다.
출처 문서
stale-while-revalidate 의 정본은 RFC 5861 §3 입니다. RFC(Request for Comments) 는 인터넷 기술 문서에 번호를 붙여 내는 문서 묶음입니다. RFC 5861 의 제목은 「HTTP Cache-Control Extensions for Stale Content」입니다. 2010년 5월에 나왔습니다. 저자는 M. Nottingham 입니다.
RFC 5861 §4 는 짝인 stale-if-error 를 정의합니다. stale-if-error 는 오류를 만났을 때 낡은 응답을 내줘도 된다는 지시어입니다.
RFC 5861 은 인터넷 표준이 아니라 정보 제공용 문서입니다. 이 지시어의 허락이 표준 캐시 규칙 안으로 들어오는 길은 아래 「HTTP 캐시 본 규격과의 분담」의 RFC 9111 이 엽니다.
표지에 적힌 분류는 Informational, 곧 정보 제공용입니다. 인터넷 표준을 향해 단계를 밟는 문서의 분류는 Standards Track 입니다. RFC 5861 은 이 분류가 아닙니다.
RFC 는 누가 내느냐에 따라 발행 경로가 갈립니다. RFC 9111 처럼 IETF(Internet Engineering Task Force) 가 공동체 합의로 내는 경로가 그 하나입니다. RFC 5861 은 Independent Submission 으로 나왔습니다. 다른 어느 발행 경로와도 무관하게 RFC Editor 가 재량으로 펴낸 문서라는 뜻입니다.
RFC Editor 는 RFC 를 편집해 펴내는 곳입니다. RFC Editor 는 이 문서가 구현이나 배포에 얼마나 값어치가 있는지 아무 말도 하지 않는다고 적습니다. RFC Editor 의 기록으로 RFC 5861 은 폐기되거나 갱신된 적이 없습니다.
지시어 이름은 IANA(Internet Assigned Numbers Authority) 의 HTTP Cache Directive Registry 에 올라 있습니다.
IANA 는 인터넷 프로토콜이 쓰는 이름과 번호의 등록부를 관리하는 기구입니다. stale-while-revalidate 의 행은
RFC5861, Section 3 을 가리킵니다. stale-if-error 의 행은 RFC5861, Section 4 를 가리킵니다.
HTTP 캐시 본 규격과의 분담
HTTP 캐시의 본 규격은 RFC 9111 「HTTP Caching」입니다. 2022년 6월에 나온 Standards Track 문서입니다. STD(Standard) 98 번을 받았습니다. STD 번호는 인터넷 표준으로 확정된 문서에 붙는 번호입니다. RFC 9111 은 RFC 7234 를 폐기했습니다.
RFC 9111 은 이 문서가 「낡은 응답을 내준다」고 부른 일을 「낡은 응답을 만든다(generate a stale response)」로 적습니다. 두 말은 같은 일을 가리킵니다.
RFC 9111 §4.2.4 는 낡은 응답에 MUST NOT 을 두 번 겁니다. 첫째, 프로토콜 안의 지시어가 막으면 캐시는 낡은 응답을 내주면 안 됩니다. 예로 드는 지시어는 no-cache 와 must-revalidate, 그리고 그 캐시에 해당하는 s-maxage 와 proxy-revalidate 입니다. 넷 다 재검증을 거치기 전에는 낡은 응답을 다시 쓰지 못하게 합니다. 하나하나는 맞물림 절의 표에 있습니다.
둘째, 캐시가 끊겨(disconnected) 있지도 않고 명시적 허락도 없으면 낡은 응답을 내주면 안 됩니다. 명시적 허락은 클라이언트나 오리진 서버가 줍니다. RFC 9111 은 그 허락의 예로 RFC 5861 이 정의한 확장 지시어를 듭니다. stale-while-revalidate 는 본 규격이 비워 둔 「명시적 허락」에 들어가는 지시어입니다.
RFC 9111 §5.2.3 은 캐시가 모르는 지시어를 무시해야 한다고 정합니다(MUST). 이 지시어를 모르는 캐시는 같이 붙은 max-age 만 따라 돕니다. RFC 9111 은 이런 방식을 behavioral extension 이라 부릅니다. 새 지시어를 기존 지시어와 함께 보내, 모르는 쪽은 기존 동작을 하게 하는 방식입니다.
요구 강도와 절 번호
강도 낱말의 뜻은 RFC 2119 를 따릅니다(RFC 5861 §2). 낱말마다의 무게는 상세 절의 「창 안에서 캐시가 하는 일」 표에 있습니다.
| 무엇을 | 강도 | 절 |
|---|---|---|
| 창 안에서 낡은 응답을 내준다 | MAY | RFC 5861 §3 |
| 낡게 내주는 동안 막지 않고 재검증을 시도한다 | SHOULD | RFC 5861 §3 |
| 창이 지나도록 재검증이 안 되면 더 낡은 채로 내주지 않는다. 다른 정보가 없을 때 | SHOULD NOT | RFC 5861 §3 |
| 백그라운드 재검증을 들어온 요청에 묶는다 | 제안(suggested). 강도 낱말이 아니다 | RFC 5861 §5 |
| 오류를 만나면 다른 신선도 정보와 상관없이 낡은 응답을 쓴다 | MAY | RFC 5861 §4 |
| stale-if-error 가 정한 상한보다 낡으면 쓰지 않는다. 다른 정보가 없을 때 | SHOULD NOT | RFC 5861 §4 |
| 막는 지시어가 걸리면 낡은 응답을 내주지 않는다 | MUST NOT | RFC 9111 §4.2.4 |
| 끊김도 명시적 허락도 없으면 낡은 응답을 내주지 않는다 | MUST NOT | RFC 9111 §4.2.4 |
| 모르는 지시어는 무시한다 | MUST | RFC 9111 §5.2.3 |
기대는 문서의 폐기 사슬
RFC 5861 이 기대는 HTTP 문서는 1999년 6월의 RFC 2616 입니다. RFC 2616 은 RFC 7230 부터 7235 까지가 폐기했습니다. 그중 캐시를 맡은 RFC 7234 는 다시 RFC 9111 이 폐기했습니다.
flowchart TD
A["RFC 2616 · 1999"] -->|"7230 ~ 7235 가 폐기"| B["RFC 7234 · 캐시를 맡은 판"]
B -->|"9111 이 폐기"| C["RFC 9111 · 2022 · 지금의 본 규격"]
D["RFC 5861 · 2010"] -.->|"문법과 요구를 빌려 온 문서"| A
C -.->|"명시적 허락의 예로 든다"| D
RFC 5861 이 RFC 2616 에서 빌린 delta-seconds 규칙은 지금 RFC 9111 §1.2.2 에 있습니다. 초를 나타내는
0 이상 정수입니다. 문법은 delta-seconds = 1*DIGIT 입니다.
RFC 5861 §3 은 「낡은」 응답이 0 이 아닌 Age 헤더와 warning 헤더를 단다고 적습니다. HTTP 규격의 요구를 따른다는 단서가 붙어 있습니다. Age 는 응답이 만들어지거나 오리진 서버에 확인받은 뒤 흐른 시간을 초로 적는 응답 헤더입니다. Warning 은 상태 코드에 드러나지 않는 메시지 정보를 덧붙여 나르던 헤더입니다.
RFC 9111 §5.5 가 Warning 헤더를 폐기했습니다. 널리 생성되지도 사용자에게 드러나지도 않아서라고 적습니다. Warning 이 나르던 정보는 Age 같은 다른 헤더에서 얻을 수 있다고 덧붙입니다. 지금 그 문장에서 힘이 남은 쪽은 Age 입니다.
브라우저가 따르는 규칙
브라우저가 이 지시어를 받는 방식은 WHATWG(Web Hypertext Application Technology Working Group) 의 Fetch Standard 가 정합니다. WHATWG 는 브라우저가 따르는 웹 표준을 펴내는 단체입니다. Fetch Standard 는 브라우저가 요청을 보내고 응답을 받는 과정을 정한 문서입니다.
Fetch Standard 는 신선하지 않으면서 나이가 stale-while-revalidate 기간 안인 응답을 「stale-while-revalidate response」라 부릅니다. 참고문헌으로 RFC 5861 을 듭니다.
Fetch 의 요청에는 HTTP 캐시를 어떻게 쓸지 정하는 캐시 모드가 붙습니다. 그 가운데 "default" 모드는 네트워크로 가는 길에 HTTP 캐시를 먼저 들여다봅니다. 이 모드에서 캐시에 stale-while-revalidate response 가 있으면 그 응답을 돌려줍니다. 그와 함께 캐시 항목을 갱신할 조건부 네트워크 요청을 냅니다.
이 갱신 요청은 들어온 요청을 복제해 만듭니다. 그 응답은 지금 요청에 쓰이지 않습니다. 캐시만 고쳐 두고 다음에 캐시를 볼 때 쓰입니다.
Fetch Standard 는 이 길을 타는 조건에 「요청에 클라이언트가 딸려 있다」를 넣습니다. 클라이언트는 그 요청을 낸 쪽입니다. 갱신 요청도 그 클라이언트의 맥락에서 나갑니다. 그래서 클라이언트가 없어지면 갱신 요청도 끊깁니다. 어느 때를 없어진 것으로 보는지는 이 표준이 적지 않습니다(미확인).
곧 이 갱신 요청은 캐시가 혼자 띄우는 요청이 아닙니다. 들어온 요청을 계기로 재검증하라는 RFC 5861 §5 의 제안과 같은 방향입니다.
정하지 않은 것
RFC 5861 은 백그라운드 재검증을 어떤 장치로 띄우는지 정하지 않습니다. 같은 창에 요청이 여럿 들어오면 재검증을 몇 번 보내는지도 정하지 않습니다. 이 두 가지는 캐시를 구현하는 쪽이 정합니다.
CDN(Content Delivery Network) 은 오리진 서버 앞에 여러 사용자가 함께 쓰는 캐시를 깔아 두는 서비스입니다. CDN 하나가 두 빈칸을 어떻게 채웠는지는 예시 절의 Fastly 에 있습니다.
예시
RFC 5861 의 예시 값
Cache-Control: max-age=600, stale-while-revalidate=30
이 응답은 600초 동안 신선합니다. 그 뒤 최대 30초 더 낡은 채로 나갈 수 있습니다. 그동안 캐시는 비동기로 재검증을 시도합니다. 재검증이 결론 나지 않으면 30초 뒤 이 지시어는 멈춥니다. 재검증을 일으킬 요청이 없어도 마찬가지입니다.
Chrome 과 Firefox 의 브라우저 캐시
Cache-Control: max-age=1, stale-while-revalidate=59
현재 시각을 돌려주는 API(Application Programming Interface) 응답에 붙인 값입니다. 신선도 수명은 1초입니다. 창은 59초입니다. 같은 요청을 언제 다시 보내느냐에 따라 브라우저 캐시의 처리가 갈립니다.
| 다시 보낸 때 | 브라우저 캐시의 처리 |
|---|---|
| 1초 안 | 캐시 값이 신선합니다. 재검증 없이 그 값을 씁니다 |
| 1초에서 60초 사이 | 캐시 값은 낡았지만 그 요청에 씁니다. 백그라운드로 재검증 요청을 보내 다음을 위해 캐시를 채웁니다 |
| 60초 넘어 | 낡은 응답을 아예 안 씁니다. 응답도 캐시 갱신도 네트워크의 답을 기다립니다 |
백그라운드로 받은 응답은 내용이 같든 다르든 캐시에 저장됩니다. 저장된 응답은 앞의 것을 갈아 치웁니다. 신선도 타이머도 처음부터 다시 셉니다.
2019년 7월 기준으로 Chrome 75 와 Firefox 68 이 max-age 와 함께 붙은 이 지시어를 지원합니다. 지원하지 않는 브라우저는 이 값을 조용히 무시합니다. 그런 브라우저는 max-age 만 씁니다.
Fastly CDN
Fastly 는 CDN 입니다. 오리진 서버가 Fastly 에 이런 응답을 보냅니다.
Cache-Control: max-age=300, stale-while-revalidate=60
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Fastly 의 캐시는 이 응답을 5분 동안 보통대로 저장해 씁니다. 신선도 수명이 끝나면 최대 60초 더 같은 응답을 내줍니다. 단, 그 시간을 백그라운드에서 오리진 서버와 재검증하는 데 쓰는 동안만입니다.
새 응답이 오면 낡은 응답을 갈아 치웁니다. 그때부터는 새 응답의 신선도 규칙이 적용됩니다. 60초가 지나도록 새 응답을 못 받으면 낡은 응답은 이 방식으로는 더 쓰이지 않습니다.
같은 창에 요청이 여럿 들어오면 Fastly 는 요청을 리더(leader)와 팔로워(follower)로 나눕니다. 창 안의 낡은 응답에 들어온 요청 가운데 하나가 리더입니다. 나머지는 팔로워입니다. 캐시는 리더와 팔로워 모두에게 낡은 응답을 바로 내줍니다.
백그라운드 재검증은 리더 요청에서 갈라져 나간 요청 하나가 맡습니다. 요청이 여럿이어도 오리진 서버로 가는 재검증은 하나입니다.
sequenceDiagram
participant 리더 as 리더 요청
participant 팔로워 as 팔로워 요청
participant 캐시 as Fastly 캐시
participant 서버 as 오리진 서버
리더->>캐시: 요청
팔로워->>캐시: 요청
Note over 캐시: 창 안의 낡은 응답
캐시-->>리더: 낡은 응답을 바로 준다
캐시-->>팔로워: 낡은 응답을 바로 준다
캐시->>서버: 리더 요청에서 갈라진 백그라운드 재검증 하나
백그라운드 재검증이 실패하면 낡은 응답은 손대지 않고 남겨 둡니다. 받은 응답을 캐시할 수 없을 때도 같습니다. 이어 오는 요청들이 창이 끝날 때까지 재검증을 계속 시도합니다.
같은 응답에 stale-if-error=86400 을 더 붙인 값도 있습니다.
Cache-Control: max-age=300, stale-while-revalidate=60, stale-if-error=86400
이때는 60초 창이 끝난 뒤에도 낡은 응답이 더 남을 수 있습니다. stale-if-error 가 허락하는 기간이 더 길기 때문입니다. 86400초는 하루입니다.
배경
HTTP 캐시는 가진 응답 가운데 가장 최신 응답으로 답해야 했습니다. 낡은 응답은 「신중히 따져 본 경우」에만 내줄 수 있었습니다. 지금 규칙도 낡은 응답에는 허락을 요구합니다. 캐시가 끊겨 있지도 않고 클라이언트나 오리진 서버의 명시적 허락도 없으면, 낡은 응답을 내주면 안 됩니다(MUST NOT).
이 규칙 아래에서 기한이 막 지난 응답을 가진 캐시는 재검증이 끝날 때까지 요청을 붙잡아 둡니다. 그 요청은 네트워크 왕복과 오리진 서버의 처리 시간을 함께 기다립니다. 서버의 값이 안 바뀌어 304 로 끝나는 확인이어도 이 기다림은 똑같이 생깁니다. 아래 그림에서 사용자는 오리진 서버 왕복이 끝난 뒤에야 응답을 받습니다.
sequenceDiagram
participant 사용자
participant 캐시
participant 서버 as 오리진 서버
사용자->>캐시: 요청
Note over 캐시: 기한이 막 지났다 · 낡은 응답을 내줄 허락이 없다
캐시->>서버: 조건부 요청으로 재검증
서버-->>캐시: 304 Not Modified
캐시-->>사용자: 응답
필요한 것은 낡은 응답을 내줘도 된다는 명시적 허락이었습니다. 이 허락은 두 지시어로 갈라 정해졌습니다. stale-while-revalidate 는 재검증하는 동안 낡은 응답을 내줘, 네트워크와 서버 양쪽의 지연을 클라이언트에게서 감춥니다. stale-if-error 는 500 Internal Server Error, 네트워크 분할, DNS(Domain Name System) 실패 같은 오류를 만났을 때 오류 대신 낡은 응답을 내줘 가용성을 높입니다.
맞물림
이 절은 같은 Cache-Control 헤더를 쓰는 형제들과 이 지시어가 어떻게 붙는지를 봅니다. 상대는 max-age · 낡은 응답을 막는 지시어 넷 · stale-if-error · 재검증 요청 · Age 헤더입니다. 소절마다 누가 붙이고 누가 읽는지, 붙이면 무엇을 치르는지를 적습니다.
max-age
창은 max-age 가 정한 신선도 수명이 끝난 다음에 열립니다. 오리진 서버는 두 지시어를 한 응답에 같이 붙입니다. 캐시는 둘을 이어 읽습니다. 응답이 캐시에서 나갈 수 있는 가장 긴 기간은 두 값의 합입니다.
대개 서버는 이 합을 자기가 견딜 수 있는 가장 긴 기간에 맞추고 싶어 할 것입니다. 둘 다 600으로 두면 그 응답이 최대 20분 동안 캐시에서 나가도 서버가 견딜 수 있어야 합니다.
max-age=600 // 10분 신선
stale-while-revalidate=600 // 10분 더
캐시에서 나갈 수 있는 기간 // 최대 20분
치르는 것은 그 합만큼 옛 값이 나갈 수 있다는 것입니다. 그리고 이 지시어를 모르는 캐시는 max-age 만 읽습니다. 그런 캐시 앞에서는 창이 없는 응답과 똑같이 돕니다.
낡은 응답을 막는 지시어
어떤 지시어는 낡은 응답을 내주는 일 자체를 막습니다. 그런 지시어가 그 캐시에 걸리면 캐시는 낡은 응답을 내주면 안 됩니다(MUST NOT). 이때 stale-while-revalidate 의 허락은 쓰이지 않습니다.
먼저 낱말 둘을 풉니다. 공유 캐시는 프록시나 CDN 처럼 여러 사용자가 함께 쓰는 캐시입니다. 사설 캐시는 브라우저 캐시처럼 한 사용자만 쓰는 캐시입니다.
| 지시어 | 낡은 응답을 막는 방식 | 걸리는 캐시 |
|---|---|---|
no-cache |
오리진 서버에 재검증해 성공하기 전에는 다른 요청에 쓰면 안 된다. 낡은 응답을 내주도록 설정한 캐시도 마찬가지다 | 모든 캐시 |
must-revalidate |
낡은 뒤에는 재검증에 성공하기 전까지 다시 쓰면 안 된다 | 모든 캐시 |
proxy-revalidate |
must-revalidate 와 같은 금지 | 공유 캐시만 |
s-maxage |
공유 캐시의 최대 나이를 이 값으로 덮어쓴다. proxy-revalidate 의 뜻도 함께 품는다 | 공유 캐시만 |
s-maxage 는 공유 캐시의 수명을 정하는 지시어입니다. 그런데 이것을 붙이면 공유 캐시는 낡은 응답을 재검증 전에 다시 쓸 수 없습니다(MUST NOT). CDN 같은 공유 캐시 앞에서는 창이 열리지 않습니다.
낡은 응답에 요청이 왔을 때 캐시가 창을 쓸지는 세 물음을 차례로 거쳐 정해집니다.
flowchart TD
A["낡은 응답에 요청이 온다"] --> B{"캐시가 이 지시어를 아나"}
B -->|"모른다"| W["재검증이 끝날 때까지 기다린다"]
B -->|"안다"| C{"낡은 응답을 막는 지시어가 걸리나"}
C -->|"걸린다"| W
C -->|"안 걸린다"| D{"창 안인가"}
D -->|"아니다"| W
D -->|"그렇다"| S["바로 내주고 백그라운드 재검증"]
어느 물음에서든 창을 못 쓰는 쪽으로 걸리면 요청은 재검증을 기다립니다. 바로 내주는 길은 세 물음을 모두 통과했을 때 하나뿐입니다.
stale-if-error
stale-if-error 는 같은 RFC 가 함께 정의한 짝입니다. 둘 다 낡은 응답을 허락합니다. 허락하는 때가 다릅니다.
| stale-while-revalidate | stale-if-error | |
|---|---|---|
| 붙는 곳 | 응답 | 요청과 응답 둘 다 |
| 낡은 응답을 내주는 때 | 창 안에서 재검증하는 동안 | 500 · 502 · 503 · 504 로 답하게 될 오류를 만났을 때 |
| 값의 뜻 | 신선도 수명 뒤에 더 받는 초 | 낡음의 상한. 넘으면 다른 정보가 없는 한 그 응답을 쓰지 않기를 권한다(SHOULD NOT) |
| 겨냥하는 것 | 지연을 감춘다 | 가용성을 높인다 |
요청에 붙은 stale-if-error 는 그 요청 하나에만 걸립니다. 응답에 붙은 것은 그 저장된 응답을 쓰는 모든 요청에 걸립니다. stale-if-error 는 오류를 만났을 때 다른 신선도 정보와 상관없이 낡은 응답을 써도 된다는 허락입니다(MAY).
둘을 함께 붙이면 창이 끝난 뒤에도 오류 때는 낡은 응답이 더 나갈 수 있습니다. stale-if-error 가 정한 낡음의 상한이 창보다 길 때입니다. 예시 절의 Fastly 값이 그 조합입니다. 클라이언트 쪽 허락으로는 max-stale 이 있습니다. max-stale 은 클라이언트가 요청에 붙여 낡은 응답을 허락하는 지시어입니다.
재검증 요청과 304
백그라운드 재검증은 새로 만든 장치가 아닙니다. HTTP 캐시의 보통 재검증을 그대로 씁니다. 캐시가 조건부 요청을 보냅니다. 오리진 서버가 답합니다.
캐시가 재검증 요청을 만드는 길은 둘입니다. 하나는 지금 답하려는 요청에서 시작하는 길입니다. 다른 하나는 캐시가 스스로 요청을 띄울 때 저장한 응답으로 요청을 지어내는 길입니다.
지어낼 때는 저장한 응답에서 세 가지를 옮겨 적습니다. 메서드, 대상 URI(Uniform Resource Identifier), 그리고 Vary 헤더가 가리킨 요청 헤더입니다. Vary 는 응답이 어느 요청 헤더에 따라 달라지는지를 적는 헤더입니다.
답이 304 Not Modified 면 캐시는 저장한 응답의 정보를 고쳐 적습니다. 그리고 그 응답을 다시 씁니다. 본문을 담은 응답이 오면 조건부 요청에 실어 보낸 저장 응답이 맞지 않는다는 뜻입니다. 캐시는 그 새 응답을 저장해도 됩니다(MAY).
Age
캐시가 창 안에서 낡은 채로 내준 응답에는 0 이 아닌 Age 가 붙습니다. Age 는 응답이 만들어지거나 오리진 서버에 확인받은 뒤 흐른 시간을, 보내는 쪽이 어림해 초로 적는 헤더입니다. 받는 쪽은 이 값으로 응답이 얼마나 묵었는지 알아봅니다.
예전 HTTP 규격은 낡은 응답에 Warning 헤더도 달도록 요구했습니다. 그 Warning 은 널리 생성되지 않아 폐기됐습니다. 이제 묵은 정도를 알릴 단서는 Age 입니다.
관련 항목
stale-while-revalidate 를 정의하고 담는 표준 문서
RFC 5861 · RFC 9111 · RFC 9110 · RFC 7234 · RFC 2616 · RFC 2119 · WHATWG Fetch Standard
이 지시어의 이름과 문서를 관리하는 기구
IANA · IETF · RFC Editor · WHATWG
이 지시어가 실리거나 함께 읽히는 헤더
Cache-Control · Age · Expires · Warning · Vary
낡은 응답을 허락하거나 막는 다른 지시어
stale-if-error · max-stale · must-revalidate · proxy-revalidate · no-cache · max-age · s-maxage · min-fresh · immutable · no-store
이 지시어가 기대는 캐시 판정 개념
신선도 · 신선도 수명 · 나이 · 낡은 데이터 · 휴리스틱 신선도 · 재검증 · 검증자 · 조건부 요청 · 무효화 · delta-seconds
재검증에 쓰는 헤더와 상태 코드
ETag · Last-Modified · If-None-Match · If-Modified-Since · 304 Not Modified
이 지시어를 해석하는 캐시와 제품
HTTP 캐시 · 브라우저 캐시 · 공유 캐시 · 사설 캐시 · 프록시 · 리버스 프록시 · CDN · 오리진 서버 · Fastly · Chrome · Firefox
오리진 서버로 요청이 몰리는 문제와 그 대책
캐시 스탬피드 · 썬더링 허드 · 증폭 공격 · request collapsing
같은 기다림을 다른 방식으로 푸는 캐시 전략
refresh-ahead · cache-aside · read-through · 캐시 워밍
이 지시어가 줄이려는 지표
이것이 속하는 상위 분류
다른 이름: stale while revalidate · SWR · 스테일 와일 리밸리데이트