본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
이론/HTTP

HTTP 웹 기본 지식 - 캐시와 조건부 요청

by 방구석 대학생 2022. 2. 24.

"인프런 - 모든 개발자를 위한 HTTP 웹 기본 지식 강의를 듣고 작성한 글입니다."

https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC/dashboard

 

모든 개발자를 위한 HTTP 웹 기본 지식 - 인프런 | 강의

실무에 꼭 필요한 HTTP 핵심 기능과 올바른 HTTP API 설계 방법을 학습합니다., - 강의 소개 | 인프런...

www.inflearn.com

캐시 기본동작

만약 캐시가 없는 경우 클라이언트와 서버는 어떻게 동작할까?

처음에 클라이언트에서 서버에게 용량이 큰 이미지 데이터를 조회하면 그에 따라 서버에서는 해당 이미지 데이터를 클라이언트에게 전송해서 정상적으로 화면에 출력 되도록해준다.

이어서 두번째로 똑같은 이미지 데이터를 다시 한번 조회하면 서버는 첫번째 와 똑같은 방식으로 이미지를 찾아서 클라이언트에게 전송을 해주게 된다.

 

이와 같이 캐시 없이 동작하는 경우 다음과 같은 문제점이 발생한다.

- 데이터가 변경되지 않아도 계속 네트워크를 통해서 동일한 데이터를 다운로드 받아야 한다.

- 그런데 인터넷 네트워크는 매우 느리고 비싸다

- 이로인해 브라우저 로딩 속도가 느려지게 된다.

- 그 결과 사용자는 굉장히 느린 프로세스 진행 처리과정을 겪게 된다.

 

반면에 캐시가 적용되어 있는 경우는 어떻게 될까?

처음 이미지를 조회할 때 클라이언트 측의 브라우저에서 캐시를 사용할 경우 서버측의 응답 HTTP 메시지에 cache-control 옵션을 넣어줄 수 있고, 이를 통해 캐시의 유효시간을 지정해 줄 수 있다.

위와 같은 요청이 클라이언트로 전송되면 웹 브라우저의 캐시 저장소에 해당 응답이 cache-control 에 지정해둔 유효시간 만큼 저장되게 된다.

이후 두번째 요청부터는 캐시 저장소를 먼저 찾아보는데, 해당 저장소에 클라이언트가 서버에 요구하고자 하는 데이터가 남아있을 경우 캐시 저장소에서 해당 데이터를 곧장 클라이언트로 가져오게 된다.

- 캐시 덕분에 캐시 유효시간 동안 네트워크를 사용하지 않아도 된다.

- 비싼 네트워크 사용량을 줄일 수 있다.

- 이로인해 브라우저 로딩 속도가 매우 빨라진다.

- 그 결과 사용자는 매우 빠른 프로세스 처리과정을 경험할 수 있다.

보통 웹 브라우저에서 들어갔던 곳 웹 페이지에 다시 들어가면 굉장히 빠른 속도로 페이지가 로딩되는 것을 확인할 수 있는데, 이게 바로 캐시가 적용되었기 때문이다.

 

그렇다면 캐시 저장소에 저장된 데이터의 유효시간이 다 지났을 경우엔 어떻게 될까?

- 캐시 유효시간이 초과하면, 서버를 통해 데이터를 다시 조회하고 캐시를 갱신한다.

- 이때 다시 네트워크 다운로드가 발생한다.

 

하지만 역시나 캐시의 유효시간이 만료 되었다고 해도 똑같은 파일을 다시 다운로드 받는 것은 조금 아쉽다.

이를 해결하려면 어떻게 해야할까?

 

* 검증 헤더와 조건부 요청 - 1

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

1. 서버에서 기존 데이터를 변경함

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

 

여기서 2의 경우 문제를 해결할 수 있는 방법이 있다.

- 캐시 만료 후에도 서버에서 데이터를 변경하지 않은 상황이라고 해보자.

