이 글에서 얻는 것
- 파일 업로드/서빙을 “백엔드가 프록시”하는 대신, Direct-to-Object Storage(S3) + CDN으로 안전하고 빠르게 설계할 수 있습니다.
- Presigned URL, 멀티파트 업로드, 캐시 정책(Cache-Control) 등 핵심 구성 요소를 요구사항(보안/권한/비용/성능)에 맞춰 선택할 수 있습니다.
- 운영에서 자주 터지는 문제(권한 우회, 악성 파일, 대용량 업로드 실패, 캐시 무효화 비용)를 설계 단계에서 예방할 수 있습니다.
0) 요구사항을 먼저 정리하자
파일 시스템 설계는 “업로드/다운로드” 두 가지만 있는 게 아니라 다음을 포함합니다.
- 접근 제어: 누가 올리고/누가 볼 수 있는가(공개/비공개)
- 대용량: 수십~수백 MB, 업로드 실패/재시도/재개(resume)
- 변환: 이미지 리사이즈/썸네일/동영상 트랜스코딩
- 보안: 악성 파일/피싱/권한 우회, 민감 데이터 노출 방지
- 비용: 트래픽을 앱 서버로 통과시키지 않기(CDN/오리진 비용)
1) 기본 아키텍처: “서버를 통과하지 않는다”
업로드(Direct upload)
- 클라이언트가 백엔드에 “업로드 요청”
- 백엔드가 권한 확인 후 Presigned URL 발급
- 클라이언트가 S3로 직접 업로드
- 업로드 완료 후 메타데이터를 백엔드에 등록(또는 S3 이벤트로 후처리)
서빙(Deliver via CDN)
- CloudFront/Cloudflare 같은 CDN이 S3 오리진에서 파일을 가져와 캐시
- 공개 파일이면 캐시 정책으로 성능/비용 최적화
- 비공개 파일이면 Signed URL/Signed Cookie로 접근 제어
핵심 효과:
- 앱 서버는 트래픽/대역폭 병목에서 해방됩니다.
- 업로드/다운로드는 CDN/S3가 처리하고, 앱은 “권한/메타데이터/워크플로”에 집중합니다.
2) 업로드 설계: Presigned URL vs 멀티파트
2-1) Presigned PUT(단순 업로드)
- 작은 파일/중간 크기 파일에 적합
- 만료 시간을 짧게(예: 1~10분) 주고, 업로드 대상 key를 서버가 통제합니다
2-2) 멀티파트 업로드(대용량/재개)
- 파일을 파트로 나눠 업로드하고, 실패한 파트만 재시도 가능
- 업로드 중단/재개가 쉬워 대용량에 적합
- 미완료 multipart 정리(수명/정리 작업)를 운영 정책으로 둬야 합니다
3) 파일 키(key) 설계: 경로가 곧 권한/운영 난이도다
권장:
- 사용자 입력 파일명은 그대로 key로 쓰지 않기(충돌/보안/인코딩 이슈)
- 서버가 생성한 안정적인 key 사용(예: UUID)
- 네임스페이스를 명확히(테넌트/유저/도메인)
예:
uploads/{tenantId}/{userId}/{fileId}/originaluploads/{tenantId}/{userId}/{fileId}/thumb_256.webp
4) 메타데이터(DB): 파일의 ‘상태’를 관리한다
S3에는 파일이 있고, DB에는 “이 파일이 무엇인지”가 있습니다.
예시 컬럼:
file_id,owner_id,s3_key,content_type,size,checksumstatus: UPLOADING → UPLOADED → SCANNED → AVAILABLEvisibility: PUBLIC/PRIVATEcreated_at,expire_at(선택)
이렇게 하면:
- 업로드가 끝났는지/스캔이 끝났는지/서빙 가능한지
- 권한 체크를 어디서 할지
가 명확해집니다.
5) 보안: “업로드가 제일 위험하다”
최소한 아래는 반드시 고려합니다.
- Content-Type/크기 제한(클라이언트 주장만 믿지 말고 서버/스캔 단계에서 확인)
- CORS 정책 최소화(허용 오리진/메서드/헤더 제한)
- Presigned URL 만료 짧게 + 한 번 쓰고 끝내는 흐름(업로드 완료 후 상태 전환)
- 악성 파일 스캔(버킷 이벤트 → 워커/Lambda → 결과에 따라 격리)
- 비공개 파일은 public ACL 금지, “권한 있는 경우에만” Signed URL 발급
6) CDN 캐시 전략: “무효화”보다 “버전”이 싸다
정적 파일은 캐시가 핵심입니다.
- 파일명을 콘텐츠 해시 기반으로 만들면(immutable)
Cache-Control: public, max-age=31536000, immutable같은 강한 캐시가 가능합니다. - 파일이 바뀌면 파일명이 바뀌므로 “캐시 무효화”를 거의 하지 않아도 됩니다.
반대로, 파일명이 고정인데 내용이 바뀌면:
- CDN invalidation(퍼지) 비용/지연이 생기고 운영이 어려워집니다.
7) 비공개 파일 서빙: Signed URL / Signed Cookie
공개 파일과 달리 “누가 볼 수 있는지”가 중요합니다.
- 백엔드에서 권한 체크 후 CDN Signed URL을 발급(짧은 만료)
- 또는 Signed Cookie로 일정 시간 접근 허용(여러 파일 접근에 편함)
실무 팁:
- 만료가 너무 길면 유출 사고가 커집니다.
- URL 공유가 가능한 UX라면 만료/재발급 흐름을 명확히 해야 합니다.
8) 변환/썸네일: 동기 처리 금지(대부분)
업로드 요청에서 이미지 리사이즈/동영상 트랜스코딩을 동기로 하면, 요청 지연/타임아웃/리소스 고갈이 쉽게 발생합니다.
권장 흐름:
- 업로드 완료 이벤트 → 비동기 워커 → 파생 파일 생성 → 상태 업데이트
연습(추천)
- “업로드 요청 → presigned 발급 → S3 업로드 → 완료 콜백 → 메타데이터 저장” 흐름을 최소 구현해보기
- 대용량 파일을 멀티파트로 올리고, 실패 파트 재시도가 동작하는지 확인해보기
- 공개 파일은 장기 캐시(해시 파일명)로, 비공개 파일은 Signed URL로 서빙하는 정책을 나눠 설계해보기
💬 댓글