Home HTTP 웹 지식 - HTTP 헤더 (캐시 & 조건부 요청)
Post
Cancel

HTTP 웹 지식 - HTTP 헤더 (캐시 & 조건부 요청)

캐시의 기본 동작 방식

캐시가 없을 때

클라이언트가 이미지 파일을 요청했다고 가정한다. 서버는 1.1M 용량의 HTTP 메시지를 응답한다. 이 응답 데이터를 바탕으로 웹 브라우저에 이미지가 띄워지게 된다.

두 번째 요청에서 1.1M 용량을 다시 응답하게 된다.

데이터가 변경되지 않아도 계속 네트워크를 통해 데이터를 다운로드 받아야 되는 것이다. 이러면 비용, 속도면에서 비효율적이다. 캐시를 적용해보자.

캐시 적용

cache-control 헤더를 통해 캐시의 유효 기간을 설정한다.

응답 결과를 캐시에 저장소에 저장되고 그 캐시는 60초가 유효하게 되는 것이다.

두 번째 요청에는 캐시의 유효 시간을 검증하고 유효하다면 캐시에서 바로 데이터를 가져올 수 있게 되는 것이다. 서버와 네트워크를 통해 통신할 필요가 없는 것이다.

네트워크를 사용하지 않아도 되어 빠르고 비용면에서 효율적이다.

세 번째 요청. 캐시의 유효 시간이 초과되었을 경우?

다시 네트워크를 통해 데이터를 전달받아야 하며 이 때 캐시를 갱신하게 된다.

그런데 내가 원하는 이미지는 그대로인데 캐시가 만료됐다고 다시 받아야 하는거 너무 비효율적이지 않나?

이 문제를 아래에서 다룬다.

검증 헤더와 조건부 요청

캐시 유효 시간이 초과해 서버에 다시 요청하면 아래의 두 가지 상황이 나타난다.

  1. 서버에서 기존 데이터를 변경함.
  2. 서버에서 기존 데이터를 변경하지 않음.

데이터가 변경되지 않았다면 저장해 두었던 캐시를 재사용할 수 있다.

단, 클라이언트의 데이터와 서버의 데이터가 같다는 사실을 확인할 수 있는 방법이 필요하다.

검증 헤더와 조건부 요청 1.

Last-Modified 헤더를 통해 이 데이터가 마지막으로 수정된 시간을 저장한다.

캐시에 Last-Modified가 있다면 클라이언트가 요청을 보낼 때 if-modified-since 헤더를 포함시켜 서버에서 이를 통해 데이터의 수정 여부를 파악하도록 할 수 있다.

수정되지 않았다면 304 Not Modified 상태코드를 전송하게 되는 것이다.

메시지를 보면 HTTP Body가 없다. 헤더만 보내면 되기 때문에 네트워크 사용량이 매우 줄어들게 된다.

이렇게 응답 HTTP 메시지를 받으면 데이터를 갱신하고 재사용하게 되는 것이다.

  • 검증 헤더 : Last-Modified: 2020년 11월 10일 10:00:00 (응답)
  • 조건부 요청 : if-modified-since: 2020년 11월 10일 10:00:00 (요청)

정리

  • 캐시 유효 시간이 초과해도, 서버의 데이터가 갱신되지 않으면 바디를 제외하고 304 Not Modified + 헤더 메타 정보만 응답한다.
  • 클라이언트는 서버가 보낸 응답 헤더 정보로 캐시의 메타 정보를 갱신한다.
  • 클라이언트는 캐시에 저장되어 있는 데이터를 재활용한다.
  • 결과적으로 네트워크 다운로드가 발생하지만 용량이 적은 헤더정보만 다운로드 하므로 효율적이라고 볼 수 있다.

** 참고: 개발자도구에서 이를 확인할 때 배경이 연한 파일이 있고 그렇지 않은 파일이 있는데 연한 파일은 캐시에서 불러왔다는 뜻이다.

검증 헤더와 조건부 요청 2.

  • 검증 헤더
    • 캐시 데이터와 서버 데이터가 같은지 검증하는 데이터이다.
    • Last-Modified, ETag 가 있다.
  • 조건부 요청 헤더
    • 검증 헤더로 조건에 따른 분기를 서버에게 요청한다.
    • If-Modified-Since -> Last-Modified 사용.
    • If-None-Match -> ETag 사용.
    • 조건이 만족하면 200 OK
    • 조건이 만족하지 않으면 304 Not Modified (변경이 일어났느냐가 조건이기 때문에 만족하지 않은 경우가 304)

If-Modified-Since, Last-Modified

데이터가 수정되지 않은 경우는 위의 경우이고, 304 Not Modified를 응답받고 헤더 데이터만 전송받게 된다.

만약 데이터가 변경된 경우라면?

200 OK를 받고 바디를 포함한 모든 데이터를 전송받게 된다.

  • 단점
    • 1초 미만(0.x초) 단위로 캐시 조정이 불가능하다.
    • 날짜 기반의 로직을 사용한다.
    • 데이터를 수정해 날짜가 다르지만 데이터의 결과가 같은 경우가 있을 수 있다. 이 때는 날짜가 바뀌었기 때문에 데이터가 같아도 다시 다운 받아야 한다.
    • 서버에서 별도의 캐시 로직을 관리하고 싶은 경우에도 이 방식으로는 할 수 없다.
    • 만약 스페이스나 주석처럼 크게 영향이 없는 변경에서는 캐시를 유지하고 싶어도 날짜가 바뀌기 때문에 이 방법으로는 구현할 수 없다.