- 생각해보면 데이터를 전송하는 대신에 저장해 두었던 캐시를 재사용 할 수도 있다.

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

 

클라이언트의 데이터와 서버의 데이터가 같다는 사실을 확인하는 방법으로 검증 헤더를 사용해볼 수 있다.

 

검증 헤더추가

Last-Modified 옵션은 해당 정보가 마지막에 수정된 시간을 의미한다.

위의 옵션이 추가된 응답 메시지가 클라이언트와 캐시 저장소로 전달된다. 이때 캐시 저장소에 마지막으로 수정된 시간 데이터가 기록된다.

여기서 캐시의 유효시간이 지나서 캐시 데이터가 사라졌다고 해보자.

 

그렇다면 클라이언트 에서는 서버에 다시 요청을 보내야 하는데, 이때 캐시 저장소에 해당 데이터에 대해 마지막으로 수정된 시간에 대한 기록이 있다면 해당 정보를 요청 메시지에 같이 실어서 보낸다.(If-modified-since 옵션)

서버에서 해당 요청을 받고 요청에 들어있던 최종 수정일과 전달해줘야 하는 데이터의 최종 수정일을 비교한다.

이렇게 되면 서버에서 요청받은 데이터가 변동 되었는지, 그렇지 않은지 검증을 할 수 있게 되고, 만약 변동되지 않았을 경우 아래의 그림과 같이 클라이언트에게 304 Not Modified 상태코드가 담긴 HTTP 메시지를 보낸다. 이때, HTTP BODY 에 아무런 내용도 존재하지 않는다.

HTTP BODY 에 아무런 내용도 담겨 있지 않은 상태이기 때문에 네트워크에 가해지는 부하가 크게 줄어들게 된다.

서버측에서 넘어온 304 Not Modified 상태코드가 포함된 응답 메시지를 캐시 저장소가 확인하고, 해당 데이터가 변경되지 않았음을 인식하고 나면 캐시 저장소는 이전 응답 데이터를 재사용하고 헤더 데이터 갱신을 통해 재사용된 캐시 데이터의 유효시간을 다시 설정해준다.

캐시 저장소에서 유효기간이 만료된 캐시 데이터의 재사용처리가 완료되고 나면, 해당 캐시 데이터를 클라이언트에게 전달해준다.

 

* 정리

- 캐시 유효시간이 초과해도, 서버의 데이터가 갱신되지 않았으면 304 Not Modified + 헤더 메타 정보로만 응답한다.(HTTP BODY 에 아무런 내용도 들어가지 않는다.)

- 클라이언트는 서버가 보낸 응답 헤더 정보로 캐시의 메타 정보를 갱신한다.

- 클라이언트는 캐시에 저장되어 있는 데이터를 재활용한다.

- 결과적으로 네트워크 다운로드가 발생하지만 용량이 적은 헤더 정보만 다운로드 된다.

- 매우 실용적인 해결책이다.

 

* 검증 헤더와 조건부 요청 - 2

* 검증 헤더

- 캐시 데이터와 서버 데이터가 같은지 검증하는 데이터이다.

- Last-Modified, ETag

 

* 조건부 요청 헤더

- 검증 헤더로 조건에 따라 분기한다.

- If-Modified-Since : 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, 헤더 데이터만 전송한다.(HTTP BODY 미포함)

'   - 전송 용량 0.1M(헤더 0.1M)

- 데이터 변경 예시 :

   - 캐시 : 2020년 11월 10일 10:00:00 vs 서버 : 2020년 11월 10일 11:00:00

   - 200 OK, 모든 데이터를 전송한다. (HTTP BODY 포함)

   - 전송 용량 0.1M + a (a : HTTP BODY 용량)

 

* Last-Modified, If-Modified-Since 의 단점

- 1초 미만 (0.x 초) 단위로 캐시 조정이 불가능하다.

- 날짜 기반의 로직을 사용한다.

