HTTP 캐시 — 같은 것을 두 번 나르지 않는다
Cache-Control: max-age 로 fresh 와 stale 을 손으로 계산하고, stale 이 되면 ETag 와 If-None-Match 의 조건부 GET 으로 확인해 304 라면 본문을 나르지 않는다 — 캐시 판정의 흐름을 시뮬레이터와 함께 몸에 익힌다.
갱신했는데 바뀌지 않는다·무거운 페이지가 한순간에 열린다 — 둘 다 캐시
CSS 를 고쳐 업로드했는데, 브라우저에서 보면 옛날 그대로. 그런가 하면, 처음에는 무겁던 페이지가 2 번째에는 한순간에 열린다. 좋든 나쁘든 뒤에 있는 것은 같은 구조 — 캐시입니다. 「같은 것을 두 번 나르지 않는다」는 Web 의 빠르기의 심장부이면서, 구조를 모르면 「왜인지 고쳐지지 않는다」의 최우선 원인이 됩니다.
다행히 HTTP 의 캐시는 헤더 읽는 법을 알면 무섭지 않습니다. 판정에 등장하는 물음은 2 가지뿐입니다.
Cache-Control: max-ageETagmax-age 의 손 계산 — fresh 인가 stale 인가는 뺄셈으로 정해진다
서버는 리스폰스에 이렇게 곁들일 수 있습니다: Cache-Control: max-age=1800. 의미는 「받고 나서 1800 초 동안은 신선(fresh)하다고 간주해도 좋다」입니다. 판정은 뺄셈뿐입니다.
구체적으로 돌려 봅니다. 10:00:00 에 max-age=1800(= 30 분)의 리스폰스를 수신했다고 하면, 10:20:00 시점의 경과는 1200 초. 1200 < 1800 이므로 아직 fresh, 남은 시간은 600 초입니다. 10:30:00 을 지나는 순간부터 stale 이 됩니다. 자주 보는 값의 환산도 손에 익혀 둡시다 — max-age=86400 은 1 일(86400 = 24 × 3600), max-age=604800 은 7 일, max-age=31536000 은 365 일입니다.
중요한 경계를 하나 더. stale = 사용 금지가 아닙니다. stale 은 「확인 없이는 쓸 수 없다」이고, 확인해서 바뀌지 않았다면 계속 쓸 수 있습니다. 이 「확인」의 구조가 다음 주역, 조건부 GET 입니다.
캐시 판정의 전체 흐름입니다. 왼쪽의 초록 출구(fresh: 통신 제로), 가운데의 파랑 출구(304: 헤더만), 오른쪽의 주황 출구(200: 전량 전송) — 아래의 시뮬레이터로 이 3 가지 출구를 오가 보세요.
연습 5-1 — max-age 를 손으로 계산한다
fresh 와 stale 의 판정은 덧셈과 뺄셈뿐입니다. 수신 시각·경과 초 수·max-age 를 종이에 적어 계산해 주세요.
Q1. 리스폰스에 Cache-Control: max-age=86400 이 붙어 있었습니다. 이 캐시가 fresh 로 있을 수 있는 기간은 몇 시간입니까.
max-age 의 단위는 초입니다. 86400 ÷ 3600 = 24 시간, 즉 꼬박 하루입니다. 86400 초 = 1 일은 캐시 설정의 빈출 값이므로, 604800 초 = 7 일과 함께 손에 익히면 설정 파일이 술술 읽히게 됩니다.
Q2. Cache-Control: max-age=1800 의 리스폰스를 10:00:00 에 수신했습니다. 10:20:00 시점에서, 이 캐시가 fresh 로 있을 수 있는 남은 시간은 몇 초입니까.
수신부터의 경과는 10:00:00 → 10:20:00 으로 20 분 = 1200 초. max-age=1800 초와의 차는 1800 − 1200 = 600 초입니다. 즉 10:30:00 이 되는 순간부터 stale 이 됩니다. 「경과 초 수와 max-age 를 비교할 뿐」— fresh 판정의 계산은 이것이 전부입니다.
Q3. 캐시가 「stale(신선하지 않음)」이 되었을 때의 올바른 이해는 어느 것입니까.
stale 은 「유통기한 만료」가 아니라 「확인 필요」의 상태입니다. 캐시 자체는 수중에 남아 있고, 서버에 조건부로 문의해 「바뀌지 않았다」(304)는 것을 알면 계속 쓸 수 있습니다. 즉시 삭제도 아니고, 확인 없이 계속 써도 되는 것도 아닙니다 — 이 「확인하고 나서 쓴다」는 구조가, 다음에 읽을 조건부 GET 입니다.
ETag 와 조건부 GET — 「바뀌었을 때만 주세요」
stale 이 된 캐시의 확인은, 이런 2 단 구성으로 이루어집니다. 먼저 최초의 리스폰스에서, 서버는 본문의 「지문」을 곁들여 둡니다.
HTTP/1.1 200 OK
Cache-Control: max-age=1800
ETag: "abc123"
Content-Length: 102400
ETag 는 그 시점의 본문의 판을 나타내는 짧은 식별자입니다. 기한이 지난 뒤, 브라우저는 보통의 GET 이 아니라 조건부 GET 을 보냅니다.
GET /style.css HTTP/1.1
Host: example.com
If-None-Match: "abc123"
If-None-Match 는 「수중의 판은 abc123 입니다. 이것과 다를 때만 본문을 주세요」라는 조건의 곁들임 메모입니다. 서버의 답은 2 가지로 나뉩니다.
여기서 전송량의 감각을 가져 주세요. fresh 는 리퀘스트조차 나가지 않는다(0 바이트). 304 는 리퀘스트는 나가지만 헤더만(수백 바이트). 200 은 전부(본문 통째로). 캐시 설계란, 이 3 가지 출구로의 배분을 설계하는 것입니다.
시뮬레이터로 확인한다
아래의 시뮬레이터는 1 개의 파일(본문 120 KB 로 가정)에 대한 캐시 판정을 재현합니다. max-age 와 경과 시간의 슬라이더, ETag 의 유무와 서버 쪽 갱신의 유무 토글을 움직여서, 리퀘스트는 나가는가·나간다면 조건부인가·리스폰스는 200 인가 304 인가·전송량은 얼마인가가 전환되는 모습을 확인해 주세요.
시뮬레이터의 동작 개요(텍스트판): max-age 슬라이더와 경과 시간 슬라이더를 움직이면, 경과 시간이 max-age 미만이라면 「fresh — 리퀘스트 없음·전송량 0 KB」로 판정됩니다. 경과 시간이 max-age 이상(stale)일 때, ETag 토글이 꺼져 있으면 「통상의 GET → 200 → 약 120.3 KB 전송」, ETag 토글이 켜져 있으면 「If-None-Match 를 붙인 조건부 GET」이 되어, 「서버 위에서 갱신이 있었다」 토글이 꺼져 있으면 「304 → 약 0.3 KB 전송」, 켜져 있으면 「200 → 약 120.3 KB 전송」으로 표시됩니다. 판정의 이유도 1 행씩 텍스트로 표시됩니다.
연습 5-2 — 조건부 GET 과 저장의 금지
ETag·If-None-Match·304 의 연계와, no-store / no-cache 의 구별입니다.
Q4. stale 이 된 캐시에 대해, 브라우저가 If-None-Match: W/"abc123" 을 붙인 GET 을 보냈습니다. 서버 위의 리소스가 바뀌지 않았을 경우, 서버의 응답으로 올바른 것은 어느 것입니까.
If-None-Match 는 「수중의 캐시의 ETag 는 abc123 입니다. 이것과 다를 때만 본문을 주세요」라는 조건부 부탁입니다. 일치했다면(바뀌지 않았다면), 서버는 304 Not Modified 를 본문 없이 돌려주고, 브라우저는 수중의 캐시를 계속 씁니다. 200 으로 전문을 돌려주는 것은 조건이 불일치(갱신됨)일 때이고, 404 는 리소스 부존재라는 별개의 이야기입니다.
Q5. 304 Not Modified 가 전송량의 절약이 되는 이유로 올바른 것은 어느 것입니까.
304 의 의미는 「당신 수중에 있는 것과 같습니다」— 같은 것을 다시 나를 필요가 없으므로, 리스폰스는 헤더만(기껏해야 수백 바이트)입니다. 100 KB 의 본문이라면, 전송은 대략 수백분의 1 이 됩니다. 다만 리퀘스트 자체는 매번 나가고 있다는 점에 주의해 주세요. 리퀘스트조차 나가지 않는 것은 fresh 인 동안뿐이고, 304 는 「나가지만, 가볍다」입니다.
Q6. Cache-Control: no-store 와 Cache-Control: no-cache 의 차이로 올바른 것은 어느 것입니까.
이름이 헷갈리는 대표격입니다. no-store 는 「애초에 저장하지 말라」(잔액 등 민감한 정보용), no-cache 는 「저장해도 되지만, 쓰기 전에 매번 확인하라」(304 가 돌아오면 수중의 사본을 쓸 수 있음). 엄격한 쪽은 no-store 입니다. 「no-cache = 캐시하지 않는다」는 직감이 사고의 원인이므로, 여기서 구별을 고정해 주세요.
no-store 와 no-cache — 이름에 속지 않는다
마지막으로, 이름이 헷갈리기로 유명한 2 가지 지시를 구별합니다. 둘 다 Cache-Control 에 적지만, 의미의 강도가 전혀 다릅니다.
직감에 반해서, 「캐시하지 말라」에 해당하는 것은 no-cache 가 아니라 no-store 입니다. no-cache 는 「캐시를 쓰기 전에 반드시 확인하라」이고, 304 와 조합하면 전송량은 절약됩니다. 이 혼동은 실무에서 정말 자주 봅니다 — 「no-cache 를 붙였는데 단말에 사본이 남아 있다」는 사양대로의 동작입니다.
이 장에서 가져갈 것
- 캐시 판정의 물음은 2 가지 — 아직 fresh 인가(max-age 와의 뺄셈), stale 이라면 내용은 바뀌었는가(조건부 GET)
- fresh 인 동안은 리퀘스트조차 나가지 않는다. stale 은 「사용 금지」가 아니라 「확인 필요」
- ETag + If-None-Match → 바뀌지 않았다면 304(본문 없음·헤더만). 출구는 3 가지: 0 바이트 / 수백 바이트 / 전량 전송
- no-store 는 저장 금지, no-cache 는 매번 재검증. 「캐시하지 말라」는 no-store 쪽
여기까지로 3 점 세트를 읽는 법이 한차례 갖춰졌습니다. 다음 장은 드디어 실물입니다 — curl 과 개발자 도구의 출력에, 이 읽는 법을 그대로 적용합니다.