웹 사이트는 일반적으로 이미지, 자바스크립트, css 등의 여러가지 추가 파일을 제공해야 한다. 이것들을 흔히 정적 파일
이라고 부른다. 서버에서 직접 이 모든 정적 파일을 제공하는 방법을 택할 수도 있지만, 이런 경우는 단점이 많다.
따라서 위와 같은 정적 파일들은 일반적으로 클라우드 업체에서 제공하는 정적 파일 저장소를 사용한다. S3 등의 정적 파일 저장소는 위 단점들에 대한 완벽한 대안이 된다. 이 글에서는 S3
와 CloudFront
를 활용하여 정적 파일을 좀 더 빠르고 안정적으로, 싼 값에 제공해보도록 하겠다.
CloudFront
는 대표적인 CDN
중 하나로 Amazon에서 제공하고 있는 서비스이다. CDN은 Contents Delivery Network의 약어로, 지리적으로 분산된 여러 개의 서버이다. 수많은 이미지 콘텐츠의 빠른 전송, 대용량 콘텐츠의 초고속 다운로드, 미디어 콘텐츠의 끊김 없는 스트리밍 서비스 등 많은 분야에서 활용된다.
일반적으로 지역별로 여러 개의 서버를 두어 사용자의 요청을 가장 가까운 로컬 서버에서 처리하도록 한다. 이 과정에서 정적 파일 저장소에서 가까운 CDN 로컬 서버로 데이터를 전송해야 하는데, 해당 지역에 데이터가 들여있나 없나에 따라 사용자의 응답 속도에 차이가 난다.
CloudFront
의 경우 215개 이상의 엣지 로케이션을 가지고 있다. 또한 기본적인 로깅과 통계를 제공해 유용하게 사용할 수 있다.
cloudfront
콘솔에서 생성을 누른뒤, 연결할 S3 도메인을 선택한다.
그 아래 캐싱 정책과 원본 요청 정책을 선택할 수 있다. S3의 CORS 설정을 존중하기 위해
관리형 정책 중 CORS 관련된 설정을 선택할 수 있다.
그 후 CNAME
등을 추가로 설정할 수 있다. 이 과정 없이 CloudFront의 도메인을 받을 경우, abcd33445.cloudfront.net
과 같이 읽기 힘든 도메인이 나오게 된다. 저 url을 직접 사용자에게 노출하는 것보다 cdn.heka1024.com
과 같이 의미를 가진 도메인을 이용하는 것이 차후 다른 서비스로 변경하거나 직접 서빙을 한다는 등의 변경사항이 생겼을 때 훨씬 유연하게 대처할 수 있다. 따라서 저 부분을 꼭 의미있는 이름으로 설정해주기 바란다. 이 때 HTTPS로 서비스를 제공하기 위해서는 Cname으로 등록할 url에 대한 SSL 인증서를 등록해야 한다.
그 과정을 거치면 위와 같이 배포가 된 것을 확인할 수 있다. 그러면, 이미지가 CDN을 거쳤는지, 거친다면 얼마나 빨라지는지 확인할 수 있을까?
클라우드 프론트를 거치지 않은 경우, x-cache
에 Miss
라는 내용이 있는 걸 확인할 수 있다. 2.5MB의 이미지인데, 받아오는데 1.8초가 걸렸다.
동일한 사진을 다시 한번 요청하면 어떻게 될까?
이번엔 x-cache
에 Hit
라는 내용이 있다. 시간은 948ms가 걸렸다. 반 정도로 줄어든 것이다. 하지만 만족하기엔 이르다. 이 시간을 훨씬 줄여보자. 물론 CDN에서 할 수 있는 일은 아니고, S3 단에서 HTTP Header
를 건드려서 할 것이다.
JPG등 이미 압축이 이루어진 포맷들은 압축을 하지 않지만, CloudFront는 그렇지 않은 파일 포맷들 가령, 자바스크립트 등에 대해 압축을 해 준다.
응답의 Content-Encoding
에서 Brotli 압축이 사용된 것을 확인할 수 있다. 해당 내용은 이전 글 Accept-Encoding의 이해를 참고하자.
압축 설정은 캐시 정책에 함께 존재한다. 일반적으로 Brotli 압축이 gzip 압축에 비해 정적 파일에 대해 좀 더 나은 압축률을 보여주는 것으로 알려져 있다. 따라서 Brotli Compress를 켜주도록 하자.
HTTP 통신에서 서버는 클라이언트에게 응답을 내려주며 해당 응답은 캐시해두고 있어도 된다고 알려줄 수 있다. 일반적으로 Cache-Control
헤더를 이용해 지시하며 다음과 같은 표준 문법을 사용할 수 있다.
Cache-control: must-revalidate
Cache-control: no-cache
Cache-control: no-store
Cache-control: no-transform
Cache-control: public
Cache-control: private
Cache-control: proxy-revalidate
Cache-Control: max-age=<seconds>
Cache-control: s-maxage=<seconds>
다음과 같은 확장 문법을 사용할 수도 있으나 브라우저에 따라 지원하지 않는 경우도 있다.
Cache-control: immutable
Cache-control: stale-while-revalidate=<seconds>
Cache-control: stale-if-error=<seconds>
자주 사용하게 되는 식은 Cache-Control: max-age=86400
과 같이 캐시의 지속 시간(초 단위)를 적어주는 방식이다. 그렇다면 max-age
가 만료되지 않은 시점에 브라우저는 어떻게 동작할까?
바로 위와 같이 memory cache
에 있다고 요청을 날리지도 않게 된다. 이 경우 주어진 max-age
동안 캐시를 이용할 뿐 서버로 직접 요청을 날리지는 않게 된다.
하지만 이 방식의 큰 단점은 이미 전파된 캐시를 주워담기 매우 힘들다는 것이다. 그렇다면 최대한 캐싱하고 싶지만 업데이트 된 것을 빠르게 전파하고 싶은 경우라면 어떻게 할까?
클라이언트가 다음과 같은 응답을 받았다고 해보자.
HTTP/1.1 200 OK
ETag: "heka1024"
Cache-control: private
Content-Length: 16384
이후에 서버에 요청을 보낼 경우, 브라우저는 다음과 같이 보내게 된다.
GET heka1024/files/twice HTTP/1.1
Host: x.y
If-None-Match: "heka1024"
ETag
값이 heka1024
로 일치하므로 이 요청을 받은 서버에서는 304 Not Modified
를 보내 로컬에서 들고 있는 파일을 이용해도 됨을 알려준다. 이 과정은 서버를 한 번 거치기는 하지만 컨텐츠를 통째로 다시 내려주는 것보다는 훨신 효율적인 과정임을 알 수 있다.
만약 ETag
값이 다르다면, 서버는 새로운 ETag
값과 함께 컨텐츠를 내려주게 된다. 대략 다음과 같은 응답을 내려줄 것이다.
HTTP/1.1 200 OK
ETag: "4201akeh"
Cache-control: private
Content-Length: 8192
웹 콘솔에서는 객체 혹은 폴더를 선택한 후 작업 > 메타데이터 편집
을 선택하자.
그 후 Cache-Control
등의 헤더를 선택할 수 있다.
그 후 max-age=<seconds>
를 통해 캐시의 지속 시간을 써주자. 이 과정을 통해 정적 파일을 CDN과 로컬 모두에 캐싱함으로서 서버의 부하를 줄이고 유저에게 좀 더 빠르게 데이터를 내려줄 수 있다.