- 데이터를 수정해서 날짜가 다르지만, 같은 데이터를 수정해서 데이터 결과가 똑같은 경우에 대해 대처할 수 없다.

- 서버에서 별도의 캐시 로직을 관리 할 수 없다.

- 예시 : 스페이스나 주석 처럼 크게 영향이 없는 변경에서 캐시를 유지하고 싶은 경우 활용하기 힘들다.

 

* ETag, If-None-Match

- ETag(Entity Tag)

- 캐시용 데이터에 임의의 고유한 버전 이름을 달아둔다.

- 데이터가 변경되면 이 이름을 바꾸어서 변경한다. (Hash 를 다시 생성)

- 진짜 단순하게 ETag 만 보내서 같으면 유지, 다르면 다시 데이터를 받는다.

 

* ETag 가 사용되는 과정을 자세히 알아보자.

1. 클라이언트에서 서버에 요청을 하면, 서버측에서는 응답 메시지에 ETag 를 포함시킨다.

2. 서버측에서 클라이언트로 응답을 전달하면 웹 브라우저의 캐시 저장소에 ETag 의 값을 저장한다.

3. 클라이언트에서 아까와 같은 요청을 다시 했을 때 캐시 유효시간이 초과된 경우, 캐시 저장소에서 가지고 있는 ETag 가 있으면 서버에 전달하는 요청에 ETag 값을 포함시킨다.

이때 ETag 값은 If-None-Match 헤더로 전달된다.

4. 서버에서 전달받은 ETag 값과 동일한 데이터가 있다면 If-None-Match 검증이 실패하게 된다. 그 결과로 아직 데이터가 수정되지 않았다는 사실을 알 수 있게 된다.

5. 이럴 경우 서버에서 클라이언트 측으로 보내는 응답 메시지에 304 Not Modified 상태코드를 싣는다.(HTTP BODY 에는 아무것도 담기지 않는다.)

6. 서버의 응답을 받으면 내부적으로 캐시를 갱신해서 재활용한다.

 

* ETag, If-None-Match 정리

- 진짜 단순하게 ETag 만 서버에 보내서 같으면 유지, 다르면 다시 받는다.

- 캐시 제어 로직을 서버에서 완전히 관리하는 방법이다.

- 클라이언트는 단순히 이 값을 서버에 제공한다.(클라이언트는 캐시 매커니즘을 모른다.)

예시 :

- 서버는 배타 오픈 기간인 3일 동안 파일이 변경되어도 ETag 를 동일하게 유지한다.

- 애플리케이션 배포 주기에 맞추어 ETag 가 모두 갱신된다.

 

* 캐시와 조건부 요청헤더 정리

- Cache-Control : 캐시 제어

- Pragma : 캐시 제어(하위 호환)

- Expires : 캐시 유효기간(하위 호환)

 

* Cache-Control : 캐시 지시어(directives)

- Cache-Control : max-age -> 캐시 유효시간, 초 단위이다.

- Cache-Control : no-cache -> 데이터는 캐시해도 되지만, 항상 원(origin) 서버에 검증하고 사용한다.

: 항상 캐시를 사용하기 전에 서버측을 통해 조건부 요청을 수행한다.

항상 원(origin) 서버에 검증한다는 것을 강조하는 이유는, 클라이언트와 서버 사이에 캐시, 또는 프록시 서버와 같은 것이 존재하기 때문이다.

조건부 요청은 중간에 존재하는 캐시 서버에서가 아니라 항상 원(origin) 서버에서 처리해야 한다.

- Cache-Control : no-store -> 데이터에 민감한 정보가 있으므로 저장하면 안된다.(메모리에서 사용하고 최대한 빨리 삭제)

 

* Pragma : 캐시 제어(하위 호환)

- Pragme : no-cache, HTTP 1.0 하위 호환이다.

- 지금은 거의 사용하지 않는다고 한다.

 

