HTTP 캐시
고친 사람 github-actions[bot]
HTTP 캐시는 서버가 한 번 내준 응답을 받은 쪽에 저장해 두었다가, 같은 것을 다시 찾을 때 서버까지 가지 않고 그 사본으로 답합니다. 사본을 그냥 써도 되는지, 언제 다시 물어봐야 하는지는 서버가 응답에 적어 보낸 규칙이 정합니다. 브라우저 안에도 있고, 여러 사람의 요청이 함께 지나가는 중간 서버에도 있습니다.
쉽고 빠른 이해
HTTP 캐시는 웹에서 주고받은 응답을 곁에 저장해 두고 다시 쓰는 장치입니다. 같은 로고 이미지를 화면을 넘길 때마다 서버에서 새로 받아오지 않는 것이 이 장치가 해 주는 일입니다.
없으면 두 가지가 곤란합니다. 안 바뀐 내용을 볼 때마다 다시 내려받느라 기다리는 시간이 늘어납니다. 그리고 서버는 같은 응답을 만드는 일을 사람 수만큼 되풀이합니다.
어떻게 도는가:
- 서버가 응답을 보내면서 이 응답을 얼마나 오래 써도 되는지 함께 알려 줍니다
- 받은 쪽은 응답을 저장해 두고, 같은 것을 다시 찾으면 저장해 둔 사본을 먼저 봅니다
- 써도 되는 기간이 지나면 서버에 바뀐 게 있는지만 짧게 물어봅니다
대가도 있습니다. 사본은 복사본이라 원본이 바뀌어도 그 사실을 모릅니다. 그래서 잠깐 옛 내용을 보게 되는 때가 생기고, 문제가 났을 때 어느 사본이 답했는지 짚기가 번거로워집니다.
상세
자주 들춰 보는 서류를 볼 때마다 본사 문서고까지 가는 대신, 복사해서 책상 서랍에 넣어 두는 모습을 떠올려 봅니다. 다음부터는 서랍만 열면 되니 걸음이 줄어듭니다.
HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 규약)는 브라우저 같은 요청하는 쪽과 서버가 요청과 응답을 주고받는 규약입니다. HTTP 캐시는 먼저 그 응답을 담아 두는 저장소를 가리킵니다. 저장소만이 아니라 무엇을 담을지, 담아 둔 것을 언제 그냥 내줄지 고르는 판단까지 함께 가리킵니다.
응답을 처음 만들어 내주는 쪽을 오리진 서버라고 부릅니다. 캐시를 두는 까닭은 오리진 서버까지 다녀오는 값이 비싸기 때문입니다. 네트워크를 건너가는 시간이 들고, 서버는 응답을 만들려고 다시 계산하거나 데이터베이스를 읽습니다.
캐시를 두는 것 자체는 선택입니다. 캐시가 하나도 없어도 웹은 돌아갑니다. 규칙이 정하는 것은 캐시를 둔다면 무엇을 저장해도 되고 언제 그것을 내줘도 되는지입니다.
사설 캐시와 공유 캐시
캐시는 누가 쓰느냐로 갈립니다. 브라우저가 자기 안에 저장해 두는 브라우저 캐시는 그 사람 하나만 씁니다. 이런 캐시를 사설 캐시라고 부릅니다.
프록시나 리버스 프록시, CDN(Content Delivery Network, 콘텐츠 전송 네트워크)처럼 여러 사람의 요청이 함께 지나가는 중간 서버도 응답을 저장합니다. 이쪽은 한 사람이 받아 온 응답을 다음 사람에게도 내주므로 공유 캐시라고 부릅니다.
flowchart TD
B["브라우저"] --> P["사설 캐시 · 한 사람만 쓴다"]
P --> S["공유 캐시 · 여럿이 함께 쓴다"]
S --> O["오리진 서버 · 응답을 만든다"]
그림처럼 요청은 위에서 아래로 내려가다가 사본을 가진 캐시를 만나면 거기서 돌아섭니다. 둘을 갈라 부르는 까닭은 저장해도 되는 응답이 다르기 때문입니다. 한 사람에게만 보여야 할 내용이 공유 캐시에 들어앉으면 다음 사람이 그것을 받습니다.
사본을 고르는 캐시 키
캐시는 요청이 올 때마다 저장해 둔 것 가운데 무엇을 내줄지 골라야 합니다. 고르는 데 쓰는 값을 캐시 키라고 합니다.
기본 재료는 두 가지입니다. 요청 메서드와 요청이 가리키는 URL(Uniform Resource Locator, 자원 위치 주소)입니다. 대상이 되는 것은 주로 GET 응답입니다. 같은 주소를 다시 읽으면 같은 내용이 오리라고 기대할 수 있기 때문입니다.
주소가 같아도 응답이 갈릴 때가 있습니다. 같은 주소에 한국어 판과 영어 판이 따로 있는 것이 그런 경우입니다. 요청에 따라 다른 내용을 내주는 일은 콘텐츠 협상이 맡습니다.
이런 응답은 주소만으로 고르면 엉뚱한 것을 내주게 됩니다. 그래서 서버는 어떤 요청 정보를 함께 봐야 하는지를 알려 줍니다.
그 알림은 헤더를 타고 옵니다. 요청과 응답에는 본문 말고도 이름과 값이 한 줄씩 딸려 옵니다. 이 줄들을 묶어 헤더라고 하고, 한 줄 하나를 필드라고 부릅니다.
Vary 라는 필드가 그 알림을 맡습니다. 캐시는 거기 적힌 헤더까지 캐시 키에 넣어, 언어가 다른
요청에는 다른 사본을 내줍니다.
신선도와 시한
저장해 둔 사본을 오리진 서버에 다시 물어보지 않고 써도 되는 동안을 신선하다고 합니다. 이 상태를 가리키는 이름이 신선도입니다. 시한을 넘긴 사본은 낡았다고 합니다.
시한은 서버가 응답에 적어 보냅니다. Cache-Control 필드의 max-age 가 저장한 뒤 몇 초 동안
신선한지를 적습니다. 언제까지 신선한지를 시각으로 적는 Expires 필드도 있습니다.
사본이 저장된 뒤 흐른 시간을 나이라고 부릅니다. 캐시는 나이가 시한 안이면 오리진 서버를 건드리지 않고 사본을 그대로 내줍니다. 이때가 캐시가 값을 내는 때입니다.
시한을 길게 잡으면 오리진 서버에 가는 횟수가 줄고, 대신 원본이 바뀐 뒤에도 옛 내용을 보는 시간이 길어집니다. 이렇게 손에 남은 옛 내용을 낡은 데이터라고 합니다.
낡은 사본을 되살리는 조건부 요청
시한이 지났다고 사본이 쓸모없어진 것은 아닙니다. 그 사이 원본이 안 바뀌었을 수 있습니다. 그래서 캐시는 사본을 버리는 대신, 바뀐 게 있는지만 물어봅니다.
이 물음에는 조건부 요청을 씁니다. 서버는 응답을 보낼 때 그 판을 가리키는 짧은 표식을 함께
줍니다. 표식으로는 내용마다 붙는 ETag 나 마지막으로 고친 시각인 Last-Modified 를 씁니다.
GET /logo.png // 같은 주소를 다시
If-None-Match: "v7" // 손에 든 판의 표식
위 요청은 「내가 든 판이 v7 인데 그새 바뀌었나」를 묻는 것입니다. 서버는 바뀌지 않았으면 내용을 빼고 짧은 응답만 돌려줍니다.
304 Not Modified // 본문 없이 이 줄만
그러면 캐시는 들고 있던 사본을 다시 신선한 것으로 삼아 내줍니다. 본문을 다시 나르지 않으므로 오가는 양이 크게 줄어듭니다. 한 왕복은 여전히 드는 값입니다.
sequenceDiagram
participant 브라우저
participant 캐시
participant 오리진 as 오리진 서버
브라우저->>캐시: 같은 주소를 다시 요청
Note over 캐시: 사본이 신선하면 여기서 끝난다
캐시->>오리진: 낡았다 · 그새 바뀌었나
오리진-->>캐시: 안 바뀌었다 · 본문 없음
캐시-->>브라우저: 들고 있던 사본
그림의 갈림이 캐시가 하는 판단의 전부입니다. 신선하면 혼자 답하고, 낡았으면 확인만 하러 다녀오고, 정말 바뀌었으면 새 응답을 받아 저장합니다.
낡은 사본을 일단 내주고 확인은 그 뒤에 하는 방식도 있습니다. 기다리는 시간을 줄이는 쪽을 고른 것이고, 이름은 stale-while-revalidate 입니다.
저장하면 안 되는 응답
모든 응답이 저장 대상은 아닙니다. 사람마다 다른 내용이거나, 로그인한 사람에게만 보여야 하는 내용이 그렇습니다.
서버는 그런 응답에 저장하지 말라는 표시를 붙입니다. Cache-Control 의 no-store 는 아예 남기지
말라는 뜻이고, private 는 그 사람의 브라우저까지만 두고 공유 캐시에는 두지 말라는 뜻입니다.
이 표시를 빠뜨리면 남의 계정 화면이 다음 사람에게 내려가는 사고가 납니다. 캐시는 스스로 판단하지 않고 서버가 붙인 표시대로만 움직입니다. 그래서 무엇을 감출지는 응답을 만든 쪽이 정해 줘야 합니다.
흩어진 사본을 거둬들이는 무효화
저장된 사본을 더는 못 쓰게 만드는 일을 무효화라고 합니다. 문제는 사본이 한 벌이 아니라는 것입니다. 브라우저마다, 중간 서버마다 흩어져 있습니다.
캐시는 자기를 지나간 요청이 건드린 것만 무효로 돌릴 수 있습니다. 그 요청이 지나지 않은 캐시에 남은 사본에는 손이 닿지 않습니다. 남의 브라우저 안에 든 사본은 특히 그렇습니다.
그래서 실무에서 많이 쓰는 방법은 반대쪽입니다. 내용이 바뀌면 주소를 바꿉니다. 파일 이름에 판을 가리키는 값을 넣어 두면, 새 내용은 새 주소가 되어 옛 사본과 겹치지 않습니다. 옛 주소의 사본은 아무도 찾지 않으니 시한이 지나면 조용히 밀려납니다.
캐시를 붙일 응답과 미룰 응답
값이 큰 쪽은 정해져 있습니다. 누가 요청해도 같은 내용이 오고, 자주 읽히고, 만드는 데 값이 드는 응답입니다. 이미지·스크립트·스타일 시트 같은 파일이 여기 해당합니다.
읽는 사람마다 내용이 갈리는 응답은 공유 캐시에 두기 어렵습니다. 초 단위로 최신이어야 하는 응답도 마찬가지입니다. 이럴 때는 저장을 막거나, 시한을 짧게 두고 확인을 자주 하는 쪽으로 갑니다.
백엔드가 내주는 응답도 대상이 됩니다. 다만 저장할지 말지를 정하는 쪽은 그 응답을 만든 서버입니다. 어떤 응답에 어떤 시한을 붙일지는 코드를 짜는 사람이 미리 정해 두어야 합니다.
아무 표시도 안 붙이면 중간 캐시들이 저마다 다르게 판단합니다. 그래서 캐시를 안 쓸 생각이어도 안 쓴다는 표시는 붙여 두는 편이 안전합니다.
관련 항목
HTTP 캐시의 규칙을 정하는 표준 문서
RFC 9111 · RFC 9110 · HTTP · HTTP-2 · HTTP-3 · RFC 7234
HTTP 캐시가 놓이는 곳으로 갈리는 종류
브라우저 캐시 · 사설 캐시 · 공유 캐시 · 프록시 캐시 · CDN · 리버스 프록시
HTTP 캐시 앞뒤에 서는 서버와 중계 장비
오리진 서버 · 프록시 · 게이트웨이 · 로드 밸런서 · Cloudflare · nginx
HTTP 캐시가 저장한 응답이 갖는 상태
신선도 · 낡은 데이터 · 나이 · TTL · 만료 · 캐시 미스
HTTP 캐시가 사본을 고를 때 보는 요청 정보
캐시 키 · 요청 메서드 · URL · Vary · 콘텐츠 협상 · 캐시 가능성
HTTP 캐시의 동작을 조정하는 헤더 필드
Cache-Control · ETag · Last-Modified · Expires · Age · 헤더
HTTP 캐시가 원본과 어긋났을 때 거치는 절차
무효화 · 조건부 요청 · 재검증 · 검증자 · stale-while-revalidate · 퍼지
HTTP 캐시에서 터지는 문제
캐시 스탬피드 · 썬더링 허드 · 캐시 포이즈닝 · 캐시 오염 · request collapsing
HTTP 캐시가 다루는 응답을 만드는 요청 메서드
GET · POST · PUT · DELETE · 안전한 메서드 · 멱등성
같은 원리를 다른 계층에서 쓰는 캐시와 그 전략
캐싱 · CPU 캐시 · 메모이제이션 · cache-aside · read-through · write-through
다른 이름: HTTP cache · 웹 캐시 · web cache