김영한님의 모든 개발자를 위한 HTTP 웹 기본 지식 강의를 수강했습니다.
강의 내용 중 캐시에 대해서만 다시 한 번 정리해보고자 합니다.
함께 이야기하며 메타인지에 도움을 준 칙촉과 필즈에게 감사를 !! 🤗🙏
요약
- 응답 헤더를 사용한 캐싱의 기준은 최종수정시간과 버전 중 한 가지를 선택해서 사용할 수 있습니다.
- 시간은 Last-Modified 와 If-Modified-Since로, 버전은 ETag 와 If-None-Match 로 사용됩니다.
- 기본적이 프로세스는 다음과 같습니다.
- 서버에서 정적 리소스 응답 시 Last-Modified 또는 ETag 헤더를 포함해서 응답합니다.
- 브라우저는 같은 리소스를 재요청할 때, If-Modified-Since 또는 If-None-Match에
이전에 Last-Modified 또는 ETag 로 받았던 값을 실어서 보냅니다.- 시간을 사용하면 최종 수정시간을 받아뒀다가, 이 시간 이후로 수정됐니 라고 물어보는겁니다.
- 버전을 사용하면 이 버전 이후로 후속 버전이 있니 라고 물어보는 거죠.
- 변경이 없었다면 304에 body가 없이 응답되고, 변경됐다면 200에 새 body와
새 Last-Modified 또는 ETag가 응답됩니다.
🗃️ 캐시 : 은닉처, 고속 기억장치
체스 기물 이미지가 바뀔 일이 없다면, 한 번만 내려받게 하자!
이미지 출처 : https://www.pngegg.com/en/png-ynnvf
레벨 1의 마지막 미션, 그리고 레벨 2의 첫번째 미션은 체스였습니다.
첫번째 웹 프레임워크를 스파크로 경험하고, 레벨2에선 이를 스프링으로 마이그레이션하는 경험을 했습니다.
체스에서 사용되는 정적 자원들에는 체스 기물 이미지가 있었습니다.
그리고 이 파일들은 어느 방을 들어가더라도 동일한 파일을 사용합니다.
즉, 한 번 체스 게임 방에 입장해서 12가지 체스 기물 이미지를 모두 서버로부터 다운받았다면,
해당 이미지를 계속해서 재사용하는 것이 사용성 측면에서도, 서버 관리 차원에서도 효율적이라 하겠습니다.
응답 속도도 빨라지고 서버의 부담도 줄어들기 때문입니다.
캐시(Cache)는 이러한 필요를 채워주기 위한 개념입니다.
정적 자원을 매번 새로 요청하는 것이 아니라, 한 번 받은 이후에는 재사용하는 거죠.
그렇지만 만약 정적 자원이 변경된다면 어떻게 될까요?
이번 포스팅에선 HTTP Header와 브라우저의 상호작용을 통해
캐시가 어떻게 관리되는지 알아보겠습니다.
브라우저가 알아서 캐싱을 해주고 있다?!
Header 정보 교환을 통한 브라우저 캐싱을 알아봅시다!
첫번째 체스 방에 입장했을 땐 Size 탭에 이미지들의 용량이 표시됩니다.
방 목록으로 나갔다가 다시 체스 방에 입장했을 땐 Size 탭에 용량이 아닌 (memory cache) 라고 표기됩니다.
서버 코드 내에 캐싱 관련 어떤 설정도 없습니다.
그러나 브라우저에서 캐싱을 처리하고 있는 것입니다.
스프링에서 디버깅을 해봐도 최초에만 모든 이미지를 요청하고,
이후에는 이미지에 대한 요청이 오지 않았습니다.
브라우저는 대체 어떻게 캐싱을 하고 있는 걸까요? 조금 더 자세히 살펴보죠.
보다 효과적인 전달을 위해, 아래와 같이 캐시 유효시간을 5초로 설정하는 코드를 추가했습니다.
@Configuration
public class CacheConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
CacheControl cacheControl = CacheControl
.maxAge(5, TimeUnit.SECONDS);
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(cacheControl);
}
}
첫번째 응답 헤더에 있는 Last-Modified
첫 요청에 대한 응답을 살펴봅시다
nb.png 를 기준으로 살펴볼게요. (nb 는 knight, black 의 의미입니다)
nb.png 에 대한 첫 요청과 첫 응답의 주요 내용은 아래와 같습니다.
GET http://localhost:8080/img/nb.png
200 OK
Content-Type: image/png
Cache-Control: max-age=5
Last-Modified : Tue, 26 Apr 2022 00:33:11 GMT
GET 으로 정적 리소스를 요청했고, 200으로 정상 응답이 왔습니다.
(Cache-Control 은 잠시 후에 알아볼게요)
Last-Modififed 라는 응답 헤더에 GMT 형식의 시간이 함께 왔습니다.
스프링의 ResourceHttpRequestHandler의 handleRequest를 보면,
정적 자원에 대한 요청이 왔을 때 처리되는 과정을 확인할 수 있습니다.
작아서 잘 안 보이긴 하지만, 스샷 내 evaluate expression을 보시면
아래와 같은 코드가 작성되어 있고, 이에 대한 결과값이 앞서 Last-Modified 헤더에 응답된 값과 동일함을 알 수 있습니다.
즉, 스프링에서 정적 자원에 대한 요청 시, 알아서 파일 시스템의 최종 수정 시간을 확인해서
Last-Modified 헤더를 생성해서 그 값을 담아주고 있음을 알 수 있습니다.
new Date(resource.lastModified()).toGMTString()
두번째 요청에 있는 If-Modified-Since
이 시간 이후로 업데이트 된 적 있나요?
1. 첫 요청 200 + 이미지 body (약 50kb)
2. 두번째 요청 200 + memory cache
3. 세번째 요청 304 + body 없음(약 0.2kb)
앞서 Last-Modified에서 살펴보지 않았던 첫 요청 응답 헤더에 있던 Cache-Control: max-age=5는,
응답을 받은 시간부터 5초 동안은 해당 자원에 대해 재요청하지 않고 캐싱된 값을 사용하라는 의미입니다.
즉 유통기한 같은 것인데요, max-age 동안은 재확인 하지 말고 재사용해도 괜찮다는 것입니다.
첫 요청의 200은 이미지를 포함해서 응답의 크기가 50kb정도 됩니다.
두번째 요청은 5초 이내로 재요청했기에 200 응답을 받았던 캐싱된 값을 재사용하기에 요청이 나가지 않습니다.
세번째 요청은 max-age 를 초과했기에 서버에 다시 요청이 나가는데요,
이때는 첫 요청과 다른 점이 있습니다.
바로 요청 헤더에 If-Modified-Since 가 포함된다는 점입니다.
이 헤더에 담는 값은 이전에 첫 요청에 대한 응답 헤더 Last-Modified로 받아뒀던 값을 사용합니다.
요청하는 자원이 이 시간 이후로 수정된 적이 있는지 물어보는 것입니다.
서버에서는 정적 리소스에 대한 요청이 왔을 때, 헤더에 If-Modified-Since가 포함되어 있다면,
이를 이용해 응답을 분기처리합니다.
만약 If-Modified-Since 에 있는 시간과 요청된 Resource의 lastModified의 시간을 비교해서,
변경되었다면 변경된 자원을 body에 담아 200으로 응답합니다.
변경되지 않았다면 body에는 아무것도 담지 않고 304 NOT_MODIFIED 응답코드로 응답합니다.
304 응답은 body에 값이 없기 때문에 트래픽이 크게 절약됩니다.
최초 응답은 이미지를 포함해서 50kb 였는데, 304 응답은 body가 필요없기에 약 0.2kb 밖에 되지 않습니다.
한 가지 첨언하고 싶은 내용은, 304 응답의 경우 Cache-Control: max-age=5 가 재차 나가지 않는다는 점입니다.
아마 브라우저의 캐시에 저장할 때부터 max-age와 Last-Modified를 함께 저장해두는 것 같습니다.
그래서 해당자원이 다시 필요할 때마다 max-age를 검증하고, 이를 초과했다면 재요청을 하되,
재요청시 If-Modified-Since 헤더를 보내는데, 이에 대한 값은 저장해두었던 Last-Modified를 사용하는 거죠.
그리고 304응답을 받으면 캐시에 저장해뒀던 max-age 만큼은 다시 요청을 보내지 않게 됩니다.
ETag 와 If-None-Match
시간이 아닌 버전으로 !
앞서 Last-Modified, Cache-Control: max-age, If-Modified-Since 헤더들을 이용해
캐싱이 처리되는 흐름을 살펴봤는데요, 이때는 기준이 시간이었습니다.
시간으로 관리할 때의 불편한 점이 있다면, 데이터가 완전히 동일한데
수정 시간만 바뀌는 경우에도 업데이트가 된 것으로 인식해 트래픽이 낭비될 수 있다는 점입니다.
또한 단순 정적 데이터에 대한 처리가 아니라, 버전 관리를 하는 파일을 내어줄 경우에도,
시간으로 관리할 경우 서버에서 캐시를 완전히 통제하지 못하는 경우가 생길 수 있습니다.
이에 대한 대안으로 서버에서 직접 버전 관리를 통한 캐시 통제를 할 수 있게 해주는
ETag 와 If-None-Match 헤더가 있습니다.
로직은 시간을 사용했을 때와 동일합니다.
첫 요청에 대한 응답 헤더에 ETag 값을 내려줍니다.
재요청 시 If-None-Match 헤더에 값을 담아서 보냅니다. 값은 ETag 헤더로 받았던 값입니다.
서버에서는 If-None-Match 헤더가 없으면 첫요청이라고 판단해서 처리하고,
If-None-Match 헤더가 있으면 검증해서 변경되었으면 200과 새 자원을 body에 담아 응답합니다.
변경되지 않았다면 304 응답코드로 응답하고 body는 비워둠으로써 트래픽을 절약합니다.
결론
- 캐싱은 주로 GET 요청에 대해서만 한다
- 스프링은 정적 리소스에 대한 응답 시, 자동으로 파일 시스템의 최종 수정시간을 Last-Modified에 담아 보낸다
- 브라우저는 최초 응답 받은 시간부터 Cache-Control의 max-age 시간 동안 재요청을 보내지 않고 재사용한다
- max-age 시간이 지난 후 필요하면 If-Modified-Since 헤더에 Last-Modified로 받았던 값을 담아 재요청을 보낸다
- 서버에서는 Last-Modified 헤더가 없으면 첫요청이니 200과 body를 응답하고
Last-Modified 헤더가 있으면 변경 여부를 검증해서 변경이 있으면 200과 body를,
변경이 없으면 304와 비어있는 body를 응답함으로써 트래픽을 절약한다 - 클라이언트는 304를 응답받으면 캐시해뒀던 값을 재사용한다. 304응답을 받은 순간부터 max-age가 새로 적용된다
- ETag 와 If-None-Match 헤더를 이용하면 시간이 아닌 서버에서 관리하는 버전으로 캐싱을 관리할 수 있다
학습 출처
'우아한테크코스 4기' 카테고리의 다른 글
📖 배민다움을 읽었습니다 (0) | 2022.07.06 |
---|---|
🧑💻 UX 워크샵 후기 (2) | 2022.07.02 |
📮 POST vs PUT (Collection, Store) (0) | 2022.06.19 |
JJWT 훑어보기 (0) | 2022.06.06 |
🪪 출입증 (8) | 2022.06.04 |