stale-while-revalidate
고친 사람 github-actions[bot]
stale-while-revalidate 는 캐시가 기한이 막 지난 응답을 기다림 없이 먼저 내주도록 허락하는 지시어입니다. 캐시는 그 응답을 내주는 동안 뒤에서 서버에 새 값을 확인합니다. 사용자는 기다리지 않는 대신 잠깐 옛 값을 받을 수 있습니다.
쉽고 빠른 이해
기한이 지난 응답을 일단 내주게 하는 설정입니다. 새로 고치는 일은 뒤로 미룹니다.
응답에 max-age=600, stale-while-revalidate=30 이 붙어 있으면 캐시는 10분 동안 그 응답을 그냥 씁니다.
10분이 지난 뒤 30초 동안은 옛 응답을 바로 내주면서 서버에 새 값을 물어봅니다.
이 설정이 없으면 기한이 지난 직후에 온 요청이 손해를 봅니다. 캐시가 서버의 답을 받을 때까지 그 요청은 기다려야 합니다. 서버의 처리가 길어지면 기한 직후에 요청을 보낸 사용자가 그 시간을 전부 기다립니다.
도는 순서는 이렇습니다.
- 기한 안의 요청에는 저장해 둔 응답을 그냥 내줍니다
- 기한이 지났지만 허락된 초 안이면, 저장해 둔 응답을 바로 내주고 뒤에서 서버에 확인합니다
- 확인이 끝나면 새 응답으로 바꿔 둡니다. 허락된 초까지 지나면 다시 기다리게 합니다
대가는 옛 값입니다. 허락한 초만큼 사용자가 바뀌기 전의 값을 더 오래 볼 수 있습니다. 그 초 안에 요청이 한 번도 안 오면 뒤에서 확인할 계기가 없어 효과도 없습니다.
상세
stale-while-revalidate 는 HTTP(HyperText Transfer Protocol) 응답의 Cache-Control 헤더에 적는 지시어입니다. 값은 초 단위 숫자 하나입니다. 응답이 써도 되는 기한을 넘긴 뒤에도, 그 초 동안은 캐시가 그 응답을 내줘도 된다고 서버가 허락하는 뜻입니다. 기한이 무엇인지는 바로 아래 소절에서 풉니다.
식당에 비유하면 이렇습니다. 반찬이 조금 식었더라도 손님을 세워 두지 않고 먼저 내놓습니다. 주방에서는 그사이 새 반찬을 데웁니다. 다음 손님부터 새 반찬을 받습니다.
신선한 응답과 낡은 응답
앞에서 말한 기한은 캐시가 응답의 나이로 따집니다. 이 소절은 그 신선도 판정만 짧게 짚습니다.
캐시는 저장한 응답마다 나이를 잽니다. 나이는 서버가 응답을 만든 뒤로 흐른 시간입니다. 나이를 재는 까닭은 오래된 응답일수록 서버의 값과 달라졌을 가능성이 커서입니다.
서버는 max-age=600 처럼 응답을 서버에 묻지 않고 써도 되는 길이를 정해 줍니다.
이 길이가 신선도 수명입니다. 앞에서 기한이라고 한 것이 바로 이것입니다.
나이가 신선도 수명보다 작으면 응답은 신선합니다. 캐시는 서버에 묻지 않고 바로 내줍니다. 나이가 신선도 수명에 닿으면 응답은 낡은 것이 됩니다. 이런 응답이 낡은 데이터입니다.
낡은 응답을 확인하는 검증
낡은 응답을 버리고 처음부터 다시 받으면 낭비일 수 있습니다. 서버의 값이 그사이 안 바뀌었을 수도 있어서입니다. 그래서 캐시는 서버에 「이 버전이 아직 최신인가」만 묻습니다. 이 확인을 재검증이라고 부릅니다.
버전을 물으려면 버전을 가리키는 값이 있어야 합니다. 이런 값을 검증자라고 합니다. 대표적인 검증자는 서버가 응답마다 붙여 주는 ETag(entity tag)입니다. 내용이 바뀌면 ETag 도 바뀝니다.
캐시는 저장한 응답의 검증자를 요청에 실어 보냅니다. 「내가 가진 것이 이 버전인데 아직 맞나」를 묻는 요청입니다. 이런 요청이 조건부 요청입니다.
서버는 버전을 비교해 둘 중 하나로 답합니다. 안 바뀌었으면 본문 없이 304 Not Modified 만 보냅니다. 캐시는 가진 응답을 계속 씁니다. 바뀌었으면 서버가 새 응답을 통째로 보냅니다.
기다림이 생기는 까닭
stale-while-revalidate 가 없으면 재검증은 요청을 붙잡아 둡니다. 낡은 응답만 가진 캐시는 서버의 답을 받은 뒤에야 요청에 답할 수 있습니다. 서버까지 왕복하는 시간과 서버가 처리하는 시간이 그 요청의 지연에 그대로 얹힙니다.
곤란한 점은 이 지연이 일부 요청에만 몰린다는 것입니다. 기한이 지난 뒤 처음 온 요청만 서버의 답을 기다립니다. 그 뒤 요청은 갱신된 응답을 곧바로 받습니다. 이렇게 드물게 길어지는 응답 시간은 꼬리 지연을 키웁니다. stale-while-revalidate 는 이 기다림을 사용자에게서 떼어 뒤로 옮깁니다.
응답이 지나는 세 구간
stale-while-revalidate 가 붙은 응답은 나이에 따라 세 구간을 차례로 지납니다. 아래 그림은
Cache-Control: max-age=600, stale-while-revalidate=30 이 붙은 응답의 구간입니다.
세 구간 가운데 둘째, 곧 낡았지만 허락된 30초를 이 문서에서는 창이라 부릅니다.
stateDiagram-v2
direction TB
신선: 신선 · 0초부터 600초 전까지
창: 낡았지만 허락된 창 · 600초부터 630초 전까지
낡음: 창 밖 · 630초부터
[*] --> 신선
신선 --> 창: 신선도 수명이 끝난다
창 --> 신선: 뒤에서 한 재검증이 성공한다
창 --> 낡음: 창 안에 요청이 없거나 재검증이 30초 안에 안 끝난다
첫 구간에서는 그냥 내줍니다. 창에서는 낡은 응답을 바로 내주면서 뒤에서 재검증을 보냅니다. 재검증이 성공하면 응답은 다시 신선해집니다. 창 안에 요청이 없었거나 재검증이 끝나지 않으면 셋째 구간으로 넘어갑니다.
셋째 구간에서는 허락이 더는 힘을 못 씁니다. 다음 요청은 재검증이 끝날 때까지 기다립니다. 지시어가 없는 응답이 낡았을 때와 같은 처리입니다.
같은 응답에 나이별로 요청이 오면 캐시는 이렇게 답합니다.
나이 300초 // 바로 내준다
나이 615초 // 바로 내주고 뒤에서 확인
나이 640초 // 확인이 끝날 때까지 기다림
창 안에 요청이 들어왔을 때
창 안에서 요청 하나가 어떻게 흐르는지를 보겠습니다. 사용자는 응답을 먼저 받습니다. 캐시와 오리진 서버 사이의 확인은 그 뒤에 따로 진행됩니다. 오리진 서버는 자원을 실제로 가진 서버입니다.
sequenceDiagram
participant 사용자
participant 캐시
participant 서버 as 오리진 서버
사용자->>캐시: 요청
Note over 캐시: 나이 615초 · 창 안
캐시-->>사용자: 저장한 낡은 응답을 바로 준다
캐시->>서버: 조건부 요청으로 확인
서버-->>캐시: 304 Not Modified 또는 새 응답
Note over 캐시: 저장한 응답을 갱신한다
그림에서 사용자 쪽 화살표가 서버 쪽 화살표보다 먼저 끝납니다. stale-while-revalidate 가 주는 것은 이것이 전부입니다. 사용자가 받은 응답은 확인 전의 값입니다. 서버의 값이 그사이 바뀌었다면 이 사용자는 옛 값을 봅니다.
캐시는 창 안에서 낡은 응답을 내주는 동안 재검증을 시도하는 것이 권장됩니다. 권장은 어겨도 규칙 위반까지는 아닌 수준입니다. 창이 끝났는데도 재검증이 안 됐으면 낡은 응답을 더는 내주지 않는 것이 권장됩니다.
창의 길이를 정하는 기준
응답이 묵을 수 있는 가장 긴 기간은 max-age 와 stale-while-revalidate 의 값을 더한 길이입니다.
예를 하나 바꿔 둘 다 600이라면, 캐시가 그 응답을 최대 20분 동안 내줄 수 있습니다. 그래서 두 값을 고를 때는 옛 값을 얼마나 오래 보여도
괜찮은지부터 정합니다.
max-age=600 // 10분 신선
stale-while-revalidate=600 // 10분 더
가장 긴 묵은 기간 // 20분
창이 짧거나 요청이 드물면 효과가 줄어듭니다. 뒤에서 하는 재검증은 창 안에 요청이 왔을 때만 시작되기 때문입니다. 창 안에 요청이 없으면 응답은 곧장 셋째 구간으로 넘어갑니다. 다음 요청은 기다립니다.
함께 붙이면 허락이 사라지는 지시어
어떤 지시어는 낡은 응답을 내주는 것 자체를 막습니다. 이런 지시어가 같이 붙으면 stale-while-revalidate 의 허락은 쓰이지 않습니다. 금지가 허락보다 앞서기 때문입니다.
| 지시어 | 낡은 응답을 막는 방식 |
|---|---|
no-cache |
내주기 전에 매번 재검증해야 한다 |
| must-revalidate | 낡은 뒤에는 재검증 없이 내줄 수 없다 |
proxy-revalidate |
프록시처럼 여러 사용자가 함께 쓰는 캐시에만 must-revalidate 와 같은 금지를 건다 |
낡은 응답을 다루는 다른 지시어
낡은 응답을 내줘도 되는 경우를 여는 지시어는 이것 말고도 있습니다. 누가 허락하는지와 어떤 때 허락하는지로 갈립니다.
| 지시어 | 누가 붙이나 | 언제 낡은 응답을 허락하나 |
|---|---|---|
| stale-while-revalidate | 서버가 응답에 | 뒤에서 재검증하는 동안 |
| stale-if-error | 서버나 클라이언트 | 서버가 오류로 답할 때 |
| max-stale | 클라이언트가 요청에 | 클라이언트가 그만큼 낡아도 받겠다고 할 때 |
stale-if-error 는 서버가 고장 나도 응답을 내주기 위한 지시어입니다. 서버가 500번대 오류를 낼 때 오류 대신 옛 응답을 내줍니다. stale-while-revalidate 는 속도를 위한 지시어라 서버가 멀쩡해도 작동합니다.
stale-while-revalidate 를 정한 문서
RFC(Request for Comments)는 인터넷 기술을 다루는 문서에 번호를 붙여 내는 문서 묶음입니다. stale-while-revalidate 는 2010년에 나온 RFC 5861 이 stale-if-error 와 함께 정의했습니다. RFC 5861 의 제목은 「HTTP Cache-Control Extensions for Stale Content」입니다.
RFC 에는 등급이 있습니다. 표준 트랙은 구현이 따라야 할 규칙으로 채택된 문서입니다. 정보 제공용 (Informational)은 방법을 알리기만 하고 따르라고 요구하지 않는 문서입니다.
RFC 5861 은 표준 트랙이 아니라 정보 제공용으로 나왔습니다. 그래서 모든 캐시가 stale-while-revalidate 를 알아듣는다고 가정할 수 없습니다. 모르는 캐시는 이 지시어를 무시하고 보통의 낡은 응답처럼 다룹니다. HTTP 캐시의 기본 규칙은 RFC 9111 이 따로 정합니다.
HTTP 밖에서 쓰는 같은 이름
이 이름은 HTTP 헤더를 떠나 캐시 전략 하나를 가리키는 말로도 쓰입니다. 뜻은 같습니다. 저장해 둔 값을 먼저 보여 주고, 뒤에서 새 값을 가져와 바꿔 넣습니다.
애플리케이션 안의 캐시나 화면에 데이터를 띄우는 라이브러리도 이 전략을 이 이름으로 부릅니다. 이때는 헤더의 초 단위 창 대신 각자 정한 조건으로 뒤쪽 갱신을 시작합니다. 흔한 조건은 사용자가 그 화면으로 다시 돌아왔을 때입니다. 화면에는 저장해 둔 목록을 먼저 띄우고, 그사이 서버에서 새 목록을 받아 바꿔 넣습니다. 기한이 지나기 전에 미리 갱신하는 refresh-ahead 와는 갱신을 시작하는 때가 다릅니다.
관련 항목
이 지시어를 정의하고 담는 표준 문서
RFC 5861 · RFC 9111 · RFC 9110 · RFC 7234 · RFC 2616
이 지시어가 들어가는 헤더
Cache-Control · Age · Expires · Warning
낡은 응답을 허락하거나 막는 다른 지시어
stale-if-error · max-stale · must-revalidate · proxy-revalidate · no-cache · max-age · s-maxage · min-fresh · immutable
이 지시어가 기대는 캐시 판정 개념
신선도 · 신선도 수명 · 나이 · 낡은 데이터 · 휴리스틱 신선도 · 재검증 · 검증자 · 조건부 요청 · 무효화
재검증에 쓰는 헤더와 상태 코드
ETag · Last-Modified · If-None-Match · If-Modified-Since · 304 Not Modified
이 지시어를 해석하는 캐시
HTTP 캐시 · 브라우저 캐시 · 공유 캐시 · 리버스 프록시 · CDN · 오리진 서버
같은 문제를 다른 방식으로 푸는 캐시 전략
refresh-ahead · cache-aside · read-through · request collapsing · 캐시 스탬피드 · 캐시 워밍
이 지시어가 줄이려는 지표
이것이 속하는 상위 분류
다른 이름: stale while revalidate · SWR · 스테일 와일 리밸리데이트