이럴 때 ETag를 사용한다.

ETag, If-None-Match

  • ETag(Entity Tag)
  • 캐시용 데이터에 임의의 고유한 버전 이름을 달아둔다.
    • ex) ETag: “v1.0”
  • 데이터가 변경되면 이 이름을 바꾸어서 변경한다. (해시를 다시 생성)
    • ex) ETag: “aaaaa” -> ETag; “bbbbb”
  • 정말 단순하게 생각한다면 ETag만 보내서 같으면 유지, 다르면 다시 다운.

ETag를 응답하고 캐시에 만료정보와 함께 ETag를 포함시킨다.

ETag를 비교해 같다면 데이터가 변경되지 않은 것이므로 304 Not Modified를 보내고 HTTP Body 없이 데이터를 전송하게 된다. 이후는 위와 같이 캐시의 갱신, 캐시 데이터의 재사용이 이루어 진다.

ETag가 변경되었다면 200 OK 응답을 보내고 HTTP 바디를 포함해 데이터를 다시 다운받도록 하게 된다.

  • 캐시 제어 로직을 서버에서 완전히 관리한다.
  • 클라이언트는 단순히 이 값을 서버에 제공하고 캐시 메커니즘은 모른다.
    • ex) 애플리케이션 배포 주기에 맞추어 ETag를 모두 갱신하는 식의 방법으로 캐시 제어 로직을 정할 수 있다.

캐시와 조건부 요청에 관한 헤더들

캐시 제어 헤더

  • Cache-Control: 캐시 제어
    • Cache-Control: max-age -> 캐시의 유효기간, 초단위.
    • Cache-Control: no-cache -> 데이터는 캐시해도 되지만, 항상 origin 서버에 검증하고 사용해라. (중간에 캐시 서버가 따로 있는 경우가 있는데 여기서 마치지 말고 origin 서버에 검증해라.)
    • Cache-Control: no-store -> 데이터에 민감한 정보가 있으니 저장하지 마라.
  • Pragma: 캐시 제어(하위 호환)
  • Expires: 캐시 유효 기간(하위 호환)
    • 더 유연한 max-age 사용 권장. 함께 쓰면 expires는 무시.

검증 헤더 및 조건부 요청 헤더

  • 검증헤더
    • ETag, Last-Modified
  • 조건부 요청 헤더
    • If-Match, If-None-Match : ETag 사용
    • If-Modified-Since, If-UnModified-Since : Last-Modified 사용

프록시 캐시

위와 같이 원서버에 직접 접근한다하면 거리가 멀기 때문에 어쩔 수 없이 물리적으로 어느 정도의 시간이 걸리게 된다.

이러한 느린 사용자 경험을 개선하기 위해 중간에 프록시 서버를 도입해 이러한 시간을 줄이게 되는 것이다.

원 서버가 프록시 캐시 서버에 해당 데이터를 미리 밀어두지 않는 한 보통은 첫 번째로 요청한 유저는 느린 사용자 경험을 하게 된다.

public 캐시는 사용자 정보 등이 아닌 이미지 같은 공용 파일이 해당된다.

기타 캐시 지시어

  • Cache-Control: public : 응답이 public 캐시에 저장되어도 된다.
  • Cache-Control: private : 응답이 해당 사용자만을 위한 것이다. private 캐시에 저장해야 한다. (기본 값)
  • Cache-Control: s-maxage : 프록시 캐시에만 적용되는 max-age
  • Age: 60 (HTTP 헤더) : Origin 서버에서 응답 후 프록시 캐시 내에 머문 시간.

캐시 무효화

캐시를 무효화하고 싶다면 아래의 것을 모두 헤더에 포함시켜야 한다.

Cache-Control : no-cache, no-store, must-revalidate

Pragma: no-cache (하위 호환)

예를 들어 현재 사용자의 통장 잔고 이런 것은 계속해서 갱신이 되기 때문에 이런 화면은 절대 캐시하면 안된다. 이럴 때 사용한다.

  • Cache-Control: no-cache
    • 데이터는 캐시해도 되지만, 항상 원 서버에 검증하고 사용
  • Cache-Control: no-store
    • 데이터에 민감한 정보가 있으므로 저장하면 안된다.
  • Cache-Control: must-revalidate
    • 캐시 만료 후 최초 조회시 원 서버에 검증해야 한다.
    • 원 서버 접근 실패시 반드시 오류가 발생해야 한다. (504 Gateway Timeout)
    • must-revalidate는 캐시 유효 시간이라면 캐시를 사용한다.
  • Pragma: no-cache
    • HTTP 1.0 하위 호환

no-chache vs must-revalidate

캐시 서버에 요청하게 되면 no-cache가 있어 프록시 캐시 서버는 원서버에 요청하게 된다.

원 서버에서 보낸 응답은 다시 프록시 서버를 거쳐 클라이언트에게 도달하게 된다.

그런데 만약 프록시 서버와 원 서버 사이에서 순간 네트워크 장애가 일어난다면?

오류보다는 오래된 데이터라도 보여주자. 라는 것이 no-cache의 정책이다.

이제 must-revalidate를 살펴보자.

같은 경우 must-revalidate는 200 OK가 아닌 504 Gateway-Timeout을 응답하게 된다.

This post is licensed under CC BY 4.0 by the author.