https://github.com/bugoverdose/mys3
이미지 호스팅 서버를 통해 알아보는 웹 성능 개선 기법들
https://github.com/bugoverdose/mys3
cache cache-busting cache-control etag filesystem http java webp
Last synced: 10 months ago
JSON representation
이미지 호스팅 서버를 통해 알아보는 웹 성능 개선 기법들
- Host: GitHub
- URL: https://github.com/bugoverdose/mys3
- Owner: bugoverdose
- Created: 2022-08-06T11:01:50.000Z (over 3 years ago)
- Default Branch: master
- Last Pushed: 2022-09-10T16:03:16.000Z (over 3 years ago)
- Last Synced: 2025-03-25T12:53:31.721Z (11 months ago)
- Topics: cache, cache-busting, cache-control, etag, filesystem, http, java, webp
- Language: Java
- Homepage:
- Size: 112 KB
- Stars: 4
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# MyS3: 이미지 호스팅 서버
## 배경
- 원래 해당 프로젝트는 우테코에서 S3 사용이 금지됨에 따라 만들어본 간단한 토이 프로젝트였습니다.
- 학습 목적으로 정적 데이터를 제공하는 서버측에서 할 수 있는 성능 개선 기법들을 몇 가지 적용해보았습니다.
## API 개요
- 이미지 조회 API : `GET /images/{uploadPath}/{fileName}`
- 이미지 저장 API : `POST /api/images/{uploadPath}`
- `image` 필드의 값으로 업로드할 이미지를 추가해줘야 합니다.
- (선택) 파라미터 값으로 `fileName`, `version` 설정할 수 있습니다.
- `filename`을 명시하지 않으면 업로드하는 파일의 이름을 그대로 활용하되, 확장자만 `webp`로 변경됩니다.
- `version`은 파일을 `filename-version.webp` 형식으로 저장해줍니다. 버전에 따른 URL 변경을 통한 캐쉬 버스팅에 활용될 수 있습니다.
- 이미지 삭제 API : `DELETE /api/images/{uploadPath}/{fileName}`
## 캐쉬 기반 성능 개선
### 1) ETag 헤더
- `ETag`/`If-None-Match` 헤더를 통해 웹 사이트의 성능 개선을 시도하였습니다.
- 특정 클라이언트의 캐쉬 저장소에 저장되어있던 파일을 서버로 다시 요청한 경우, 서버에서는 etag 값의 비교를 통해 캐쉬 저장소의 데이터와 서버의 데이터가 동일한지를 확인합니다.
- 만일 클라이언트가 서버에서 전송하려는 데이터와 동일한 데이터를 이미 지니고 있다면 `304 Not Modified`를 응답하여 해당 캐쉬를 재사용하도록 합니다.
- 이를 통해 네트워크 상에서 오가는 데이터의 양(bandwidth)을 줄임으로써 응답 속도를 크게 감소시킬 수 있습니다.
- 다만, etag를 생성하기 위해 해당 이미지 파일을 조회하는 작업에 따른 서버 측의 부담 자체는 여전히 존재합니다.
#### ShallowEtagHeaderFilter 동작 방식
1. 이미지 조회시, 서버는 응답하기 직전에 HTTP 요청의 `If-None-Match` 헤더 값과 응답하려는 이미지 파일에 해당되는 etag 값을 비교합니다.
- 이를 위해 서버에서는 클라이언트로 보내려는 이미지 파일의 내용을 토대로 etag 값을 생성합니다.
- 이미지 파일의 내용이 변하지 않았다면 매번 동일한 etag 값이 생성됩니다.
- 해당 경로에 저장된 파일의 내용이 다른 파일로 변경된 경우, 응답될 때 다른 etag 값이 생성됩니다.
2. HTTP 요청의 `If-None-Match` 헤더 값이 없는 경우 해당 클라이언트에서는 해당 이미지를 최초로 보낸 것으로 간주하며, `200 OK` 로 응답합니다.
- HTTP 응답에는 요청한 이미지 파일의 내용이 담기며, `ETag` 헤더 값에 해당 이미지 파일에 대해 생성된 etag 값이 담깁니다.
- 브라우저에서는 캐쉬 저장소에 `ETag` 헤더의 값을 해당 캐쉬에 대해 함께 저장합니다.
- 이후 브라우저에서는 캐쉬 저장소에 등록된 해당 데이터를 서버에 다시 요청할 때 `If-None-Match` 헤더에 해당 etag 값을 담아 요청하게 된다.
3. HTTP 요청의 `If-None-Match` 헤더의 etag 값이 서버에서 응답하려는 이미지 파일의 etag 값과 동일한 경우 `304 Not Modified`를 응답합니다.
- 이때 응답에는 요청한 이미지 파일이 담기지 않으므로, 네트워크 상을 오가는 데이터의 양은 훨씬 적고 응답은 더 빠릅니다.
- 브라우저에서는 `304` 응답 코드를 받게 되면 자동으로 캐쉬 저장소의 데이터를 계속 재사용하게 됩니다.
4. HTTP 요청의 `If-None-Match` 헤더의 etag 값이 서버에서 응답하려는 이미지 파일의 etag 값과 다른 경우 `200 OK`를 응답합니다.
- 이 경우 해당 이미지 파일이 이전과는 달라졌으므로 HTTP 응답에는 요청한 이미지 파일의 내용과 그에 대응되는 `ETag` 헤더 값이 담깁니다.
- 브라우저에서는 응답 내용을 토대로 캐쉬 저장소의 캐쉬를 갱신한다.
### 2) Cache-Control 헤더
- 기본적으로 이미지 조회시 응답에는 다음과 같은 헤더값이 추가됩니다.
- `Cache-Control: max-age=600, public`
- 이는 캐쉬 저장소에 해당 URI에 대한 응답 정보가 존재하는 경우 10분 동안 해당 캐쉬 값을 그대로 사용하라는 의미입니다.
- 브라우저와 서버 사이의 컴포넌트들로부터 Shared Cache의 이점을 볼 수 있도록 기본적으로 `public` 옵션을 설정하였습니다.
- `max-age` 값은 `application.yml` 파일의 `cache.max-age` 프로퍼티의 값에 해당하므로 쉽게 수정할 수 있습니다.
### 3) Cache Busting 지원
- 기본적으로 캐쉬 버스팅이란 캐쉬의 유효기간을 최대한 높게 설정하여 오랜기간 재활용하되, 버전이 변경되었을 때에 즉시 서버에 요청을 보내도록 하는 기법입니다.
- 이를 위해서는 **자원의 URI를 버전에 따라 다르게 설정**함으로써 캐쉬 저장소에 관리 중인 캐쉬를 사용하지 않도록 해야 합니다.
- 이를 구현할 수 있도록 해당 서버에서는 캐쉬의 유효기간을 수정하는 것만이 아니라 이미지를 업로드할 때 `version` 정보를 받아 이미지의 파일명에 추가하는 기능을 구현했습니다.
#### 구현 방법 예시
1. 우선 `Cache-Control` 헤더의 `max-age` 값을 31536000초로 설정함으로써 1년 동안 캐쉬를 재사용하도록 합니다.
2. 특정 이미지를 업로드할 때 `version` 정보를 명시하면 해당 파일명에는 해당 버전 정보가 추가됩니다.
- 예를 들어 `user/profile1`라는 파일을 저장할 때 `asdfds`라는 해쉬값을 버전으로 명시하면, 파일은 `user/profile1_asdfds.webp`와 같이 저장됩니다.
3. 특정 클라이언트에서 `GET /images/user/profile1_asdfds.webp`로 요청을 보내는 경우, 해당 URI에 대한 응답은 1년 동안 재사용됩니다.
4. 이때 해당 파일을 새로운 버전으로 수정해야 하는 경우, **새로운 버전의 이미지를 업로드**하고 사용자에게 이를 사용하도록 강제하도록 하면 됩니다.
- 예를 들어 기존과 같은 `user/profile1`라는 파일을 저장하되 `bkfefd`처럼 다른 해쉬값을 버전으로 명시하면, 파일은 `user/profile1_bkfefd.webp`와 같이 저장됩니다.
- 이후 기존 버전의 파일은 불필요해졌으므로 `DELETE /api/images/user/profile1_asdfds`와 같은 요청으로 제거할 수 있습니다.
5. 이제 클라이언트에서 `GET /images/user/profile1_bkfefd.webp`로 요청을 보내도록 한다면 당연히 캐쉬 저장소에 있는 `asdfds` 버전 데이터를 사용하지 않고 서버로 요청을 보내게 됩니다.
## 기타 성능 개선
### 1) `.webp`
- 해당 서버는 업로드되는 이미지 파일을 `.webp` 확장자로 저장하여 제공합니다.
- 이를 통해 응답 성능 개선만이 아니라, 서버의 부담을 줄이고 하드 디스크에 더 많은 파일을 저장할 수 있도록 해줍니다.
- 물론 업로드하려는 파일이 이미지 파일이 아닌 경우 `400 Bad Request`로 응답합니다.
- cf) `webp`의 장점
- 무손실 압축을 통해 이미지 품질은 유지하면서도 파일 크기 자체를 감소시키기 위한 확장자.
- I.E.를 제외한 모든 브라우저들에서 호환되는 형식. ([참고](https://caniuse.com/webp))
- gif 등 모든 이미지 형식에 대응 가능.
### 2) HTTP 압축
- HTTP 응답을 압축함으로써 웹 사이트의 성능을 향상시키고자 하였습니다.
- `server.compression.enabled` 프로퍼티를 통해 Spring Boot에서 제공하는 기본 설정인 gzip 압축 알고리즘을 적용하였습니다.
- HTTP 응답에 다음 헤더들이 추가됩니다.
- `Content-Encoding: gzip`
- `Transfer-Encoding: chunked`
---
## 실습 방법
- 직접 해당 서버를 실행해보고 싶은 경우, 아래 두 가지 정보를 알아야 합니다.
- 데모 내용이 궁금한 경우 [블로그](https://bugoverdose.github.io/development/image-hosting-server-toy-project/)를 참고하기 바랍니다.
- 주의. API 및 구현된 기능면에서 현재 서버와 다소 차이가 있습니다.
### 이미지 파일 저장 경로
- 디폴트 경로: `~/static/images/{uploadPath}/{fileName}`
- `static/images`는 `application.yml` 파일의 `image.storage.root-directory` 프로퍼티의 값에 해당함.
- 해당 yml 파일의 프로퍼티 값을 수정하거나, 구동 시점에 값을 주입해줌으로써 저장 경로 변경 가능.
### 인증 기능
- 이미지를 업로드/제거하려는 경우, 해당 API를 호출할 때 `Authorization` 헤더 값으로 서버에서 설정한 비밀 키를 추가해줘야 합니다.
- 비밀 키는 `application.yml` 파일의 `security.authorization-key` 프로퍼티 값에 해당합니다.
- `prod` 프로파일로 실행하는 경우 `SECRET_KEY` 환경변수 값을 활용하게 됩니다.