[Network] HTTP cache (browser cache, proxy cache) 파헤치기
Code/Network

[Network] HTTP cache (browser cache, proxy cache) 파헤치기

반응형

Photo by Charles Chen on Unsplash

 

저는 사실 캐싱의 의미를 명확히 이해하지 못하고 있었습니다.

어딘가에 데이터를 저장해놓는다는 건 알았지만 서버, 브라우저, 클라, 이미지, 라이브러리 등등 너무 여기저기 등장하는지라 '네트워크의 거대한 무언가'로 남아 캐싱 부분이 애매한 지식으로 남아있던 것 같습니다.

이번에 개인적으로 캐싱을 공부하며 알게된 것들이 많아 학습한 내용을 공유해볼까 합니다.


반응형

우선 cache / 캐싱 의미에 대해 설명해보겠습니다.

Cache ?

캐싱은 특정 기술이 아니라 기법의 명칭입니다.

밥을 먹기 위한 과정
- 밥솥을 열고 숟가락에 얹은 뒤 다시 자리에 앉아서 한 숟갈을 먹고 다시 밥 한 숟갈을 뜨기위해 밥솥을 열러 간다.

이 밥 데이터에 대해 캐싱을 해놓는다면 아래와 같습니다.
- 밥솥을 열고 밥을 가져와 식탁에 올려 놓는다.
- 먹는다. 또 먹는다.

캐싱은 데이터를 근접한 장소에 가져다놓고 필요한 때에 효율적으로 사용하기 위해 사용되는 기법입니다.
그렇기 때문에 브라우저 캐시, 웹 캐시, proxy 캐시, react query cache, apollo client cache 등등 다양한 곳에서 불리우고 있는 것입니다.

캐시의 이점은 크게 아래와 같습니다.

- 브라우저 로딩 속도가 빠르다
- 불필요한 네트워크 요청 / 리소스를 줄일 수 있다.
- 사용자 경험 증대

오늘은 우선적으로 HTTP 캐시에 대해 작성해보겠습니다.

 


캐싱 매커니즘

캐시는 크게 fresh / stale의 상태로 구분할 수 있습니다.

fresh - 신선한 데이터
stale - 상한 데이터, 쉽게 말해 데이터의 정합성을 보장할 수 없는 상태

우선 이런 의미를 가지고 있다 정도만 기억하고 이후 내용을 보며 세부 설명을 진행하겠습니다.

http 캐싱 매커니즘에 대해 먼저 가볍게 이해하고 들어가면 좋을 것 같습니다.

프론트엔드답지 않게 색조합이 좀 그렇긴 한데 정보 전달 목적이니 이해해주실 거라 믿습니다.

브라우저에서 GET요청이 발생하면 uri를 통해 브라우저 캐시 데이터를 먼저 탐색합니다.
캐시 데이터가 존재하지 않는다면 origin 서버로 요청을 보내고 응답을 받습니다. 
세부 설정에 따라 달라질 수 있겠지만, 캐시를 허용한 경우 해당 응답을 캐시에 저장합니다.

캐시 데이터가 존재한다면 해당 캐시 데이터가 신선한지 판별을 해야 합니다.

판별이 필요한 이유는 간단합니다.
캐싱은 데이터를 서버가 아닌 곳에 저장해놓고 사용하기 때문에 서버의 실제 데이터와 캐싱된 데이터간의 정합성 문제가 발생할 수 있기때문입니다. (이 캐시 헤더는 개발자가 지정하여 사용할 수 있습니다. )

캐시에는 두 가지 상태가 존재한다고 말씀드렸습니다.

1. 캐시가 fresh 상태인 경우

캐시가 fresh상태인 경우 실제 origin 서버까지 요청을 보내지 않고 브라우저에 데이터 응답을 보냅니다.

chrome network

위 200 응답을 보면 브라우저 캐시를 통해 가져온 데이터임을 확인할 수 있습니다.

 

2. 캐시가 stale 상태인 경우

캐시 헤더를 통해 개발자가 fresh 기간에 대해 정의 하긴 하지만, 실제로는 데이터가 변경되지 않아 같은 데이터를 제공하여도 괜찮은 경우가 있습니다.

그럼 실제로 데이터가 변경되었는지를 확인할 수 있어야 할 것입니다.
이 경우 origin 서버에서 새로운 데이터를 받아오면 데이터 정합성은 보장되겠지만 변하지 않았을 수도 있는 데이터를 요청하는 게 효율적이라고 보기는 어렵습니다.

받은 응답을 저장하는 것이 캐시 데이터이기 때문에 캐시는 캐시가 저장된 시점의 http 응답 헤더 데이터를 소유하고 있습니다.

