구리

[네트워크] HTTP 헤더 - 캐시와 조건부 요청 본문

네트워크

[네트워크] HTTP 헤더 - 캐시와 조건부 요청

guriguriguri 2023. 6. 18. 21:20

캐시란?

자주 사용하는 데이터나 값을 미리 복사해 놓은 임시 장소를 가리킵니다. 캐싱을 통해 처리 속도를 향상시키고 향후 요청을 더 빠르게 처리할 수 있습니다. 웹 캐시는 사용자가 웹사이트 접속시, 정적 컨텐츠(JS, CSS, 이미지 등)를 특정 위치에 저장해 웹사이트 서버에 해당 컨텐츠를 매번 요청하지 않고 특정 위치에 불러옴으로써 사이트 응답시간을 줄이고, 서버 트래픽 감소 효과를 볼 수 있습니다.

 

캐시 기본 동작

캐시가 없을 경우

  1. 클라이언트는 서버에서 리소스(star.jpg)를 최초로 요청
  2. 서버는 HTTP header가 0.1M, HTTP body가 1M인 총 1.1M 크기의 사진 데이터를 전송
  3. 클라이언트는 응답 결과를 화면에 보여줌
  4. 클라이언트는 이후 동일 이미지에 대해 매번 서버에게 요청해 응답 결과를 받아 화면에 보여줌

 

캐시를 적용한 경우

  1. 클라이언트는 서버에서 리소스(star.jpg)를 최초로 요청
  2. 서버는 HTTP 응답 header의 cache-control 옵션을 통해 캐시 유효 시간을 설정해 클라이언트에게 응답 결과를 전달
  3. 클라이언트는 브라우저 내부 캐시 저장소에 60초동안 유효한 데이터를 저장
  4. 클라이언트가 star.jpg 데이터를 요청시, 서버에 요청하기 전 캐시 저장소에 유효한 데이터가 있는지 조회
    1. 유효한 데이터가 존재하다면 캐시에서 데이터를 가져와 사용
    2. 유효 시간이 만료한 경우 서버에 요청해 2~3의 과정이 동일하게 진행

 

위 과정을 정리하면 다음과 같습니다.

  • 캐시가 없을 경우
    • 데이티가 변경되지 않아도 계속 네트워크를 통해 데이터를 다운로드 받아야 함
    • 인터넷 네트워크는 하드디스크에 비해 비교적 느리고 비쌈
    • 브라우저 로딩 속도가 느려짐 → 느린 사용자 경험
  • 캐시 적용
    • 캐시 덕분에 캐시 유효 시간동안 네트워크를 사용하지 않아도 됨
    • 비싼 네트워크 사용량을 줄일 수 있음
    • 브라우저 로딩 속도 빠름 → 빠른 사용자 경험
  • 캐시 유효 시간 초과
    • 캐시 유효 시간이 초과되면, 서버를 통해 데이터 재조회 및 캐시 갱신 → 이 과정에서 네트워크 다운로드 다시 발생

 

만약 위 예시에서 요청 데이터(star.jpg)가 변하지 않았는데 캐시 유효 시간이 만료될 때마다 네트워크 다운로드가 발생하는 건 비효율적이지 않을까요?

이럴 때 사용되는 것이 검증 헤더(Last-Modified)와 조건부 요청(if-modified-since)입니다.

 

Last-Modified, If-Modified-Since 검증 헤더, 조건부 요청 헤더 사용시

  1. 클라이언트가 데이터(star.jpg) 요청 후 서버는 응답 결과 전달시 데이터 최종 수정일을 나타내는 HTTP 응답 header의 Last-Modified(UTC 기준)를 추가해 응답
  2. 클라이언트는 응답 결과를 캐시에 저장 (데이터 최종 수정일 포함)
  3. 캐시 유효 시간 초과시 클라이언트는 서버에 데이터를 재요청하는데 이때, 캐시가 가진 데이터 최종 수정일을 나타내는 HTTP 요청 header인 If-Modified-Since 옵션을 포함
  4. 서버는 전달 받은 데이터 최종 수정일을 통해 데이터가 변했는지에 대해 검증
    1. 데이터가 신선한 상태(stale)라면?
      1. 데이터가 신선한(stale) 상태라면 304 Not-Modified code로 HTTP body(1M)없이 header(0.1M 크기)만 응답
      2. 클라이언트는 데이터 변화가 없다는 결과를 받았기에, 응답 결과의 헤더 데이터(cache-control, last-modified 등) 갱신 후 캐시 내의 데이터를 사용해 사용자에게 제공
    2. 데이터가 신선하지 않은 상태라면?
      1. HTTP status code 200 OK로 모든 데이터 (header + body → 1.1M)를 클라이언트에게 전송 

 

