합동회사 코무라소프트
5장

HTTP 캐시 — 같은 것을 두 번 나르지 않는다

Cache-Control: max-age 로 fresh 와 stale 을 손으로 계산하고, stale 이 되면 ETag 와 If-None-Match 의 조건부 GET 으로 확인해 304 라면 본문을 나르지 않는다 — 캐시 판정의 흐름을 시뮬레이터와 함께 몸에 익힌다.

갱신했는데 바뀌지 않는다·무거운 페이지가 한순간에 열린다 — 둘 다 캐시

CSS 를 고쳐 업로드했는데, 브라우저에서 보면 옛날 그대로. 그런가 하면, 처음에는 무겁던 페이지가 2 번째에는 한순간에 열린다. 좋든 나쁘든 뒤에 있는 것은 같은 구조 — 캐시입니다. 「같은 것을 두 번 나르지 않는다」는 Web 의 빠르기의 심장부이면서, 구조를 모르면 「왜인지 고쳐지지 않는다」의 최우선 원인이 됩니다.

다행히 HTTP 의 캐시는 헤더 읽는 법을 알면 무섭지 않습니다. 판정에 등장하는 물음은 2 가지뿐입니다.

물음 1 — 아직 신선(fresh)한가?
신선한 동안은, 리퀘스트조차 보내지 않고 수중의 사본을 쓴다. 신선함의 기한을 정하는 것이 Cache-Control: max-age
물음 2 — 기한이 지났다(stale)면, 내용은 바뀌었는가?
서버에 조건부로 확인하여, 바뀌지 않았다면 「그대로 쓰라」(304), 바뀌었다면 새것(200)이 도착한다. 확인의 표식이 ETag

max-age 의 손 계산 — fresh 인가 stale 인가는 뺄셈으로 정해진다

서버는 리스폰스에 이렇게 곁들일 수 있습니다: Cache-Control: max-age=1800. 의미는 「받고 나서 1800 초 동안은 신선(fresh)하다고 간주해도 좋다」입니다. 판정은 뺄셈뿐입니다.

경과 시간 < max-age → fresh
리퀘스트를 보내지 않고, 수중의 캐시를 즉시 쓴다. 통신 제로, 최속
경과 시간 ≥ max-age → stale
「확인 필요」의 상태. 버리는 것이 아니라, 서버에 확인하고 나서 쓴다

구체적으로 돌려 봅니다. 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 인가 stale 인가, ETag 의 유무로 분기하여 리퀘스트 없음·304·200 중 하나에 이르는 HTTP 캐시 판정의 플로차트

캐시 판정의 전체 흐름입니다. 왼쪽의 초록 출구(fresh: 통신 제로), 가운데의 파랑 출구(304: 헤더만), 오른쪽의 주황 출구(200: 전량 전송) — 아래의 시뮬레이터로 이 3 가지 출구를 오가 보세요.

연습 5-1 — max-age 를 손으로 계산한다

fresh 와 stale 의 판정은 덧셈과 뺄셈뿐입니다. 수신 시각·경과 초 수·max-age 를 종이에 적어 계산해 주세요.

Q1. 리스폰스에 Cache-Control: max-age=86400 이 붙어 있었습니다. 이 캐시가 fresh 로 있을 수 있는 기간은 몇 시간입니까.

시간

Q2. Cache-Control: max-age=1800 의 리스폰스를 10:00:00 에 수신했습니다. 10:20:00 시점에서, 이 캐시가 fresh 로 있을 수 있는 남은 시간은 몇 초입니까.

Q3. 캐시가 「stale(신선하지 않음)」이 되었을 때의 올바른 이해는 어느 것입니까.

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 가지로 나뉩니다.

바뀌지 않았다 → 304 Not Modified
본문 없음·헤더만의 답장. 「수중의 것을 그대로 써도 좋다」. 100 KB 의 본문이라도 전송은 수백 바이트로 끝난다
바뀌었다 → 200 OK
새 본문과 새 ETag 가 도착한다. 캐시를 교체한다

여기서 전송량의 감각을 가져 주세요. 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 을 보냈습니다. 서버 위의 리소스가 바뀌지 않았을 경우, 서버의 응답으로 올바른 것은 어느 것입니까.

Q5. 304 Not Modified 가 전송량의 절약이 되는 이유로 올바른 것은 어느 것입니까.

Q6. Cache-Control: no-storeCache-Control: no-cache 의 차이로 올바른 것은 어느 것입니까.

no-store 와 no-cache — 이름에 속지 않는다

마지막으로, 이름이 헷갈리기로 유명한 2 가지 지시를 구별합니다. 둘 다 Cache-Control 에 적지만, 의미의 강도가 전혀 다릅니다.

no-store — 저장하지 말라
캐시에의 저장 자체를 금지. 계좌 잔액이나 개인 정보 등, 사본이 어디에도 남지 않기를 바라는 리스폰스에 쓴다. 가장 엄격
no-cache — 저장해도 되지만, 매번 확인하라
저장은 허용하지만, 쓰기 전에 매번 서버에의 재검증을 필수로 한다. ETag 가 일치해 304 가 돌아오면 수중의 사본을 쓸 수 있다. 「max-age=0 으로 항상 확인 필요」에 가까운 동작

직감에 반해서, 「캐시하지 말라」에 해당하는 것은 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 과 개발자 도구의 출력에, 이 읽는 법을 그대로 적용합니다.