이 글에서 얻는 것

  • 파일 업로드/서빙을 “백엔드가 프록시”하는 대신, Direct-to-Object Storage(S3) + CDN으로 안전하고 빠르게 설계할 수 있습니다.
  • Presigned URL, 멀티파트 업로드, 캐시 정책(Cache-Control) 등 핵심 구성 요소를 요구사항(보안/권한/비용/성능)에 맞춰 선택할 수 있습니다.
  • 운영에서 자주 터지는 문제(권한 우회, 악성 파일, 대용량 업로드 실패, 캐시 무효화 비용)를 설계 단계에서 예방할 수 있습니다.

0) 요구사항을 먼저 정리하자

파일 시스템 설계는 “업로드/다운로드” 두 가지만 있는 게 아니라 다음을 포함합니다.

  • 접근 제어: 누가 올리고/누가 볼 수 있는가(공개/비공개)
  • 대용량: 수십~수백 MB, 업로드 실패/재시도/재개(resume)
  • 변환: 이미지 리사이즈/썸네일/동영상 트랜스코딩
  • 보안: 악성 파일/피싱/권한 우회, 민감 데이터 노출 방지
  • 비용: 트래픽을 앱 서버로 통과시키지 않기(CDN/오리진 비용)

1) 기본 아키텍처: “서버를 통과하지 않는다”

업로드(Direct upload)

  1. 클라이언트가 백엔드에 “업로드 요청”
  2. 백엔드가 권한 확인 후 Presigned URL 발급
  3. 클라이언트가 S3로 직접 업로드
  4. 업로드 완료 후 메타데이터를 백엔드에 등록(또는 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}/original
  • uploads/{tenantId}/{userId}/{fileId}/thumb_256.webp

4) 메타데이터(DB): 파일의 ‘상태’를 관리한다

S3에는 파일이 있고, DB에는 “이 파일이 무엇인지”가 있습니다.

예시 컬럼:

  • file_id, owner_id, s3_key, content_type, size, checksum
  • status: UPLOADING → UPLOADED → SCANNED → AVAILABLE
  • visibility: PUBLIC/PRIVATE
  • created_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(퍼지) 비용/지연이 생기고 운영이 어려워집니다.

공개 파일과 달리 “누가 볼 수 있는지”가 중요합니다.

  • 백엔드에서 권한 체크 후 CDN Signed URL을 발급(짧은 만료)
  • 또는 Signed Cookie로 일정 시간 접근 허용(여러 파일 접근에 편함)

실무 팁:

  • 만료가 너무 길면 유출 사고가 커집니다.
  • URL 공유가 가능한 UX라면 만료/재발급 흐름을 명확히 해야 합니다.

8) 변환/썸네일: 동기 처리 금지(대부분)

업로드 요청에서 이미지 리사이즈/동영상 트랜스코딩을 동기로 하면, 요청 지연/타임아웃/리소스 고갈이 쉽게 발생합니다.

권장 흐름:

  • 업로드 완료 이벤트 → 비동기 워커 → 파생 파일 생성 → 상태 업데이트

연습(추천)

  • “업로드 요청 → presigned 발급 → S3 업로드 → 완료 콜백 → 메타데이터 저장” 흐름을 최소 구현해보기
  • 대용량 파일을 멀티파트로 올리고, 실패 파트 재시도가 동작하는지 확인해보기
  • 공개 파일은 장기 캐시(해시 파일명)로, 비공개 파일은 Signed URL로 서빙하는 정책을 나눠 설계해보기