검증 헤더 Last-Modified, 조건부 요청 헤더 If-Modified-Since 정리

  • 캐시 유효 시간이 초과해도, 서버 데이터가 갱신되지 않았다면 → 304 Not Modified + header 메타 정보만 응답 (body X)
  • 클라이언트는 서버가 보낸 응답 헤더 정보로 캐시 메타 정보를 갱신
  • 장점
    • 클라이언트는 캐시에 저장된 데이터 재활용 → 네트워크 다운로드가 발생하지만 용량이 적은 헤더 정보만 다운로드하기에 네트워크 부하 감소
  • 단점
    • 1초 미만 단위(0.x초) 캐시 조정 불가능
    • 날짜 기반의 로직을 사용하기에 아래의 경우에는 유효시간으로 유효성을 체크하는 것이 충분하지 않음
      • 같은 데이터를 수정해 결과적으로 데이터 결과가 같은 경우에도 데이터를 새로 받아오는 문제 발생 (A → B → A로 변경시 데이터 자체는 변경이 없지만 데이터 수정 날짜만 변경됨)
      • 서버에서 별도의 캐시 로직을 관리할 수 없음 (ex : 스페이스나 주석처럼 크케 영향이 없는 변경일 경우 캐시를 유지하고 싶은 경우)

(메타데이터 - 데이터를 설명해주는 데이터로 데이터를 표현하며 빨리 찾기 위한 목적을 가짐)

Last-Modified, If-Modified-Since로 캐시를 검증할 수 있지만 데이터 최종 수정 날짜로 검증하기에 위와 같은 단점이 발생합니다. 이런 단점을 보완하기 위해선 ETag, If-None-Match 옵션을 사용할 수 있습니다.

 

ETag, If-None-Match 검증 헤더, 조건부 요청 헤더 사용시

 

  1. 클라이언트가 데이터(star.jpg) 요청 후 서버는 응답 결과 전달시 데이터 최종 수정일을 나타내는 HTTP 응답 header의 ETag를 추가해 응답
  2. 클라이언트는 응답 결과를 캐시에 저장 (ETag 포함)
  3. 캐시 유효 시간 초과시 클라이언트는 서버에 데이터를 재요청하는데 이때, 캐시가 가진 ETag 데이터를 HTTP 요청 header인 If-None-Match 옵션에 담아서 요청
  4. 서버는 전달 받은 데이터 Etag 값을 통해 데이터가 변경되었는지 검증
    1. 데이터가 신선한 상태(stale)라면?
      1. 서버 데이터의 ETag 값과 전달 받은 ETag 값이 일치(stale 상태)하다면 304 Not-Modified code로 HTTP body(1M)없이 HTTP header(0.1M 크기)만 응답
      2. 클라이언트는 데이터 변화가 없다는 결과를 받았기에, 응답 결과의 헤더 데이터(cache-control 등) 갱신 후 캐시 내의 데이터를 사용해 사용자에게 제공
    2. 데이터가 신선하지 않은 상태라면?
      1. HTTP status code 200 OK로 모든 데이터 (header + body → 1.1M)를 클라이언트에게 전송 

 

검증 헤더 ETag, 조건부 요청 헤더 If-None-Match 정리

  • ETag (Entity Tag)
    • 캐시용 데이터에 날짜가 아닌 임의의 고유한 버전을 명시 (ex - ETag: "v1.0", ETag: "a2jiowjekl3")
  • 데이터 변경시 hash를 다시 생성해 변경 (ETag: "aaa" → ETag: "bbb")
  • 클라이언트는 단순히 ETag 값만 서버에 전송 → ETag 값이 같으면 캐시 유지, 다르면 새로운 데이터를 전달 받음
  • 장점
    • 클라이언트는 캐시 메커니즘을 알 필요가 없으며 캐시 제어 로직을 서버에서 완전히 관리할 수 있음
    • ex - 애플리케이션 배포 주기에 맞춰 ETag를 모두 갱신

 