브라우저는 데이터의 변경 여부를 검증하기 위해 저장되어 있던 캐시의 http 헤더 데이터를 함께 origin 서버로 전달합니다.
서버는 헤더에 존재하는 캐시 검증 헤더를 통해 데이터가 변경되었는지를 판별하고 데이터가 존재하는 경우 304 Not Modified 응답을 반환합니다.

이 경우 http 요청은 진행되나 응답에 Body 데이터가 존재하지 않고 헤더 정보만을 반환합니다.

첨부 스샷은 같은 문서를 처음 진입했을 때와 다시 새로고침하여 캐싱된 데이터를 받아온 경우입니다.
용량 차이를 확인할 수 있습니다.

304응답이 오면 브라우저는 캐시 데이터의 헤더를 반환받은 응답의 http 헤더로 교체하여 원본 데이터가 아닌 캐시 정보에 대한 헤더만 변경합니다.

검증 헤더를 통해 확인한 결과 데이터가 변경된 경우엔 304가 아닌 200 응답을 새롭게 내려주고, 이 데이터를 다시 캐싱할 수 있게 됩니다.

여기까지가 HTTP 캐싱의 큰 흐름입니다.

 


우선 캐시에는 private cache / public cache가 존재합니다.


1. private cache (웹 브라우저 캐시)

- 캐시가 저장된 IP에서만 사용되는 개인 캐시
- 웹사이트의 개인 정보나 브라우저 네비게이션을 캐싱하여 뒤로 가기/앞으로 가기 빠른 처리 등이 가능함

2. public cache / ex) Proxy cache, CDN cache

- 둘 이상의 클라이언트에서 사용되는 캐시
- 자주 요청되며, 자주 변경되지 않는다.

proxy cache

프록시 캐시는 본 서버로 요청이 들어가기 전 위치하며, AWS의 CloudFront과 같은 CDN (Content Delivery Network)를 예로 들 수 있습니다. 
위에서 보았던 플로우와 비슷한 방식으로 작동합니다. 

CDN은 근접한 곳에 자원을 배치하여 네트워크 사용량을 최적화하고 보다 빠른 사용자 경험을 위해 존재한다는 캐시의 역할에서 추가적으로 개인이 아닌 다수의 ip가 보다 효율적으로 자원을 이용할 수 있도록 근거리에 존재하는 저장소(엣지 로케이션)에 데이터를 저장하고 좀 더 빠르게 받아볼 수 있는 기술입니다. 

origin 서버가 미국인데 한국에서 네트워크 요청을 보낸다면 상당히 오래 걸릴 것입니다.
이를 보완하기 위해 CDN이 존재하며, CDN은 해당 리소스가 근접한 저장소에 캐싱되어있는지 확인하고 캐싱 데이터가 존재한다면 해당 데이터를 응답으로 반환하며 origin 서버까지 전달되는 요청을 줄일 수 있다는 장점이 있습니다.

공유 가능한 자원이라는 의미에서 프록시 캐시는 public으로 분류되는 것입니다.
(물론 접속한 ip에 배정된 가장 근접한 저장소에 캐싱 데이터가 존재하지 않는다면 최초 요청 시 origin 서버 요청과 같은 지연이 발생할 수 있습니다)


캐시를 관리하기 위한 헤더

http 헤더에는 Cache를 관리하기 위한 필드가 몇 개 존재하고 있습니다.

 

HTTP header : Expires

Expires: Wed, 21 Oct 2015 07:28:00 GMT

캐시의 만료일을 직접 설정할 수 있습니다.
다만 날짜를 명시해주어야 하니 Cache-Control의 'max-age', 's-max-age'에 비해 유연성이 떨어진다는 단점이 있습니다.
- (Cache-Control 헤더에 'max-age', 's-max-age'가 존재할 경우 Expires 헤더는 무시됩니다)

 

HTTP header : Pragma (http/1.0)

http/1.0에서 존재하던 Pragma입니다. http/1.1부터 좀 더 강화된 버전의 cache-control 헤더가 나타나면서 대체 가능하지만, 하위 호환성을 위해 사용되곤 합니다. (캐싱 무효화)

Pragma : no-cache

 

 

HTTP header : Cache-Control (http/1.1)

http 헤더의 cache-control 필드에는 '디렉티브'를 통해 캐시의 생명주기, 타입, 캐시 관리 방법을 정의할 수 있습니다.
cache가 얼만큼 어떤 방식으로 관리되면 좋을지 설정할 수 있는 헤더입니다.

 

 

주요한 디렉티브 몇 개를 설명해보겠습니다.

public

Cache-Control : public

 

- 모든 캐시에 의해 캐싱될 수 있는 리소스

 

private

Cache-Control : private

 

- 개인 클라이언트 환경에서만 캐시 가능한 리소스

 

max-age<second>

Cache-Control : max-age=400

 

