Skip to content

presigned 발급에 contentLength 를 받아 서명에 묶고 5MB 상한 강제 (과도기 선택 필드) - #1053

Merged
m-a-king merged 6 commits into
devfrom
feat/1047-presign-content-length
Sep 7, 2026
Merged

presigned 발급에 contentLength 를 받아 서명에 묶고 5MB 상한 강제 (과도기 선택 필드)#1053
m-a-king merged 6 commits into
devfrom
feat/1047-presign-content-length

Conversation

@m-a-king

@m-a-king m-a-king commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Situation

  • 이미지 등록(위시·토너먼트·프로필)은 클라가 presigned URL 로 S3 에 직접 PUT 한다. 발급 시 Content-Type 은 서명에 묶여 S3 가 강제하지만 Content-Length 는 서명에 없었다.
  • multipart 시절의 5MB 제한(spring.servlet.multipart.max-file-size)은 S3 직행 경로에 걸리지 않는다. 서명 유효 시간 안이면 어떤 크기의 객체든 올라갔고, 용량 방어선은 클라 검사뿐이었다.
  • 클라 쪽에서 "용량 5MB 와 확장자만 검사하면 되는가" 를 묻는 과정에서 이 공백을 확인했다.

Task

  • 발급 요청에 파일 바이트 수를 받아 서명에 묶고, 서버가 5MB 상한을 강제한다.
  • 결정 포인트 둘: 용량을 어디서 막을지(서명 vs confirm 의 HEAD), 그리고 아직 바이트 수를 보내지 않는 구버전 클라를 어떻게 받아 줄지.

Action

결정 1: 서명에 묶는다

방식 업로드 자체를 막나 클라 변경 비고
presigned PUT 서명에 Content-Length (채택) 막는다 발급 요청에 필드 하나 추가 서명은 정확한 값만 받으므로 상한 검사는 서버가 발급 전에 한다
presigned POST policy 의 content-length-range 막는다 업로드 방식이 multipart form POST 로 통째로 바뀜 범위 조건은 가능하지만 클라 비용이 크다
confirm 시 HeadObject 크기 검사 못 막는다 (사후 삭제) 없음 5분 창 안의 업로드와 저장 비용은 그대로 든다

결정 2: 과도기 호환, 이후 필수로 전환

방식 구버전 앱 강제 시점 비고
크기 없으면 크기 없이 발급 (채택) 깨지지 않는다 클라 전환 뒤 미지정을 400 으로 막는 후속 PR 과도기엔 구버전 업로드에 상한이 안 걸리는 것을 감수
즉시 필수 (breaking) 400 지금 클라와 배포 순서를 정확히 맞춰야 한다
새 경로 v3 깨지지 않는다 v2 폐기 시 같은 흐름이 두 벌이 되어 유지비만 는다
  • 요청 계약: 위시·토너먼트는 images: [{contentType, contentLength}] 를 추가하고, 구버전 contentTypes 목록은 deprecated 로 남겨 images 가 없을 때만 읽는다. 프로필은 contentLength 필드를 추가했다. 두 배열을 인덱스로 맞추는 형태보다 한 장의 정보를 묶는 쪽이 명확하다.
  • 크기 규칙
contentLength 동작 code
생략 (또는 구버전 contentTypes) 크기 없이 발급, 서명에 Content-Length 없음 200
1 이상 5MB 이하 검증 후 서명에 묶음 200
5MB 초과 거절 400 UPLOAD-003
0 이하 거절 400 UPLOAD-004
  • 크기 검증의 자리: 상품·프로필 공통이라 형식 정책(ProductImage·ProfileImageFile)과 분리해 UploadSize 값 객체로 두었다. 미지정을 통과시키는 ofOrNull 이 과도기 코드이며, 클라 전환이 끝나면 이것만 지우면 미지정이 400 이 된다.
  • 서명: S3ImageStorage.presignUpload 가 값이 있을 때 PutObjectRequest.contentLength 를 넣어 서명한다. Content-Type·Content-Length 둘 다 signed header 가 되어 다른 값의 PUT 은 S3 가 403 으로 거절한다.
  • confirm 은 그대로: 서명이 크기를 강제하므로 confirm 에서 HEAD 로 크기를 다시 보지 않는다.
  • 문서: 발급 엔드포인트 3곳의 OpenAPI 설명·400 사유·예시와 에러 코드 카탈로그에 새 code 와 과도기 동작을 반영했다.

Result

  • 통합 테스트는 발급 계약(400 code), "거부되면 서명도 발급되지 않는다", "선언한 바이트 수가 그대로 서명으로 넘어간다", "생략·구버전 형식은 크기 없이 발급된다" 를 stub 기록으로 고정한다. 경계값(0·음수·상한·상한+1) 망라는 UploadSizeTest 가 단위로 담당한다.
  • 클라 후속(별도): 발급 요청에 contentLength 를 실어 보낸다. 배포 순서 제약은 없다.
  • 서버 후속(별도): 클라 전환이 끝나면 contentTypes 필드와 UploadSize.ofOrNull 을 지워 미지정을 400 으로 막는다. 과도기 호환 테스트 3건은 그때 400 단언으로 뒤집는다.
  • 남는 것: Content-Type 은 헤더일 뿐이라 5MB 이하의 비이미지 바이트는 서명을 통과한다. 프로필은 확정 시 매직 바이트로, 상품 이미지는 extractor 디코딩으로 걸러지는 현 구조를 유지한다.