프록시 캐시

  • 프록시 캐시를 도입하지 않은 경우
    • 한국에 있는 클라이언트들은 미국에 있는 원 서버에 요청을 전송하고 응답 받는데 0.5초의 시간이 걸린다고 가정
  • 프록시 캐시를 도입한 경우
    • 최초 요청시
      • 한국 클라이언트 A는 한국에 있는 프록시 캐시 서버에 접근 후 데이터가 없다면 미국의 원서버로 요청을 전달
      • 프록시 캐시 서버에 데이터를 저장 후 클라이언트 A에게 데이터를 응답 (총 0.5초)
    • 이후 요청시
      • 한국 클라이언트 B는 같은 데이터 요청시 한국에 있는 프록시 캐시 서버에 먼저 접근
      • 프록시 캐시 서버에 있는 데이터를 응답 (0.1초)

결과적으로, 프록시 캐시를 사용하면 더 빠른 응답이 가능합니다. 예를 들면 유튜브의 경우, 인기 동영상은 다운로드가 빠르지만 사람들이 잘 보지 않는 기술 관련 영상의 경우 프록시 서버에 저장된 데이터가 없으므로 네트워크 다운로드가 느릴 수 있습니다.

 

이때 중간에서 공용으로 사용되는(프록시 서버에 저장) 캐시를 public 캐시, 클라이언트(로컬, 웹브라우저)에 저장되는 캐시를 private 캐시라고 합니다.

캐시 데이터는 언제 삭제되나요?
더보기

디바이스 용량이 꽉 차면 브라우저가 자동으로 데이터를 지우는 최적화 스토리지 방식을 사용합니다.

크롬을 사용하는 브라우저의 경우, 가장 오래된 데이터부터 용량 제한을 해결할 때까지 데이터를 삭제합니다.

 

프록시란?
더보기

"대리"의 의미로, 인터넷에서는 더 빠른 접근, 안전한 통신 등을 확보하기 위한 중계서버로 캐시 프록시 서버를 사용하면 원서버에 접근하지 않고도 데이터를 제공할 수 있기에 더 빠른 응답이 가능합니다.

 

캐시 무효화

캐시 무효화는 말그대로 웹브라우저의 캐시를 완전히 제거하는 것을 의미합니다.

캐시 적용을 하지 않아도,  브라우저가 GET 요청을 받을 경우 임의로 캐시를 하는 경우도 있습니다. 또한 리소스의 캐시 유효 기간을 길게 설정해 리소스의 업데이트가 필요할 경우 캐시 저장소의 복사본을 갱신해야 하는데, 기본적으로 브라우저는 캐시 유효 시간이 끝나야 캐시 유효성 검증을 서버에게 요청합니다. 이러한 문제를 해결하기 위해 캐시 무효화 전략을 사용하게 됩니다.

cache-control 흐름도

캐시 무효화 헤더

만약 캐시를 사용하면 안되는 페이지(리소스)가 존재한다면, 다음과 같이 Cache-Control 헤더에 파라미터를 설정해야 합니다.

Cache-Control: no-cache, no-store, must-revalidate, Pragma: no-cache
  • Cache-Control: no-cache
    • 데이터는 캐시해도 되지만 항상 원서버에 검증 후 사용해야 함 (max-age=0과 동일한 의미)
    • 즉, 서버로부터 304 응답을 받아야 캐시에서 가져온다는 의미 → 비록 네트워크 트래픽이 발생하지만 헤더 메세지만 응답 받기에 네트워크 다운로드량은 적음
    • no cache라는 이름 때문에 혼란이 올 수 있지만, 원래 의미는 캐시 유효 기간이 남아있으면 무조건 캐시 저장소를 조회하는 것이 아닌 무조건 원서버에 검증 후 사용하라는 의미
  • Cache-Control: no-store
    • 데이터에 민감한 정보가 있기에 저장하면 안된다는 의미 
    • 메모리에서 사용하고 최대한 빨리 삭제 (ex : 사용자 계좌 정보)
  • Cache-Control : must-revalidate
    • 캐시 만료 후 최초 조회시 원서버에 검증해야 함
    • 원서버 접근 실패시 반드시 504(Gateway Timeout) 에러가 발생해야 함
    • 캐시 유효 시간 내라면 반드시 캐시를 사용함

 

일부 웹페이지에서 캐시 무효화 우회 로직으로 Cache-Control 헤더에 max-age=0으로 설정합니다. 캐시 유효 시간을 0으로 설정하면, 매번 리소스 요청시 서버에 재검증 요청을 보냅니다.
하지만 일부 모바일 브라우저의 경우, 네트워크 요청을 아끼고 사용자에게 빠른 웹 경험을 제공하기 위해 웹 브라우저를 껐다 켜기 전까지 리소스가 만료되지 않도록 하는 경우가 존재합니다.
따라서 max-age=0보다는 더 명확한 no-store 파라미터를 사용하는 것이 좋습니다.

 

