{"id":16921848,"url":"https://github.com/bugoverdose/mys3","last_synced_at":"2026-02-10T20:30:47.441Z","repository":{"id":109989050,"uuid":"521922303","full_name":"bugoverdose/MyS3","owner":"bugoverdose","description":"이미지 호스팅 서버를 통해 알아보는 웹 성능 개선 기법들","archived":false,"fork":false,"pushed_at":"2022-09-10T16:03:16.000Z","size":115,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-03T19:03:08.316Z","etag":null,"topics":["cache","cache-busting","cache-control","etag","filesystem","http","java","webp"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bugoverdose.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-08-06T11:01:50.000Z","updated_at":"2023-04-14T15:48:18.000Z","dependencies_parsed_at":"2023-03-13T13:59:34.948Z","dependency_job_id":null,"html_url":"https://github.com/bugoverdose/MyS3","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/bugoverdose/MyS3","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugoverdose%2FMyS3","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugoverdose%2FMyS3/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugoverdose%2FMyS3/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugoverdose%2FMyS3/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bugoverdose","download_url":"https://codeload.github.com/bugoverdose/MyS3/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugoverdose%2FMyS3/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29314705,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-10T17:48:59.043Z","status":"ssl_error","status_checked_at":"2026-02-10T17:45:37.240Z","response_time":65,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache","cache-busting","cache-control","etag","filesystem","http","java","webp"],"created_at":"2024-10-13T19:53:20.257Z","updated_at":"2026-02-10T20:30:47.404Z","avatar_url":"https://github.com/bugoverdose.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MyS3: 이미지 호스팅 서버\n\n## 배경\n\n- 원래 해당 프로젝트는 우테코에서 S3 사용이 금지됨에 따라 만들어본 간단한 토이 프로젝트였습니다.\n- 학습 목적으로 정적 데이터를 제공하는 서버측에서 할 수 있는 성능 개선 기법들을 몇 가지 적용해보았습니다.\n\n## API 개요\n\n- 이미지 조회 API : `GET /images/{uploadPath}/{fileName}`\n- 이미지 저장 API : `POST /api/images/{uploadPath}`\n    - `image` 필드의 값으로 업로드할 이미지를 추가해줘야 합니다.\n    - (선택) 파라미터 값으로 `fileName`, `version` 설정할 수 있습니다.\n        - `filename`을 명시하지 않으면 업로드하는 파일의 이름을 그대로 활용하되, 확장자만 `webp`로 변경됩니다.\n        - `version`은 파일을 `filename-version.webp` 형식으로 저장해줍니다. 버전에 따른 URL 변경을 통한 캐쉬 버스팅에 활용될 수 있습니다.\n- 이미지 삭제 API : `DELETE /api/images/{uploadPath}/{fileName}`\n\n## 캐쉬 기반 성능 개선\n\n### 1) ETag 헤더\n\n- `ETag`/`If-None-Match` 헤더를 통해 웹 사이트의 성능 개선을 시도하였습니다.\n- 특정 클라이언트의 캐쉬 저장소에 저장되어있던 파일을 서버로 다시 요청한 경우, 서버에서는 etag 값의 비교를 통해 캐쉬 저장소의 데이터와 서버의 데이터가 동일한지를 확인합니다.\n- 만일 클라이언트가 서버에서 전송하려는 데이터와 동일한 데이터를 이미 지니고 있다면 `304 Not Modified`를 응답하여 해당 캐쉬를 재사용하도록 합니다.\n- 이를 통해 네트워크 상에서 오가는 데이터의 양(bandwidth)을 줄임으로써 응답 속도를 크게 감소시킬 수 있습니다.\n- 다만, etag를 생성하기 위해 해당 이미지 파일을 조회하는 작업에 따른 서버 측의 부담 자체는 여전히 존재합니다.\n\n#### ShallowEtagHeaderFilter 동작 방식\n\n1. 이미지 조회시, 서버는 응답하기 직전에 HTTP 요청의 `If-None-Match` 헤더 값과 응답하려는 이미지 파일에 해당되는 etag 값을 비교합니다.\n    - 이를 위해 서버에서는 클라이언트로 보내려는 이미지 파일의 내용을 토대로 etag 값을 생성합니다.\n    - 이미지 파일의 내용이 변하지 않았다면 매번 동일한 etag 값이 생성됩니다.\n    - 해당 경로에 저장된 파일의 내용이 다른 파일로 변경된 경우, 응답될 때 다른 etag 값이 생성됩니다.\n\n2. HTTP 요청의 `If-None-Match` 헤더 값이 없는 경우 해당 클라이언트에서는 해당 이미지를 최초로 보낸 것으로 간주하며, `200 OK` 로 응답합니다.\n    - HTTP 응답에는 요청한 이미지 파일의 내용이 담기며, `ETag` 헤더 값에 해당 이미지 파일에 대해 생성된 etag 값이 담깁니다.\n    - 브라우저에서는 캐쉬 저장소에 `ETag` 헤더의 값을 해당 캐쉬에 대해 함께 저장합니다.\n    - 이후 브라우저에서는 캐쉬 저장소에 등록된 해당 데이터를 서버에 다시 요청할 때 `If-None-Match` 헤더에 해당 etag 값을 담아 요청하게 된다.\n\n3. HTTP 요청의 `If-None-Match` 헤더의 etag 값이 서버에서 응답하려는 이미지 파일의 etag 값과 동일한 경우 `304 Not Modified`를 응답합니다.\n    - 이때 응답에는 요청한 이미지 파일이 담기지 않으므로, 네트워크 상을 오가는 데이터의 양은 훨씬 적고 응답은 더 빠릅니다.\n    - 브라우저에서는 `304` 응답 코드를 받게 되면 자동으로 캐쉬 저장소의 데이터를 계속 재사용하게 됩니다.\n\n4. HTTP 요청의 `If-None-Match` 헤더의 etag 값이 서버에서 응답하려는 이미지 파일의 etag 값과 다른 경우 `200 OK`를 응답합니다.\n    - 이 경우 해당 이미지 파일이 이전과는 달라졌으므로 HTTP 응답에는 요청한 이미지 파일의 내용과 그에 대응되는 `ETag` 헤더 값이 담깁니다.\n    - 브라우저에서는 응답 내용을 토대로 캐쉬 저장소의 캐쉬를 갱신한다.\n\n### 2) Cache-Control 헤더\n\n- 기본적으로 이미지 조회시 응답에는 다음과 같은 헤더값이 추가됩니다.\n  - `Cache-Control: max-age=600, public`\n  - 이는 캐쉬 저장소에 해당 URI에 대한 응답 정보가 존재하는 경우 10분 동안 해당 캐쉬 값을 그대로 사용하라는 의미입니다.\n  - 브라우저와 서버 사이의 컴포넌트들로부터 Shared Cache의 이점을 볼 수 있도록 기본적으로 `public` 옵션을 설정하였습니다.\n\n- `max-age` 값은 `application.yml` 파일의 `cache.max-age` 프로퍼티의 값에 해당하므로 쉽게 수정할 수 있습니다.\n\n### 3) Cache Busting 지원\n\n- 기본적으로 캐쉬 버스팅이란 캐쉬의 유효기간을 최대한 높게 설정하여 오랜기간 재활용하되, 버전이 변경되었을 때에 즉시 서버에 요청을 보내도록 하는 기법입니다.\n  - 이를 위해서는 **자원의 URI를 버전에 따라 다르게 설정**함으로써 캐쉬 저장소에 관리 중인 캐쉬를 사용하지 않도록 해야 합니다.\n- 이를 구현할 수 있도록 해당 서버에서는 캐쉬의 유효기간을 수정하는 것만이 아니라 이미지를 업로드할 때 `version` 정보를 받아 이미지의 파일명에 추가하는 기능을 구현했습니다.\n\n#### 구현 방법 예시\n\n1. 우선 `Cache-Control` 헤더의 `max-age` 값을 31536000초로 설정함으로써 1년 동안 캐쉬를 재사용하도록 합니다.\n2. 특정 이미지를 업로드할 때 `version` 정보를 명시하면 해당 파일명에는 해당 버전 정보가 추가됩니다.\n  - 예를 들어 `user/profile1`라는 파일을 저장할 때 `asdfds`라는 해쉬값을 버전으로 명시하면, 파일은 `user/profile1_asdfds.webp`와 같이 저장됩니다.\n3. 특정 클라이언트에서 `GET /images/user/profile1_asdfds.webp`로 요청을 보내는 경우, 해당 URI에 대한 응답은 1년 동안 재사용됩니다.\n4. 이때 해당 파일을 새로운 버전으로 수정해야 하는 경우, **새로운 버전의 이미지를 업로드**하고 사용자에게 이를 사용하도록 강제하도록 하면 됩니다.\n  - 예를 들어 기존과 같은 `user/profile1`라는 파일을 저장하되 `bkfefd`처럼 다른 해쉬값을 버전으로 명시하면, 파일은 `user/profile1_bkfefd.webp`와 같이 저장됩니다.\n  - 이후 기존 버전의 파일은 불필요해졌으므로 `DELETE /api/images/user/profile1_asdfds`와 같은 요청으로 제거할 수 있습니다.\n5. 이제 클라이언트에서 `GET /images/user/profile1_bkfefd.webp`로 요청을 보내도록 한다면 당연히 캐쉬 저장소에 있는 `asdfds` 버전 데이터를 사용하지 않고 서버로 요청을 보내게 됩니다.\n\n## 기타 성능 개선\n\n### 1) `.webp`\n\n- 해당 서버는 업로드되는 이미지 파일을 `.webp` 확장자로 저장하여 제공합니다.\n- 이를 통해 응답 성능 개선만이 아니라, 서버의 부담을 줄이고 하드 디스크에 더 많은 파일을 저장할 수 있도록 해줍니다.\n- 물론 업로드하려는 파일이 이미지 파일이 아닌 경우 `400 Bad Request`로 응답합니다.\n\n- cf) `webp`의 장점\n    - 무손실 압축을 통해 이미지 품질은 유지하면서도 파일 크기 자체를 감소시키기 위한 확장자.\n    - I.E.를 제외한 모든 브라우저들에서 호환되는 형식. ([참고](https://caniuse.com/webp))\n    - gif 등 모든 이미지 형식에 대응 가능.\n    \n### 2) HTTP 압축\n\n- HTTP 응답을 압축함으로써 웹 사이트의 성능을 향상시키고자 하였습니다.\n- `server.compression.enabled` 프로퍼티를 통해 Spring Boot에서 제공하는 기본 설정인 gzip 압축 알고리즘을 적용하였습니다.\n- HTTP 응답에 다음 헤더들이 추가됩니다.\n    - `Content-Encoding: gzip`\n    - `Transfer-Encoding: chunked`\n\n---\n\n## 실습 방법\n\n- 직접 해당 서버를 실행해보고 싶은 경우, 아래 두 가지 정보를 알아야 합니다.\n- 데모 내용이 궁금한 경우 [블로그](https://bugoverdose.github.io/development/image-hosting-server-toy-project/)를 참고하기 바랍니다.\n    - 주의. API 및 구현된 기능면에서 현재 서버와 다소 차이가 있습니다.\n\n### 이미지 파일 저장 경로\n\n- 디폴트 경로: `~/static/images/{uploadPath}/{fileName}`\n    - `static/images`는 `application.yml` 파일의 `image.storage.root-directory` 프로퍼티의 값에 해당함.\n    - 해당 yml 파일의 프로퍼티 값을 수정하거나, 구동 시점에 값을 주입해줌으로써 저장 경로 변경 가능.\n\n### 인증 기능\n\n- 이미지를 업로드/제거하려는 경우, 해당 API를 호출할 때 `Authorization` 헤더 값으로 서버에서 설정한 비밀 키를 추가해줘야 합니다.\n- 비밀 키는 `application.yml` 파일의 `security.authorization-key` 프로퍼티 값에 해당합니다.\n    - `prod` 프로파일로 실행하는 경우 `SECRET_KEY` 환경변수 값을 활용하게 됩니다.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbugoverdose%2Fmys3","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbugoverdose%2Fmys3","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbugoverdose%2Fmys3/lists"}