웹 사이트의 성능을 높이기 위해서는 여러 기술을 적용할 수 있다.
이 중 백엔드 개발자가 적은 시간을 투자하며 최대 성능 효과를 얻을 수 있는 방법으로 HTTP 압축, 다양한 리소스 최적화 기법(이미지, JS, CSS, 기타 리소스), HTTP 캐싱 등의 방법이 있다.
이 중 HTTP 캐시가 무엇인지와 적용 방법에 대해서 조금 더 자세히 알아보도록 하겠다.
1. 캐시 (Cache) & 캐싱 (Caching)
간단하게 설명하자면 캐시는 자주 사용되는 데이터나 값을 미리 복사해 놓는 임시 장소를 말하고 캐싱은 데이터를 이 캐시 영역에 저장하는 행위를 뜻한다.
캐시는 캐시의 접근 시간에 비해 원래 데이터를 접근하는 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용한다.
캐시에 데이터를 미리 복사해 놓으면 계산이나 접근 시간없이 더 빠른 속도로 데이터에 접근할 수 있다.
2. HTTP 캐시
HTTP 캐시도 캐시와 같다. 자주 쓰이는 문서 리소스의 사본을 보관하는 것이다. 요청과 관련된 응답을 저장한다.
클라이언트가 서버에게 요청을 보내면, 서버가 클라이언트에게 응답을 보내준다. 이 때 응답으로 받은 파일을 Local Browser Cache에 저장한다.
단, HTTP 캐시는 url과 GET 메소드에 대해서만 적용된다. (POST와 같은 다른 HTTP 메소드에 대해서는 적용되지 않는다.) url이 key값이 되어 캐싱을 하더라 라고 알고 있으면 된다.
이 후 똑같은 주소(url)로 요청을 보내면 클라이언트는 서버로 요청을 보내기 전 Local Browser Cache에 해당 url의 리소스를 캐싱되어있는지 점검해본 뒤 캐시가 존재한다면 캐시된 리소스를 통해 응답을 하는 것이다.
이렇게 HTTP 캐시는 요청과 관련된 응답을 저장하고, 이 후 동일한 요청에 대해서 저장된 응답을 재사용한다.
1) HTTP 캐시의 이점
응답을 재사용하면 몇 가지 이점이 있다.
첫 번째로, 서버에 요청을 전달할 필요가 없어지므로 클라이언트와 캐시가 가까울수록 응답이 빨라진다.
두 번째로, 이렇게 응답을 재사용할 수 있는 경우 서버는 요청을 처리할 필요가 없어진다. 요청을 파싱하고 라우팅하거나, 쿠키를 기반으로 세션을 복원하거나, DB에 결과를 쿼리하거나, 템플릿 엔진을 렌더링할 필요가 없어져 서버의 부하를 줄일 수 있다.
직접 브라우저 관리자 모드를 통해 확인해보면 속도에 유의미한 차이가 있는 것을 확인할 수 있다.
3. HTTP 캐시 종류
HTTP 캐시는 크게 private caches와 shared caches로 나뉜다.
Private Cache
Private Cache 는 특정 클라이언트에 연결된 캐시로 일반적으로 Browser Cache를 말한다. 저장된 응답은 다른 클라이언트와 공유되지 않으므로 개인 캐시는 해당 사용자에 대한 개인화된 응답을 저장할 수 있다.
Shared Cache
Shared Cache는 클라이언트와 서버 사이에 위치하며 사용자 간에 공유할 수 있는 응답을 저장할 수 있다. Shared Cache는 Proxy Cache와 Managed Cache로 세분화할 수 있다.
4. HTTP 캐시 작동 방법
클라이언트가 요청을 보내기 전 먼저 캐시에 리소스가 있는지 확인한다.
- 캐시에 리소스가 저장되어 있다(
Cache hit
)면 캐시에 있는 리소스를 통해 응답한다. - 캐시에 리소스가 저장되어 있지 않다(
Cache miss
)면 서버에 요청을 한다. 서버에서 받은 리소스를 캐시에 저장하고 응답한다. - 캐시에 리소스에 저장되어 있지만 최신 버전 인지 확인하기 위해 재검사(
Cache revalidate hit
)를 해야한다면 서버에게 최신 데이터인지 요청한다.- 최신 버전일 경우(
Revalidate hit (slow hit)
) 서버는 304 Not Modified 응답을 하고, 캐시에 있는 리소스를 통해 응답한다. - 최신 버전이 아닐 경우(
Revalidate miss
) 서버는 200 OK와 사본을 함께 응답해준다. 해당 사본을 다시 캐시에 저장하고 응답한다.
- 최신 버전일 경우(
5. HTTP 캐시 적용하기
HTTP 응답 헤더에 Cache-Control
헤더를 명시한다. Cache-Control
헤더를 명시하지 않는다면 휴리스틱 캐싱(Heuristic caching)이 된다.
클라이언트(Broswer)가 경험적(Heuristic)으로 만료 일자를 정한다. 클라이언트가 임의로 캐시한다는 의미이다.
HTTP는 가능한 한 많이 캐시하도록 설계되었기 때문에 Cache-Control 헤더를 지정하지 않더라도 특정 조건이 충족되면 응답이 저장되어 재사용된다. 이를 휴리스틱 캐싱이라고 한다.
그렇다면 어떻게 HTTP 캐시 만료(Expire)를 설정할 수 있을까
6. HTTP 캐시 검증
만료 설정 방법에는 유효 기간 설정, 조건부 요청, 강제 재검사 세 가지가 있다.
1) 유효 기간 설정 (Expires or max-age)
Cache-Control: max-age=604800
Expires: Tue, 28 Feb 2022 22:22:22 GMT
HTTP/1.0에서는 Expires
헤더를 통해 유효 기간을 설정했다.
Expries
헤더는 명시적인 시간(HTTP-date timestamp)을 사용하여 캐시의 수명을 지정한다. 음식의 유통기한과 같다고 생각하면 된다.
하지만 Expires
는 HTTP-date timestamp를 텍스트로 직접 작성해야 한다. 작성, 파싱의 어려움 및 많은 문제점(구현 버그, 의도적으로 시스템 시계를 변경)으로 HTTP/1.1 부터는 Cache-Control
의 max-age
를 사용해 경과 시간(초 단위)으로 유효 기간을 설정하는 기능이 추가되었다.
Expires
와 Cache-Control: max-age
를 둘 다 설정한 경우 max-age
가 적용된다. 따라서 HTTP/1.1이 널리 사용되는 지금은 Expires
를 사용할 필요가 없다.
2) 조건부 요청(conditional request)
If-Modified-Since (날짜 재검사)
HTTP 응답 헤더의 Last-Modified(최종 수정 날짜)
를 통해 검증하는 방법이다.
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600
<!doctype html>
…
클라이언트에서 받은 요청에 의한 응답이 다음과 같이 생성되었다고 한다면, 22:22:22에 생성되고 최대 유효 기간이 1시간이므로 23:22:22까지 fresh 하다는 것을 알 수 있다.
HTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600
경과 시간이 지나면 클라이언트에서는 If-Modified-Since
요청 헤더에 Last-Modified
의 값을 담아 서버로 재검사 요청을 보낸다.
서버에서는 해당 시간을 통해 최신 버전인지 확인한 후 응답을 보내는 것이다.
ETag / If-None-Match (엔티티 태그 재검사)
시간 형식을 사용해 검사를 진행하면 Expires
헤더를 사용할 때와 마찬가지로 문제가 많이 생겨 Etag
라는 특정 해시값을 지정해 해당 해시 값을 통해 검증하는 방법이다.
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "33a64df5"
Cache-Control: max-age=3600
<!doctype html>
…
클라이언트가 index.html에 대한 요청을 보냈을 때, index.html 리소스의 해시값이 33a64df5인 경우 ETag에 해당 해시값이 담긴 위와 같은 응답이 생성된다. 경과 시간이 지나면 클라이언트에서는 If-None-Match
요청 헤더에 ETag
값을 담아 서버로 재검사 요청을 보낸다. 서버에서는 ETag
값을 통해서 최신 버전인지 확인한 후 응답을 보내는 것이다.
캐시 재검증 중에 If-Modified-Since
와 If-None-Match
가 모두 존재하면 If-None-Match
가 유효성 검사시 우선된다. 캐싱만 고려하는 경우 Last-Modified
가 불필요하다고 생각할 수 있으나, Last-Modified
는 캐싱 뿐만 아니라 다양한 용도로 사용되는 표준 HTTP 헤더이기 때문에 ETag
와 Last-Modified
를 모두 제공하는 것이 좋다.
3) 강제 재검사(force revalidation)
요청에 대한 응답을 재사용하지 않고 항상 최신화를 하고 싶다면 no-cache
를 사용해 강제할 수 있다.
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: deadbeef
Cache-Control: no-cache
<!doctype html>
…
Cache-Control: max-age=0, must-revalidate
을 통해서도 강제 재검사를 할 수 있다.
max-age=0
은 응답이 즉시 유효하지 않음을, must-revalidate
는 응답이 유효하지 않은 경우 재검증 없이 재사용해서는 안 됨을 의미하므로, no-cache
와 동일한 의미를 가진다. 하지만 max-age=0
는 HTTP/1.1 이전에는 no-cache
를 처리할 수 없었기 때문에 대안으로 사용하던 방법이라 지금은 no-cache
를 사용하면 된다!
참고자료
우아한테크코스 웹 백엔드 구구 코치님의 HTTP 활용하기 강의를 기반으로 작성된 글입니다.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
이 글은 동글(https://donggle.blog)을 통해 포스팅 된 글입니다. 동글 많관부 👍