- 캐시한 데이터를 몇 초동안 fresh한 데이터로 볼 것인지 정의
- Expires헤더와 함께 사용될 경우 Expires 헤더는 무시

 

s-max-age<second>

Cache-Control : s-max-age=400
Cache-Control : max-age=400, s-max-age=100000

 

- public cache에서만 유효하며, max-age와 동일한 역할 (public cache에서 'max-age=30, s-max-age=2000'과 같이 사용 가능하나 max-age, Expires 보다 우선순위가 높다

 

no-store

Cache-Control : no-store

- 캐싱하지 않는다. (개인정보에 민감한 내용일 때)

 

no-cache

Cache-Control : no-cache

- 데이터는 캐싱하지만, 항상 origin 서버에 검증 후 사용한다.

 

must-revalidate

Cache-Control : must-revalidate

- 프록시 캐시에서 origin 서버로 요청을 보내는 도중 네트워크 연결 문제 등이 발생할 수 있습니다.
 이 경우 504에러를 응답하기때문에 오래된 캐싱 데이터를 보여주면 안되는 경우 사용합니다.

이렇게 캐시의 관리 방식을 정의할 수 있습니다.



이제 캐시가 유효한지 검증하는 검증 헤더에 대해 알아보겠습니다.

검증 헤더

- Last-Modified (If-Modified-Since)
- Etag (If-None-Match)

검증 헤더의 목적은 stale한 캐시 데이터가 실제 서버 데이터에서도 변경되었는지 판별하기 위해 존재합니다.

하나씩 살펴 보겠습니다.

 

1. Last-Modified (조건부 요청 헤더 : if-Modified-Since)

(마지막 수정일자 기준으로 변경 여부를 판별)

캐싱 데이터는 첫 요청의 응답을 저장해놓는다 설명한 부분이 있었습니다.
단순히 body 데이터만을 저장하는 것이 아닌 데이터를 설명하는 메타 데이터인 헤더도 저장하고 있습니다.

캐시가 stale하여 서버쪽으로 실제 변경이 필요한 데이터인지 확인하기 위해 요청을 보낼 때 캐시 데이터에 존재하던 Last-Modified 값을  요청 헤더 If-Modified-Since 에 실어 아래와 같이 GET 요청을 전송합니다.

서버는 If-Modified-Since를 통해 해당 자원의 마지막 수정일자를 확인하고 동일하다면 304응답을 반환할 수 있게 됩니다.

하지만 Last-Modified, If-Modified-Since는 1초 미만 단위로 캐시 조정이 불가능하며, 날짜 형식이기때문에 실제 데이터가 변경되었는지 정확히 판별하기 어려운 부분이 있습니다.

A데이터를 수정하여 B데이터로 가공했다가 다시 A데이터로 수정하여 원복하는 경우를 예로 들어보겠습니다.
결과적으로 데이터는 동일하나 Last-Modified는 변경되기 때문에 캐싱 로직을 별도로 서버에서 관리하고싶을 수도 있습니다.

이를 보완하기 위해 ETag 헤더를 사용할 수 있습니다.

 

2. Etag (조건부 요청 헤더 : If-None-match)

(Etag 값 기준으로 변경 여부를 판별)

Etag (Entity tag) 값은 개발자가 지정하여 사용할 수 있습니다. 버전명, 컨텐츠의 해시값 등으로 정할 수도 있습니다.

검증 로직과 동일하게 stale한 캐시의 변경 여부를 감지하기 위해 응답을 통해 받아 놓았던 Etag의 값을 요청 헤더의 If-None-Match에 실어 요청을 보냅니다.

간단하게 리소스의 Etag값과 If-None-Match의 값이 일치하는지 확인 후 처리합니다.

 


웹 성능 최적화와 캐싱은 떼어낼 수 없는 존재인 것 같습니다.

평소에 업무를 진행하면서 '여기 최적화시켜야 하는데...' 하던 부분이 꽤나 있었는데 잠시 여유시간을 틈타 이번주엔 캐싱과 이미지 최적화를 공부하며 개선을 조금 시도해보았습니다.

멀게만 느껴지던 CloudFront도 직접 만져보며 CDN을 이해하게 되고, 어쩌다 보니 AWS lambda@Edge까지 흘러가서 AWS cloud9, cloudWatch까지 사부작 사부작 만져보았는데 머리가 얼마나 뜨거웠는지 모릅니다.

그래도 뭐랄까 요즘엔 공부를 하다 보면 하나씩 연결되는 느낌이 들면서 기분 좋은 고양감이 들 때가 있는데 이럴땐 참 개발이 재밌게 느껴지는 것 같습니다. 

이번 글도 가볍게 쓰려고 했는데 쓰다 보니 분량 조절이 안돼서 생각보다 오래 걸렸네요.

 

그래도 누군가에게 도움이 되었으면 좋겠네요!

감사합니다!

 

반응형