no-cache와 must-revalidate의 비교

캐시 무효화 헤더 설정시 보통 no-cache와 must-revalidate를 같이 설정합니다.

must-revalidate 사용시, 캐시 만료가 되면 원서버에 검증 요청을 하는데 프록시 캐시 서버와 원서버와의 연결이 끊겨 검증이 불가능할 경우, 504 Gateway Timeout 오류를 발생시킵니다.

일부 프록시 캐시 서버에서 원서버에 접근이 불가능하면 검증을 거치지 않고 이전 캐시 데이터 (프록시 서버 캐시 데이터)를 반환하기에 계좌 정보 같은 민감한 데이터의 경우, 원서버와의 연결이 불가능할 경우 이전 캐시 데이터를 반환하면 문제가 되므로 must-revalidate를 이용해 5XX code의 에러를 발생시킬 수 있습니다.

 

no-cache 동작원리

  1. 클라이언트는 no-cache + ETag를 설정해 서버에 데이터를 요청
  2. 중간에 프록시 캐시 서버가 요청을 받고, no-cache 설정이 되어 있기에 원서버에 요청을 전달
  3. 원서버에서 ETag를 통해 캐시 검증을 거침
  4. 캐시가 유효하다면 프록시 캐시 서버에게 304 응답을 함
  5. 프록시 캐시 서버는 다시 클라이언트에게 그대로 304 응답을 전달
  6. 클라이언트는 캐시를 재사용

원서버와의 연결이 끊긴 경우, no-cache 동작원리

  1. 클라이언트는 no-cache + ETag를 설정해 서버에 데이터를 요청
  2.  중간에 프록시 캐시 서버는 원서버에게 다시 요청, 이때 원서버와의 접근이 불가한 경우가 발생
  3. 그러면 no-cache이므로 캐시 서버 설정에 따라 캐시 데이터를 담아 200 OK 반환 (보통 오류보다는 오래된 데이터라도 보여주자는 의미로 프록시 캐시 서버에 있는 캐시 데이터와 함께 200 OK 응답)
  4. 원서버의 처리 상태도 모른채 클라이언트는 신선하지 않은 데이터를 클라이언트에게 제공할 수도 있으므로, 통장 잔고 같은 민감한 정보의 경우 서비스에 차질이 생길 수 있음

 

must-revalidate 동작원리

  1. 클라이언트는 no-cache + ETag를 설정해 서버에 데이터를 요청
  2.  중간에 프록시 캐시 서버는 원서버에게 다시 요청, 이때 원서버와의 접근이 불가한 경우가 발생
  3. 그러면 must-revalidate의 경우, 무조건 504 Gateway Timeout 에러를 응답
  4. 클라이언트는 원서버에 문제가 있음을 알게됨 → 별도의 재수정 로직을 거침 (개발자의 판단)
다만, must-revalidate는 캐시 유효성 기간이 남았다면 우선적으로 캐시 저장소를 조회해 캐시 데이터를 사용합니다.
따라서 항상 신선한 상태의 데이터를 받고 원서버에 캐시 검증을 하고 싶다면 no-cache와 같이 사용하면 됩니다.

 

캐시 제어 헤더 정리

(1) Cache-Control

  • Cache-Control : max-age
    • 캐시 유효 시간을 나타내며 초단위로 설정
  • Cache-Control : no-cache
    • 데이터는 캐시해도 되지만, 항상 원(origin)서버에 검증하고 사용
    • If-Modified-Since나 If-None-Match를 통해 캐시를 검증하며 중간에 캐시서버가 아닌 뒤에 있는 원서버에서 검증
    • 대부분의 브라우저에서 max-age=0과 동일한 뜻을 가짐
  • Cache-Contro: must-revalidate
    • 캐시 만료 후 최초 조회시 원(origin)서버에 검증하고 사용
    • 원서버 접근 실패시 반드시 오류 발생해야 함 (504 Gateway Timeout)
    • 캐시 유효시간 내인경우, 반드시 캐시 사용
  • Cache-Control : no-store
    • 데이터에 민감한 정보가 있는 경우 캐시에 저장을 하지 않음 (메모리에서 사용하고 최대한 빨리 삭제)
  • Cache-Control : public
    • 응답이 public 캐시에 저장되어도 됨
  • Cache-Control : private
    • 응답이 해당 사용자만을 위한 것으로 private 캐시에 저장해야 하는 것이 기본값 (ex : 로그인한 사용자 관련 정보)
  • Cache-Control : s-maxage
    • 프록시 캐시(중간 서버)에만 적용되는 max-age
    • 만약 s-maxage=3153600, max-age=0과 같이 설정하면 CDN에서는 1년동안 캐시되지만 브라우저에서는 매번 재검증 요청을 보내도록 설정됨
  • Age : 60 (HTTP 헤더)
    • 오리진 서버에서 응답 후 프록시 캐시 내에 머문 시간(초)