* Expires : 캐시 만료일 지정(하위 호환)

- 캐시 만료일을 아래와 같이 정확한 날짜로 지정한다.

- expires: Mon, 01 Jan 1990 00:00:00 GMT

- HTTP 1.0 부터 사용한다.

- 지금은 더 유연한 Cache-Control: max-age 를 권장한다.

- Cache-Control: max-age 와 함께 사용하면 Expires 는 무시된다.

 

* 검증 헤더와 조건부 요청 헤더 정리

* 검증 헤더(Validator)

- ETag: "v1.0", ETag: "asid93jkrh2l" -> 그냥 임의의 값을 지정하면 된다.

- 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) 서버에 직접 접근하는 경우, 클라이언트와 서버간의 거리가 멀 수록 서버에 접근하는 시간이 오래 걸리게 된다.

이를 방지하기 위해 클라이언트 에게서 가까이 있는 위치에 프록시 캐시 서버를 두고, 클라이언트 측에서 프록시 서버로 접근하도록 하는 방법이다.

이렇게 하면 프록시 서버에서 원 서버에게 전송받은 데이터를 가지고 있기 때문에 클라이언트 입장에서는 원 서버에 접근할 때보다 더 빠르게 서버측의 응답을 받아볼 수 있게 된다.

물론 처음 데이터를 요청할 땐 프록시 캐시 서버에 해당 데이터가 없기 때문에 속도가 느린편이다. 하지만 그 다음부터 요청할 때는 프록시 캐시 서버에 해당 데이터가 존재하기 때문에 클라이언트 측은 굉장히 빠른 속도로 데이터를 받을 수 있게 된다.

+ 참고 : 클라이언트 측에서 웹 브라우저 상에 존재하는 캐시를 private 캐시라고 하고, 클라이언트와 서버의 중간에 있는 프록시 캐시 서버는 public 캐시 라고 한다.

 

* Cache-Control : 캐시 지시어(directives) - 기타

- Cache-Control : public -> 응답이 public 캐시에 저장되어도 된다.

- Cache-Control : private -> 응답이 해당 사용자만을 위한 것이다. private 캐시에 저장해야 한다.(기본값)

- Cache-Control : s-maxage -> 프록시 캐시에만 적용되어 있는 max-age 이다.

- Age: 60 (HTTP 헤더) -> 원 서버에서 응답 후 프록시 캐시 내에 머문 시간(초) 이다.

 

 

캐시 무효화 - Cache-Control : 확실한 캐시 무효화 응답

예를 들어 현재 사용자의 통장 잔고와 같은 정보는 계속 갱신이 될 수 있기 때문에 캐시가 되면 좋지 않을 것이다.

그런 경우 캐시를 무효화시킬 방법이 필요하다.

- 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-cache vs must-revalidate

- no-cache : 기본 동작

no-cache + ETag 를 통해 원 서버와 요청, 응답을 주고 받다가 갑자기 프록시 캐시 서버와 원 서버간에 연결이 끊어진 상황이라고 가정하자.

이럴경우 클라이언트의 요청을 서버에서 검증 받지 못하게 되는데, no-cache 로 옵션이 설정되어 있는 경우 프록시 캐시 서버의 설정에 따라 캐시 데이터를 반환해 줄 수도 있다.

(최신으로 수정된 데이터가 아닌 옛날 데이터라고 해도) 즉, 오류보다는 오래된 데이터라도 프록시 캐시 서버에서 클라이언트로 보내주는 것이다.

 

- must-revalidate

프록시 캐시 서버와 원 서버간에 네트워크가 단절된 경우, 클라이언트 입장에서는 원 서버에게 요청을 보낼 수가 없게된다.

이렇게 되었을 때, must-revalidate 옵션으로 요청이 설정되어 있다면, 원 서버에 접근하지 못 하게 된 경우 항상 오류를 발생시킨다.(504 - Gateway Timeout)