캐시와 조건부 요청
캐시 기본 동작
캐시가 없을 때
웹 브라우저에서 star.jpg를 요청한다면, 서버에서는 star.jpg가 있으면 응답을 내려준다.
응답에는 헤더와 실제 이미지(star.jpg)와 관련된 바이트 코드들이 있다.
이때, 헤더의 용량이 0.1M이고 실제 이미지를 표현하는 바디 부분은 1.0M일 때, 총 1.1M의 네트워크를 차지하면서 웹 브라우저에 내려가게 된다.
웹 브라우저는 이 이미지를 화면에 표시한다.
이후 클라이언트(웹 브라우저)에서 똑같은 요청을 한다면 서버에서는 헤더와 바디 부를 다시 만들어서 1.1M 용량을 내려주면, 웹 브라우저는 받아서 사용한다.
- 데이터가 변경되지 않아도 계속 네트워크를 통해서 데이터를 다운로드 받아야 한다.
- 인터넷 네트워크는 상대적으로 PC의 메모리나 하드 디스크에 비해 매우 느리고 비싸다.
- 브라우저 로딩 속도가 느리다.
- 느린 사용자 경험
캐시 적용
먼저, 서버에서 캐시와 관련된 것을 세팅해야 한다.
그러면 cache-control을 헤더에 넣어줄 수 있다.
- max-age: 캐시가 유효한 시간(초)
- ex) max-age=60 > 60초 동안 유효
첫 번째 요청시에는 동일하게 동작한다. 그리고 웹 브라우저에는 내부에 캐시를 저장하는 저장소가 있다. 거기에 응답 결과를 저장한다.
두 번째 요청할 때엔 캐시를 먼저 확인 후, 있으면 거기에서 가져온다. (네트워크를 아예 안 탄다)
- 캐시 덕분에 캐시 가능 시간동안 네트워크를 사용하지 않아도 된다.
- 비싼 네트워크 사용량을 줄일 수 있다.
- 브라우저 로딩 속도가 매우 빠르다.
- 메모리 아니면 하드 디스크에서 바로 불러온다.
- 빠른 사용자 경험
세 번째 요청할 때, 만약 캐시 시간이 초과되었으면 다시 요청을 해야 한다.
- 캐시 유효 시간이 초과(stale하다고 한다)되면 서버를 통해 데이터를 다시 조회하고, 캐시를 갱신한다.
- 이때 다시 네트워크 다운로드가 발생한다.
검증 헤더와 조건부 요청
캐시 유효시간 초과
캐시 유효 시간이 초과해서 서버에 다시 요청하면 다음 두 가지 상황이 나타난다.
- 서버에서 기존 데이터를 변경함
- 서버에서 기존 데이터를 변경하지 않음
이때, 기존 데이터가 변경되지 않았을 때 다시 다운로드 받아야 할 이유가 있을까?
캐시를 재사용
캐시 만료 후에도 서버에서 데이터를 변경하지 않았을 경우, 데이터를 다시 전송하는 대신에 저장해 두었던 캐시를 재사용할 수 있다.
단, 클라이언트의 데이터와 서버의 데이터가 같다는 사실을 확인할 수 있는 방법이 필요하다.
검증 헤더 추가
첫 번째 star.jpg를 요청하면, 서버의 응답 헤더에 Last-Modified를 추가할 수 있다.
- Last-Modified: 데이터가 마지막에 수정된 시간(데이터 최종 수정일), UTC 표기법
그러면 클라이언트는 브라우저 캐시(저장소)에 저장하고 유효 기간과 최종 수정일을 기록한다.
유효 기간이 만료되었으면 다시 서버 요청을 보내야 하는데, 최종 수정일(last-modified)가 있다면 if-modified-since라는 HTTP 요청 헤더를 추가한다.
ex) if-modified-since: 2020년 11월 10일 10:00:00
그러면 서버에서 요청을 받은 후 이 헤더를 보고 날짜를 이용해 데이터가 아직 수정되지 않을 지 검증할 수 있다.
수정이 되지 않았다면 HTTP 응답에 304 Not Modified를 보낸다. cache-control과 last-modified도 동일하게 헤더에 추가하지만, HTTP Body가 없다.
- 스타트 라인과 헤더만 추가해서 보낸다.
- 즉, 1.0M 정도는 보내지 않는 것이다. (네트워크 부하가 줄어든다)
- 클라이언트에서는 캐시 유효 시간을 갱신하고, 캐시를 불러와서 사용한다.
정리하자면
- 검증 헤더: Last-Modified
- 조건부 요청: if-modified-since
이렇게 두 개를 조합해서 캐시가 갱신이 되었는지 아니었는지 확인할 수 있다.
- 캐시 유효 시간이 초과해도 서버의 데이터가 갱신되지 않으면 304 Not Modified + 헤더 메타 정보만 응답 (바디X)
- 클라이언트는 서버가 보낸 응답 헤더의 정보로 캐시의 메타 정보를 갱신
- 클라이언트는 캐시에 저장되어 있는 데이터 재활용
- 결과적으로 네트워크 다운로드가 발생하지만 용량이 적은 헤더 정보만 다운로드
- 매우 실용적인 해결책
웹 브라우저들은 대부분 이 매커니즘을 사용하고 있다.
구글 예제
devTools에서 네트워크 탭을 열어 본다.
Status 항목이 연한 것은 캐시에서 불러온 것이다. 더블클릭해서 열어 본 후, 새로고침하여 재요청해보면 상태 코드가 304로 나오는 것을 볼 수 있다.
검증 헤더와 조건부 요청
- 검증 헤더
- 캐시 데이터와 서버 데이터가 같은 지 검증하는 데이터
- Last-Modified, ETag
- 조건부 요청 헤더
- 검증 헤더로 조건에 따른 분기
- If-Modified-Sice: Last-Modified 사용
- If-None-Match: ETag 사용
- 조건이 만족하면 200 OK
- 조건이 만족하지 않으면 304 Not Modified
If-Modified-Since
이 이후에 데이터가 수정되었는지 물어보는 것이다.
- 데이터 미변경 예시
- 캐시: 2020년 11월 10일 10:00:00 vs 서버: 2020년 11월 10일 10:00:00
- 304 Not Modified, 헤더 데이터만 전송
- 전송 용량 0.1M (헤더 0.1M, 바디 1.0M)
- 데이터 변경 예시
- 캐시: 2020년 11월 10일 10:00:00 vs 서버: 2020년 11월 10일 11:00:00
- 200 OK, 모든 데이터 전송
- 전송 용량 1.1M (헤더 0.1M, 바디 1.0M)
단점
- 1초 미만(0.x초) 단위로 캐시 조정이 불가능 (최대 초단위)
- 날짜 기반의 정해진 로직을 사용
- 데이터를 수정해서 날짜가 다르지만, 같은 데이터를 수정해서 데이터 결과가 똑같은 경우 (a -> b -> a)
- 실제 컨텐츠는 갱신되지 않았지만 날짜는 갱신되어서 전체 데이터를 다운로드 받는다.
- 서버에서 별도의 캐시 로직을 관리하고 싶은 경우
- 스페이스나 주석처럼 크게 영향이 없는 변경에서 캐시를 유지하고 싶은 경우
ETag, If-None-Match
ETag(Entity Tag)는 캐시용 데이터에 임의의 고유한 버전 혹은 이름을 달아둔 것이다.
- 버전: v1.0
- 해시: 파일을 해시 알고리즘에 넣어서 해시 값을 얻는다. (파일의 컨텐츠가 같으면 동일한 해시 값이 나온다)
- 임의로 부여
데이터가 변경되면 이 이름을 바꾸어서 변경한다. 단순하게 ETag만 보내서 같으면 유지하고 다르면 다시 받는다.
동작
서버에서 ETag를 내려주면, 브라우저 캐시에 이 값을 저장한다.
캐시가 유효 시간이 초과되면 If-None-Match에 이 값을 담아 보낸다.
서버에서 매치되면 데이터가 아직 수정되지 않았으므로(실패) 304 Not Modified를 보낸다. -> 캐시로 리다이렉트
서버에서 매치되지 않으면 데이터가 수정되었으므로(성공) 200 OK와 전체 데이터를 보낸다.
정리
- 진짜 단순하게 ETag만 서버에 보내서 같으면 유지, 다르면 다시 받기
- 캐시 제어 로직을 서버에서 완전히 관리
- 클라이언트는 단순히 이 값을 서버에 제공: 클라이언트는 캐시 매커니즘을 모른다.
- ex) 애플리케이션 배포 주기에 맞추어 ETag 모두 갱신
캐시와 조건부 요청 헤더
캐시 제어 헤더
Cache-Control
캐시 제어
캐시 지시어(directives)
뒤의 두 가지가 하위 호환을 위해 사용한다면, 지금은 이 캐시 컨트롤로 다 할 수 있다.
- Cache-Control: max-age
- 캐시 유효 시간, 초 단위
- 보통 길게 잡는다.
- Cache-Control: no-cache
- 데이터는 캐시해도 되지만, 항상 원(origin) 서버에 검증하고 사용해야 한다.
- 중간에 캐시 서버;cache proxy 서버라는 게 있고, 이걸 거쳐서 나의 진짜 서버에 접근할 수 있다. 이 중간 서버에서 잠깐 캐시해주거나 그런 기능이 있다. 중간 서버에서는 뭘 하면 안되고, 뒤에 있는 원 서버까지 가야 한다는 의미이다. 뒤의 프록시 서버에서 추가 설명.
- Cache-Control: no-store
- 데이터에 민감한 정보가 있으므로 저장하면 안됨
- 보통 캐시하면 하드 디스크 같은 곳에 저장되는데, 메모리에서 사용하고 최대한 빨리 삭제하라는 듯이다.
Pragma
캐시 제어(하위 호환) - 하위 호환을 위해 사용한다.
- Pragma: no-cache
- HTTP 1.0 하위 호환
no-cache처럼 동작하며, 지금은 거의 사용하지 않지만 하위 호환때문에 필요하면 사용하기도 한다.
Expires
캐시 유효 기간(하위 호환) - 하위 호환을 위해 사용한다.
- expires: Mon, 01 Jan 1990 00:00:00 GMT
- 캐시 만료일을 정확한 날짜로 지정 (초 단위X)
- HTTP 1.0 부터 사용
- 지금은 더 유연한 Cache-Control: max-age 권장
- Cache_Control: max-age와 함께 사용하면 Expires는 무시
검증 헤더와 조건부 요청 헤더
검증 헤더 (Validator)
- ETag: "v1.0", ETag: "asid93jkrh2I"
- Last-Modified: Thu, 04 Jun 2020 07:19:24 GMT
조건부 요청 헤더
- If-Match, If-None-Match: ETag 값 사용
- If-Modified-Since, If-Unmodified-Since: Last-Modified 값 사용
프록시 캐시
원 서버 직접 접근
Origin 서버
만약 한국에 있는 여러 클라이언트가 미국에 있는 서버에 접근할 때, 진짜 애플리케이션에서 이미지를 제공하는 원 소스가 있는 서버를 원 서버(스펙은 Origin 서버)라고 한다.
프록시 캐시 도입
미국에 있는 서버에 요청할 때 500ms(0.5초)가 걸린다고 하면, 이미지 하나 다운로드 받는 데도 시간이 오래 걸린다.
그래서 proxy-cache라는 서버를 도입했다.
보통 CDN 서비스라고 해서, AWS는 클라우드 프론트 같은 걸로 하고 있다.
한국 어딘가에 프록시 캐시 서버를 놓고, DNS 요청을 할 때 약간 조작해서 요청이 오면 미국 원 서버를 직접 접근하는 게 아니라 이 서버를 거쳐서 오도록 한다.
웹 브라우저가 프록시 캐시 서버에 접속하면 응답을 빠르게 받을 수 있다.
현재는 CDN 서비스라고 하며 다 이용한다.
첫 번째 유저는 보통 느리지만, 한 번 다운로드 받아놓으면 두 번째 요청부터는 빠르다. 아니면 아예 캐시에 뒤에서 데이터를 밀어 넣는 경우도 있다.
프록시 캐시 서버에 저장되는 캐시를 public 캐시라고 하고, 웹 브라우저 쪽에 저장되는 캐시를 private 캐시라고 한다.
Cache-Control
캐시 지시어(directives) - 기타
- Cache-Control: public
- Cache-Control: private
- 응답이 해당 사용자만을 위한 것임, private 캐시에 저장해야 함 (기본값)
- Cache-Control: s-maxage
- Age: 60 (HTTP 헤더)
- 오리진 서버에서 응답 후 프록시 캐시 내에 머문 시간(초)
캐시 무효화
Cache-Control
확실한 캐시 무효화를 할 수 있는 응답이 있다.
캐시를 적용하지 않아도 웹 브라우처 측에서 GET 요청인 경우 임의로 캐시를 해버리기도 한다. 휴리스틱하게 한다고 한다.
따라서 이 페이지는 캐시가 되면 안된다고 하면 아래 지시어를 사용해야 한다.
하위 호환을 위해 Pragma까지 넣어줘야 한다.
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
캐시 지시어(diriectives) - 확실한 캐시 무효화
캐시 컨트롤의 오른쪽은 캐시 지시어라고 한다.
- Cache-Control: no-cache
- 데이터는 캐시해도 되지만, 항상 원 서버에 검증하고 상용 (이름에 주의)
- Cache-Control: no-store
- 데이터에 민감한 정보가 있으므로 저장하면 안됨
- 메모리에서 사용하고 최대한 빨리 삭제
- Cache-Control: must-revalidate
- 캐시 만료 후 최초 조회 시 원 서버에 검증해야 함
- 원 서버 접근 실패 시 반드시 오류가 발생해야 함 - 504(Gateway Timeout)
- must-revalidate는 캐시 유효기간이라면 캐시를 사용함
- Pragma: no-cache
네이버나 구글도 이렇게 되어 있다.
no-cache vs must-revalidate
no-cache(no-cache+ETag)가 프록시 캐시 서버를 가면, 프록시 캐시 서버는 원 서버에 요청하여 응답을 반환한다.
그런데, 만약 원 서버와의 네트워크가 순간적으로 단절되어서 접근이 불가능하다면 프록시 캐시 서버 쪽에서 설정에 따라 캐시 데이터를 반환할 수도 있다. (오류 보다는 오래된 데이터라도 보여주자)
여기서 무조건 오류가 발생해야 할 때 must-revalidate를 사용하는 것이다.
예를 들어 통장 잔고와 같이 중요한 데이터는 옛날 데이터를 보여주면 안된다.
Reference
인프런 강의
모든 개발자를 위한 HTTP 웹 기본 지식 강의 - 인프런