캐시와 조건부 요청
참고 및 정리: 위키피디아, 모든 개발자를 위한 HTTP 웹 기본 지식(김영한 님) 인프런 강의
캐시
위키피디아
캐시(cache)는 컴퓨터 과학에서 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. 같은 데이터에 반복적으로 엑세스하는 경우나 잘 변하지 않는 데이터의 경우 캐시를 사용하면 더 빠른 속도로 데이터에 접근할 수 있다. 그래서 데이터의 재사용 횟수가 한 번 이상이어야 캐시는 의미가 있다.
여러가지 캐시의 종류
- CPU 캐시
- 디스크 캐시
- 웹 캐시: 브라우저 캐시, 프록시 캐시
이번 포스팅에서는 웹 캐시를 정리하고자 한다.
캐시의 기본 정의에 따라 웹 캐시는 웹 페이지의 요소를 저장하기 위한 임시 저장소이다. HTTP 요청으로 가져올 수 있는 모든 종류의 리소스(HTML, CSS, JS, 이미지, 비디오 파일)를 로컬 브라우저 내부나 근처에 있는 프록시 서버에 임시 저장하여 사용자가 동일한 서버로 다시 요청할 때, 원 서버와의 연결없이 데이터를 캐시에서 빠르게 가져올 수 있다.
서버는 HTTP 헤더에 캐시 유효 시간을 설정하여 클라이언트에 응답하고, 브라우저는 응답 결과를 캐시에 저장한다. 2번째 요청 시 브라우저 캐시를 먼저 조회하여 캐시 유효 시간 내에 있으면 브라우저 내 캐시에서 데이터를 가져온다.
1) 첫번째 요청
2) 두번째 요청
캐시 장점
- 캐시 유효 시간동안 네트워크를 사용하지 않아도 된다.
- 브라우저 로딩 속도가 빠르다.
🔎 캐시 유효 시간이 만료되면?
서버에 다시 데이터를 요청하고 캐시를 새로 갱신한다. 이때 다시 네트워크 다운로드가 발생한다. 그러나 서버에서 데이터가 바뀌지 않았는데도 다시 같은 데이터를 다운로드 받는다면 비효율적이다. 캐시가 만료되었어도 서버와 캐시 데이터가 여전히 같다면 다운로드 받지 않도록 검증 헤더를 사용하면 된다.
💻 검증 헤더와 조건부 요청1
캐시 유효 시간이 초과해서 서버에 다시 요청하면 두 가지 상황이 나타난다.
1. 서버에서 기존 데이터를 변경하지 않은 경우
2. 서버에서 기존 데이터를 변경
1. 서버에서 기존 데이터를 변경하지 않은 경우
저장해두었던 캐시를 재사용하면 된다. 단 클라이언트의 데이터와 서버의 데이터가 같다는 사실을 확인해야 한다.
1) 서버는 검증 헤더(Last-Modified)를 추가하여 응답을 보내고, 클라이언트는 응답 결과를 캐시에 저장한다.
2) 캐시 유효 시간이 지나고 클라이언트가 서버에 다시 요청을 할 때, 조건부 요청 헤더(if-modified-since)에 데이터 최종 수정일 값을 포함하여 보낸다.
3) 요청받은 데이터 정보를 서버가 확인하여 변경된 게 없으면 304 Not Modified 응답을 내보낸다. HTTP 바디 없이 응답 헤더만 보내면 되므로, 네트워크 부하가 상당히 줄어들게 된다.
4) 클라이언트는 서버가 보낸 응답 헤더 정보로 캐시의 메타 정보를 갱신해서 캐시에 있는 데이터를 재활용한다. 결과적으로 네트워크 다운로드가 발생하지만 용량이 적은 헤더 정보만 다운로드한다.
💻 검증 헤더와 조건부 요청2
- 검증 헤더
- 캐시 데이터와 서버 데이터가 같은지 검증하는 데이터
- Last-Modified, ETag
- 조건부 요청 헤더
- 검증 헤더로 조건에 따른 분기
- If-Modified-Since(요청 헤더): Last-Modified(응답 헤더)와 같이 사용
- If-None-Match(요청 헤더): ETag(응답 헤더)와 같이 사용
- 조건이 만족하지 않으면 304 Not Modified
- 조건이 만족하면 200 OK
- 만약 If-Modified-Since 이후에 데이터가 수정되었다면?
- 데이터가 변경되지 않았다면
- 캐시: 2020년 11월 10일 10:00:00 vs 서버: 2020년 11월 10일 10:00:00
- 304 Not Modified
- HTTP 헤더 데이터만 전송 (Body가 없으므로 용량 적음)
- 데이터가 변경되었다면
- 캐시: 2020년 11월 10일 10:00:00 vs 서버: 2020년 11월 10일 11:00:00
- 200 OK
- 모든 데이터 전송 (Body를 포함하므로 용량 큼)
- 데이터가 변경되지 않았다면
- If-Modified-Since와 Last-Modified의 단점
- 1초 미만 단위로는 캐시 조정이 불가능
- 날짜 기반의 로직을 사용하여 아래와 같은 단점이 있다.
- 데이터를 수정해서 날짜가 다르지만, 데이터 결과는 똑같은 경우 날짜가 달라 데이터도 변경되었다고 인식
- 서버에서 별도의 캐시 로직을 관리하고 싶은 경우 (스페이스나 주석처럼 크게 영향이 없는 변경에서 캐시를 유지하고 싶은 경우)
- 위 단점을 보완하고 별도로 캐시 로직을 관리하기 위해 ETag를 사용한다.
- ETag와 If-None-Match
- ETag: Entity Tag
- 캐시용 데이터에 임의의 고유한 버전 이름을 달아두는 것
- ex) ETag: "v1.0", ETag: "a2jiodwjekjl3
- 데이터가 변경되면 이 이름을 바꾸어서 변경(hash를 다시 생성) -> 파일 내용이 같으면 hash 값은 동일하다. 그래서 아무리 변경을 많이 하더라도 데이터 내용이 원본과 같으면 동일한 hash를 갖는다.
- 진짜 단순하게 ETag만 보내서 같으면 유지하고, 다르면 다시 받는다.
- 결국은 캐시 제어 로직을 서버에서 완전히 관리하는 방식이다. 서버에서 이름을 부여하기 때문에.
- 클라이언트는 단순히 이 값을 서버에 제공(클라이언트는 캐시 메커니즘을 모름)
- ex) 서버는 배타 오픈 기간인 3일 동안 파일이 변경되어도 ETag를 동일하게 유지
- ex) 애플리케이션 배포 주기에 맞추어 ETag 모두 갱신
1) 클라이언트에서 2번째 요청을 ETag로 'Match가 안되면' 으로 보내면,
2) Match가 되었을 때(데이터가 같을 때), 실패인 304 Not Modified를 보내게 된다. 즉 수정된 게 없다는 뜻이다.
💻 캐시와 조건부 요청 헤더
캐시 제어 헤더
- Cache-Control: 캐시 제어
- Pragma: 캐시 제어(하위 호환)
- Expires: 캐시 유효 기간(하위 호환)
Cache-Control (캐시 지시어, directives)
- Cache-Control: max-age 캐시 유효 시간, 초 단위
- Cache-Control: no-cache 데이터는 캐시해도 되지만, 항상 원 서버(origin서버)에 검증하고 사용
- Cache-Control: no-store 데이터에 민감한 정보가 있으므로 저장하면 안됨 (메모리에서 사용하고 바로 삭제)
Pragma: 캐시 제어(하위 호환)
- Pragma: no-cache - HTTP 1.0 하위호환이므로 거의 사용X
Expires: 캐시 만료일 지정(하위 호환)
- 캐시 만료일을 정확한 날짜로 지정
- HTTP 1.0부터 사용했지만, 지금은 더 유연한 Cache-Control: max-age를 더 권장한다.
- Cache-Control: max-age와 함께 사용하면 Expires는 무시된다.
💻 프록시 캐시
- 웹 브라우저 1, 2, 3은 미국에 있는 ORIGIN 서버와 통신해야 하는데, 일일히 통신하기에 너무 느리다는 단점이 있다.
- 그래서 한국 어딘가에 프록시 캐시 서버를 만들어 거기에 데이터를 저장해놓으면, 더 응답이 빠를 것이다.
- 프록시 캐시 서버는 public 캐시이고 개인 웹 브라우저는 private 캐시이다. (위의 cache control의 기타 부분 참고)
보통 처음으로 서버에 데이터를 요청한 사용자는 원 서버에서 데이터를 가져오게 되고, 이 데이터를 캐시 서버에 저장한다. 그 다음 사용자부터는 프록시 캐시 서버에서 데이터를 다운로드 받게 된다. 혹은 처음부터 원 서버에서 미리 캐시 서버로 데이터를 넣어 놓는다.
Cache-Control (캐시 지시어, directives) 기타
- Cache-Control: public 응답이 public 캐시에 저장되어도 됨
- Cache-Control: private(디폴트값) 응답이 해당 사용자만을 위한 것임, private 캐시에 저장해야 함
- Cache-Control: s-maxage 프록시 캐시에만 적용되는 max-age
- Age: 60 오리진 서버에서 응답 후 프록시 캐시 내에 머문 시간(초)
💻 캐시 무효화
확실한 캐시 무효화 응답
- 캐시를 만들지 않아도, 요즘은 브라우저는 자동으로 캐시를 만드는 경우가 많다. 따라서 절대로 캐시가 되면 안되는 페이지는 아래와 같은 캐시 제어 헤더를 전부 넣어줘야 한다.
- ex) 현재 사용자의 통장 잔고
- Cache-Control: no-cache, no-store, must-revalidate
- Pragma: no-cache : HTTP 1.0 하위 호환(과거 구식의 브라우저에서도 요청이 올 수도 있으므로 넣어주는 게 좋다)
Cache-Control: no-cache
- 캐시가 만료되지 않았어도 항상 원 서버에 검증하고 사용
Cache-Control: no-store
- 데이터에 민감한 정보가 있으므로 저장하면 안됨 (메모리에서 사용하고 최대한 빨리 삭제)
Cache-Control: must-revalidate
- 만료된 캐시는 원 서버에 검증해야 함(캐시 유효 시간이면 캐시를 사용)
- 원 서버 접근 실패시 반드시 오류가 발생해야 함, 504(Gateway Timeout)
🔎 no-store만으로 캐시가 무효화가 가능하지 않을까?
사실 의미를 보면 no-store만으로 캐시가 무효화되어야 하는 것이 맞다. 그러나 오래된 브라우저와의 호환, 그리고 버그
수 많은 프록시 캐시 업체들과 그 구현 서버 등의 문제로 no-store만으로 캐시가 무효화되지 못하고, 나머지 옵션들도 함께 사용하게 됩니다. 그래서 구글이나 네이버 등 주요 메이저 사이트의 응답을 보면 cache-control: no-cache, no-store, must-revalidate를 함께 가져간다.
no-cache와 must-revaildate 차이
웹 브라우저가 프록시 캐시 서버에 요청을 하면, 프록시 캐시는 no-cache를 보고 자기가 처리하면 안된다는 것을 알고 원 서버로 요청을 넘긴다. 원 서버는 검증을 하고 데이터가 같다면 정상적으로 응답을 해준다.
그런데 no-cache로 온 요청을 프록시 캐시가 원 서버에 넘길 때, 만약 원 서버와의 네트워크가 단절되면 옛날 데이터라도 보내주는 설정을 프록시 캐시 서버에 할 수 있다.
이번엔 must-revalidate로 온 요청을 프록시 캐시가 원 서버에 넘길 때, 만약 원 서버와의 네트워크가 단절되면 옛날 데이터를 보내는 것이 아닌 항상 오류를 발생시킨다. 예를 들어 계좌이체를 해서 화면을 새로 갱신을 했는데, 원 서버와 단절이 되었을 때 내 통장 잔고가 예전 게 보이면 안되는 것이다.