연관 이슈

https://claude.ai/code/session_01JmUdSz2GqQ2KWMtR9TJ5tY

- Content-Type 은 이미 서명에 묶여 S3 가 강제하지만 Content-Length 는 없어, multipart 시절의 5MB 제한이 S3 직행 PUT 경로에는 걸리지 않았다. 서명은 정확한 값만 받으므로 클라가 발급 요청에 바이트 수를 보내고 서버가 상한을 검사한 뒤 서명에 박는 방식을 택했다 (범위 조건은 presigned POST policy 에만 있어 클라 업로드 방식이 통째로 바뀐다)
- 위시·토너먼트 요청은 contentTypes 목록을 images: [{contentType, contentLength}] 로 교체하고, 프로필은 contentLength 필드를 추가. 두 배열을 인덱스로 맞추는 형태보다 한 장의 정보를 묶는 쪽이 명확하다
- 크기 검증은 상품·프로필 공통이라 형식 정책(ProductImage·ProfileImageFile)과 분리해 UploadSize 값 객체로 두고, 미지정·0 이하(UPLOAD-004)와 상한 초과(UPLOAD-003)를 나눠 클라가 취할 행동이 갈리게 했다
- confirm 이후 크기 검사는 두지 않는다. 서명에 묶인 값과 다른 PUT 은 S3 가 거절하므로 HEAD 로 다시 볼 이유가 없다
- 클라 후속(별도): 발급 요청에 contentLength 를 실어 보내도록 변경

Claude-Session: https://claude.ai/code/session_01JmUdSz2GqQ2KWMtR9TJ5tY
@m-a-king m-a-king added the feat 외부 가시적 새 기능 label Sep 6, 2026
@m-a-king m-a-king self-assigned this Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Team

Run ID: d8a9edc9-0c00-4987-a004-77582626dd35


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

- 직전 커밋은 contentLength 를 필수로 두어 기존 클라 요청이 즉시 400 이 됐다. 구버전 앱을 깨지 않으려면 새 경로(v3)를 파거나 과도기 호환을 두는 두 길이 있었는데, 같은 흐름을 두 벌로 유지하는 비용이 커서 호환 쪽을 택했다
- contentLength 가 없으면 크기 없이 발급한다(서명에 Content-Length 없음). 있으면 종전대로 검증해 서명에 묶는다. UPLOAD-004 는 미지정이 아니라 0 이하에만 붙는다
- 위시·토너먼트 요청은 구버전 contentTypes 목록을 deprecated 로 남겨 images 가 없을 때만 읽는다
- 과도기 코드는 UploadSize.ofOrNull 과 contentTypes 필드 둘이다. 클라 전환이 끝나면 이 둘을 지워 미지정을 400 으로 막는다. 호환 테스트 3건은 그때 400 단언으로 뒤집는다

Claude-Session: https://claude.ai/code/session_01JmUdSz2GqQ2KWMtR9TJ5tY
@m-a-king m-a-king changed the title presigned 발급에 contentLength 를 받아 서명에 묶고 5MB 상한 강제 presigned 발급에 contentLength 를 받아 서명에 묶고 5MB 상한 강제 (과도기 선택 필드) Sep 6, 2026
m-a-king and others added 4 commits September 7, 2026 01:56
- 코드로 읽히는 설명(서명 동작·검증 흐름 재진술)을 지우고, 과도기 코드의 제거 지점과 선언값을 그대로 서명하는 이유만 남긴다

Claude-Session: https://claude.ai/code/session_01JmUdSz2GqQ2KWMtR9TJ5tY
- 과도기 코드 제거 지점과 선언값을 그대로 서명하는 이유, 두 줄만 남긴다

Claude-Session: https://claude.ai/code/session_01JmUdSz2GqQ2KWMtR9TJ5tY
- ImageStorage.presignUpload 의 예외 변환 설명과 ImageUploadException 의 도달 경로 설명은 구현·에러 코드로 읽힌다

Claude-Session: https://claude.ai/code/session_01JmUdSz2GqQ2KWMtR9TJ5tY
@m-a-king
m-a-king merged commit b0fcfaa into dev Sep 7, 2026
5 checks passed
@m-a-king
m-a-king deleted the feat/1047-presign-content-length branch September 7, 2026 06:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 외부 가시적 새 기능

Projects

None yet

Development

Successfully merging this pull request may close these issues.

presigned 업로드 발급에 contentLength 를 받아 서명에 묶고 5MB 상한을 강제

1 participant