(2) Pragma (하위 호환)

  • Pragma : no-cache
  • HTTP/1.1 버전의 Cache-Control 헤더가 생기기 전과 동일한 역할로 하위 호환을 위해 사용

(3) Expires (하위 호환)

  • 캐시 유효 시간을 설정하며 UTC 기준으로 만료 시간을 명시
  • HTTP/1.0부터 사용되며 하위 호환을 위해 사용됨
  • 더 유연한 max-age 사용 권고 
  • max-age와 사용될 경우 해당 header 무시됨

 

데이터를 요청시, 항상 캐시 무효화를 하고 싶다면 no-store만 사용해도 되지 않나요?
더보기

HTTP  스펙도 디테일하게 보면 모호한 부분이 발생합니다. 예를 들어 웹브라우저 앞으로 가기, 뒤로 가기를 실행할 경우 이걸 캐시로 볼 것인가? 이때는 no-store만 보고 판단할 것인가? 아니면 no-cache만 보고 판단할 것인가?의 경우가 있고 HTTP/1.1을 지원하지만 조금 오래된 브라우저의 호환 및 버그 등의 문제들로 인해 모든 케이스를 no-store만으로 해결하기는 어렵습니다.

따라서 나머지 옵션들(no-cache, must-revalidate)도 같이 사용하는 것이 좋습니다.

no-cache, must-revalidate 지시자를 같이 사용할 때, 원서버와의 연결이 끊기면 어떤 응답 코드를 받나요?
더보기

504 Gateway Timeout 응답 코드를 받게 되며, 모든 서버가 HTTP 스펙을 완벽히 구사한 것이 아니기에 2개의 지시자를 같이 전달하는 것이 좋습니다.

 

캐시 사용 전략

위에서는 캐시를 어떻게 다뤄야 하는지에 대해 알아봤습니다. 그렇다면 캐시 전략을 어떻게 세우는 것이 좋을까요?

  • 캐시된 데이터 사용 전 서버에서 재검증해야 하는 리소스의 경우 → no-cache
  • 캐싱되지 않아야하는 리소스의 경우 (계좌 정보)  no-store
  • 버전이 지정된 리소스의 경우 max-age=3153600 (최대 시간)
  • 나머지 캐시 검증을 해야하는 경우 ETag, Last-Modified 사용

토스에서의 cache-control은 다음과 같이 사용하고 있습니다.

  • HTML 파일
    • Cache-Control : max-age=0, s-maxage=3153600
    • 일반적으로 HTML 리소스는 새로 배포가 이뤄질 때마다 값이 바뀌기에 브라우저는 항상 HTML 파일을 불러올 때 새로운 배포가 있는지 확인해야 함
    • 브라우저는 HTML 파일을 가져올 때마다 프록시 서버에 재검증 요청을 보내고, 그 사이에 배포가 있다면 새로운 HTML 파일을 받음
    • CDN은 계속해서 HTML 파일에 대한 캐시를 가지고 있게 하며, 배포가 이뤄질 때마다 CDN Invalidation을 발생시켜 CDN이 서버로부터 새로운 HTML 파일을 받아오도록 설정
  • JS, CSS 파일
    • Cache-Control : max-age=3153600
    • JS, CSS 파일은 빌드시 새로 생기기에 임의의 버전 번호를 URL 앞부붙에 붙여 빌드 결과물마다 고유한 URL을 보유
    • 같은 URL에 대해 내용이 바뀔 수 있는 경우는 없음 → 리소스 캐시가 만료될 일도 없음
    • 새로 배포가 일어나지 않는 한, 브라우저는 캐시에 저장된 JS 파일을 계속 사용

참고 자료

 

HTTP Cache로 불필요한 네트워크 요청 방지

불필요한 네트워크 요청을 어떻게 피할 수 있습니까? 브라우저의 HTTP 캐시는 첫 번째 방어선입니다. 이 방법이 반드시 가장 강력하고 유연한 방법은 아니며 캐시된 응답의 수명을 제한적으로 제

web.dev