본문 바로가기
서비스 웹 서비스 개발 앱 개발 AI 알고리즘 개발 AI 컨설팅 AI 마케팅 프로젝트 인사이트 회사소개 고객게시판
클라우드 아키텍처 10 MIN READ

대용량 파일 업로드, 서버를 거치지 않아도 안전할까?

파일을 애플리케이션 서버로 모두 받으면 메모리와 네트워크가 병목이 됩니다. Presigned URL, 멀티파트·재개 업로드, 무결성 검증, 악성 파일 격리와 접근 제어까지 안전한 업로드 구조를 정리합니다.

Category
클라우드 아키텍처
Published
Read
10분
Author
Nature Fractal
대용량 파일 업로드, 서버를 거치지 않아도 안전할까? 대표 이미지

사용자가 5GB 영상을 업로드하는 기능을 만든다고 생각해 보겠습니다. 브라우저가 파일 전체를 애플리케이션 서버로 보내고, 서버가 다시 오브젝트 스토리지에 전달하면 같은 데이터가 네트워크를 두 번 지나갑니다. 업로드가 오래 걸리는 동안 서버 연결과 버퍼를 점유하고, 중간에 연결이 끊기면 처음부터 다시 시작해야 할 수도 있습니다.

그래서 대용량 업로드에서는 클라이언트가 S3, Azure Blob Storage, Google Cloud Storage 같은 오브젝트 스토리지로 직접 전송하는 구조를 자주 사용합니다. 애플리케이션 서버는 권한과 정책을 확인해 짧은 시간만 유효한 업로드 권한을 발급하고, 실제 파일 바이트는 통과시키지 않습니다.

하지만 “서버를 거치지 않는다”는 것이 “서버가 관여하지 않는다”는 뜻은 아닙니다. 파일 크기와 종류, 저장 위치, 업로드 완료 확인, 무결성, 악성 파일 검사, 공개 권한과 삭제 정책은 여전히 서버가 통제해야 합니다. 이번 글에서는 파일 한 개가 선택된 순간부터 안전하게 게시되기까지의 전체 생명주기를 살펴보겠습니다.

먼저 제어 경로와 데이터 경로를 분리한다

애플리케이션 서버가 업로드 바이트를 직접 프록시하면 인증과 검증을 한곳에서 처리하기 쉽습니다. 작은 프로필 이미지나 내부 관리 파일에는 충분할 수 있습니다. 하지만 파일이 커지거나 동시 업로드가 많아지면 서버 네트워크, 요청 시간 제한, 임시 디스크와 메모리가 병목이 됩니다.

직접 업로드 구조에서는 클라이언트가 먼저 파일 이름, 예상 크기, MIME type, 업무 대상 ID를 서버에 보냅니다. 서버는 사용자가 해당 업무에 파일을 첨부할 권한이 있는지, 크기와 할당량이 정책 안에 있는지 확인하고 upload session과 서버가 생성한 object key를 발급합니다.

그다음 클라이언트는 짧은 수명의 서명된 URL이나 임시 자격증명으로 스토리지에 파일을 전송합니다. 완료 후 서버에 세션 ID와 업로드 결과를 통보하고, 서버가 스토리지의 실제 객체 정보를 확인해 후속 처리를 시작합니다.

애플리케이션 서버는 업로드 권한만 발급하고 대용량 파일은 클라이언트에서 오브젝트 스토리지로 직접 전송하는 구조
API는 인증·정책·상태를 담당하고 대용량 데이터는 스토리지로 직접 보내면 애플리케이션 서버의 병목을 줄일 수 있습니다.

Presigned URL은 임시 권한이지 공개 URL이 아니다

Presigned URL에는 특정 객체 경로와 HTTP 메서드, 만료 시각을 포함한 서명이 들어갑니다. URL을 가진 클라이언트는 제한된 시간 동안 별도 클라우드 자격증명 없이 업로드할 수 있습니다. 만료를 짧게 하고 한 세션의 특정 object key에만 권한을 줘야 합니다.

가능하다면 Content-Length 범위, Content-Type, 체크섬과 암호화 헤더 같은 조건을 서명 정책에 포함합니다. 다만 클라우드 서비스와 서명 방식에 따라 강제할 수 있는 조건이 다르므로, 업로드 후 서버 검증을 생략해서는 안 됩니다.

서명 URL은 로그, 분석 도구, 브라우저 기록과 프록시를 통해 노출될 수 있는 bearer credential입니다. 쿼리 문자열 전체를 서버 로그에 남기지 않고 HTTPS만 사용하며, 필요 이상으로 긴 유효 시간을 주지 않습니다. 사용자가 취소하거나 권한을 잃어도 이미 발급한 URL을 즉시 취소하기 어려운 방식이라면 짧은 만료와 격리 영역이 특히 중요합니다.

객체 경로는 클라이언트가 정하지 않게 한다

사용자가 보낸 파일 이름을 그대로 object key로 사용하면 같은 이름의 파일이 덮어써지거나 ../ 같은 경로 표현, 특수문자와 Unicode 정규화 차이로 문제가 생길 수 있습니다. 서버가 UUID 같은 예측하기 어려운 식별자로 키를 만들고 원래 파일명은 메타데이터로 별도 저장합니다.

quarantine/{tenant_id}/{upload_session_id}/{object_id}

테넌트와 사용자 경계를 키 접두사와 IAM 정책에 반영하되, 키 이름만으로 권한을 보장하지 않습니다. 서버가 현재 사용자의 소유권을 확인한 뒤 정확한 경로에만 서명해야 합니다. 외부에 다운로드시킬 때는 안전하게 인코딩한 원본 이름을 Content-Disposition에 사용합니다.

큰 파일은 여러 Part로 나누고 재개할 수 있게 한다

수 GB 파일을 한 요청으로 보내면 마지막 순간의 네트워크 오류가 전체 재전송으로 이어집니다. Multipart Upload는 파일을 일정 크기의 part로 나눠 독립적으로 전송합니다. 실패한 part만 다시 보내고 여러 part를 제한된 동시성으로 병렬 업로드할 수 있습니다.

서버는 upload ID, part 크기와 만료 정책을 관리하고 클라이언트는 완료된 part 번호와 ETag 또는 체크섬을 로컬에 보존합니다. 앱이 재시작된 뒤 서버에서 이미 업로드된 part 목록을 확인하고 이어서 전송할 수 있습니다.

part를 너무 작게 하면 요청 수와 메타데이터 비용이 커지고, 너무 크게 하면 재시도 비용과 진행률 정밀도가 나빠집니다. 모바일 네트워크와 브라우저 메모리, 스토리지의 part 제한을 고려해 결정합니다. 병렬 수를 과도하게 늘리면 사용자 네트워크를 포화시키고 다른 API 요청까지 느려질 수 있습니다.

완료되지 않은 multipart session은 자동으로 사라지지 않는 경우가 있으므로 수명 주기 규칙으로 일정 기간 뒤 abort하고 임시 part 비용을 정리해야 합니다.

진행 상태를 명시적인 상태 머신으로 관리한다

파일 메타데이터 행 하나에 uploaded = true만 두면 스토리지에는 있지만 검사되지 않았거나, 처리는 끝났지만 DB 갱신이 실패한 상태를 표현하기 어렵습니다. 다음과 같은 상태를 명시적으로 관리하는 편이 좋습니다.

  • INITIATED: 권한과 정책을 확인하고 업로드 세션을 생성한 상태
  • UPLOADING: 일부 또는 전체 바이트가 임시 영역에 전송 중인 상태
  • UPLOADED: 스토리지 객체 존재와 크기·체크섬을 서버가 확인한 상태
  • SCANNING: 악성 파일과 형식을 검사하고 파생 파일을 만드는 상태
  • READY: 안전 검증이 끝나 업무 객체에 연결된 상태
  • REJECTED: 정책 또는 보안 검사에 실패해 격리된 상태
  • EXPIRED: 제한 시간 안에 완료되지 않아 정리 대상인 상태

상태 전이는 중복 완료 알림과 이벤트 재전송에도 안전해야 합니다. 같은 세션을 두 번 finalize해도 파일 레코드와 처리 작업이 중복 생성되지 않도록 조건부 갱신과 멱등성 키를 사용합니다.

파일 이름과 Content-Type을 믿지 않는다

photo.jpg라는 이름과 image/jpeg 헤더는 클라이언트가 마음대로 보낼 수 있습니다. 실행 파일의 확장자만 바꾸거나 여러 형식으로 해석되는 polyglot 파일을 업로드할 수도 있습니다. 허용 목록 기반으로 확장자, 선언된 MIME type, 파일의 magic bytes와 실제 파서 결과를 함께 검사해야 합니다.

검사 라이브러리와 이미지·문서 파서도 공격 표면입니다. 비정상적으로 큰 해상도의 이미지, 압축 해제 시 수 TB가 되는 archive bomb, 깊게 중첩된 파일과 손상된 메타데이터가 CPU와 메모리를 고갈시킬 수 있습니다. 처리 시간, 메모리, 픽셀 수, 압축 해제 크기와 내부 파일 수에 상한을 둡니다.

사용자가 올린 HTML과 SVG를 같은 도메인에서 그대로 제공하면 스크립트가 실행될 수 있습니다. 브라우저에서 해석 가능한 파일은 별도 다운로드 도메인, 강제 attachment, 올바른 Content-Type과 보안 헤더를 사용하거나 안전한 형식으로 변환합니다.

업로드 직후에는 비공개 격리 영역에 둔다

스토리지 업로드가 성공했다고 곧바로 공개하면 악성 파일 검사 전에 다른 사용자가 내려받을 수 있습니다. 모든 신규 객체는 기본적으로 비공개 quarantine 영역에 저장하고 검증이 끝난 뒤에만 READY 상태와 공개·업무 연결을 허용합니다.

업로드 파일을 격리한 뒤 무결성과 악성코드를 검사하고 파생 파일을 만들어 안전한 저장소에 게시하는 과정
파일 수신 완료와 게시 가능 상태를 분리하면 검사 중이거나 의심스러운 파일이 사용자에게 노출되는 것을 막을 수 있습니다.

악성코드 검사기는 업데이트된 시그니처와 격리된 실행 환경을 사용하고, 검사 실패와 타임아웃을 안전한 것으로 처리하지 않습니다. 의심 파일은 접근이 제한된 별도 위치에 두고 보존 기간과 보안 담당자의 검토·삭제 절차를 정합니다.

이미지는 원본을 직접 서비스하기보다 안전한 디코더로 다시 인코딩해 메타데이터를 제거하고 썸네일을 만듭니다. 동영상과 문서 변환 작업도 네트워크와 권한이 제한된 샌드박스에서 실행합니다. 변환기는 원본 버킷 전체를 읽거나 다른 사용자의 경로에 쓸 권한이 없어야 합니다.

업로드 완료를 클라이언트 말만 듣고 확정하지 않는다

클라이언트가 “완료” API를 호출했다고 실제 객체가 올바르게 저장됐다고 가정하면 안 됩니다. 서버는 자신의 권한으로 스토리지 metadata를 조회해 object key, 실제 크기, 체크섬, 암호화 상태를 세션의 예상값과 비교합니다.

스토리지 이벤트를 받아 비동기 처리를 시작할 수도 있지만 이벤트는 중복되거나 순서가 바뀌고 지연될 수 있다고 가정합니다. 세션 상태와 객체 버전을 확인해 같은 이벤트를 여러 번 처리해도 안전하게 만들고, 이벤트가 유실됐을 때 READY 이전 상태를 주기적으로 대사하는 작업을 둡니다.

체크섬은 전송 중 손상 여부를 확인하는 수단이지 악성 여부를 판단하는 보안 검사와는 다릅니다. 멀티파트 ETag가 항상 파일 전체의 단순 MD5라는 가정도 클라우드 서비스와 암호화 방식에 따라 맞지 않을 수 있으므로 공식 체크섬 기능을 사용합니다.

다운로드 권한은 업로드 권한과 별도로 검증한다

오브젝트 버킷은 기본 비공개로 두고, 다운로드할 때 서버가 현재 사용자의 테넌트와 리소스 권한을 다시 확인한 뒤 짧은 수명의 signed URL을 발급합니다. 한 번 업로드할 권한이 있었다고 영구 다운로드 권한이 생기는 것은 아닙니다.

CDN을 사용하면 캐시 키와 권한 만료를 주의해야 합니다. 사용자별 서명 값을 무시한 공용 캐시가 비공개 파일을 다른 사용자에게 제공하지 않도록 구성합니다. 권한 회수가 즉시 반영되어야 하는 파일은 URL 수명을 짧게 하고 CDN purge나 토큰 검증 방식을 준비합니다.

파일 ID를 순차 번호로 노출해도 서버 권한 검사가 올바르면 접근할 수 없어야 합니다. “URL을 추측하기 어렵다”는 것은 보조 방어일 뿐 접근 제어가 아닙니다.

암호화와 키 관리 범위를 정한다

전송 구간은 HTTPS로 보호하고 스토리지의 서버 측 암호화를 기본으로 사용합니다. 고객별 키나 규제 요구가 있다면 KMS 기반 키 정책, 키 회전과 폐기 절차를 설계합니다. 애플리케이션이 직접 파일 전체를 암복호화하면 직접 업로드와 서버 측 처리 구조가 복잡해질 수 있습니다.

클라이언트 측 종단 간 암호화는 스토리지 운영자가 내용을 볼 수 없게 하지만 서버 측 악성코드 검사, 썸네일 생성과 검색이 불가능하거나 별도 신뢰 실행 환경이 필요합니다. 보안 목표와 기능 요구를 함께 판단해야 합니다.

백업과 복제본, 파생 파일도 원본과 같은 데이터 등급으로 관리합니다. 원본을 삭제했는데 썸네일과 변환본, CDN 캐시가 남으면 개인정보 삭제 요구를 충족하지 못할 수 있습니다.

DB 레코드와 스토리지 객체 사이의 고아를 정리한다

업로드 URL을 발급했지만 사용자가 전송하지 않을 수 있고, 파일은 올라갔지만 완료 API를 호출하지 않을 수도 있습니다. 반대로 DB에 READY 레코드가 있지만 객체가 수동 삭제되는 상황도 생깁니다. 두 시스템을 하나의 트랜잭션으로 묶기 어렵기 때문에 고아 객체와 깨진 참조를 예상해야 합니다.

quarantine 경로에 짧은 lifecycle expiration을 적용하고, 완료된 객체만 영구 영역으로 복사·이동하거나 삭제 예외 태그를 부여합니다. 주기적 대사 작업은 만료된 세션의 객체를 정리하고 READY 메타데이터가 가리키는 객체의 존재를 표본 검사합니다.

삭제 요청도 DB 행만 지우지 말고 원본, 모든 파생본과 CDN 무효화를 추적합니다. 즉시 물리 삭제가 어렵다면 삭제 예정 상태와 재시도 가능한 작업을 두고 완료를 감사 로그에 남깁니다.

파일 중복 제거에는 개인정보 위험이 있다

같은 체크섬의 파일을 하나만 저장하면 비용을 줄일 수 있지만, 서로 다른 사용자의 파일 존재 여부가 응답 시간이나 결과로 노출될 수 있습니다. 한 사용자가 다른 사용자가 이미 같은 기밀 문서를 업로드했는지 추측하는 부채널이 될 수 있습니다.

중복 제거는 같은 테넌트나 동일한 권한 경계 안에서만 적용하고, 체크섬 일치가 곧 소유권을 의미하지 않게 합니다. 참조 카운트와 삭제도 조심해야 합니다. 한 사용자의 삭제가 다른 사용자의 논리 파일을 제거해서는 안 됩니다.

사용자 경험에는 진행률·재개·취소가 필요하다

대용량 업로드에서 진행률은 전송 완료와 처리 완료를 구분해 보여줘야 합니다. 바이트 업로드가 100%여도 서버 검증과 변환이 남아 있다면 “검사 중” 상태를 표시합니다. 브라우저를 닫아도 상태 조회로 이어갈 수 있어야 합니다.

네트워크가 바뀌거나 앱이 재시작되면 완료된 part를 확인해 재개하고, 만료된 서명만 새로 발급받습니다. 취소 시에는 클라이언트 전송을 중단하는 것뿐 아니라 서버 세션을 취소하고 multipart upload와 임시 객체를 정리합니다.

정책 위반은 업로드가 끝난 뒤에만 알리지 않도록 파일 선택 시 크기와 형식을 먼저 검사합니다. 다만 클라이언트 검사는 편의를 위한 것이며 서버와 후처리 검증이 최종 기준입니다.

비용과 남용을 제어한다

직접 업로드는 애플리케이션 서버 비용을 줄이지만 스토리지, 요청 수, 멀티파트 미완료 데이터, 스캔과 변환, 외부 전송 비용은 남습니다. 사용자·테넌트별 총 저장량, 일일 업로드 바이트, 동시 세션과 파일 수를 제한합니다.

서명 URL 발급 API에 rate limit을 두고 실제 완료율을 관측합니다. URL만 대량 발급해 스토리지를 채우거나 스캔 작업을 폭주시킬 수 있으므로 완료되지 않은 세션도 할당량에 잠시 포함할 수 있습니다.

작은 압축 파일이 해제 후 큰 비용을 만드는 경우처럼 업로드 바이트만으로 후처리 비용을 예측할 수 없습니다. 미디어 길이, 해상도, 페이지 수와 압축 해제 상한을 업무 정책에 포함합니다.

운영에서 봐야 할 지표

  • 세션 생성, 업로드 시작·완료·포기 비율
  • 파일 크기와 전체 업로드 시간, part 재시도 분포
  • INITIATED와 UPLOADING 상태에 머문 시간
  • 체크섬·크기·형식 검증 실패율
  • 악성코드 검사 대기 시간, 실패와 격리 건수
  • 변환 작업 처리 시간, 큐 적체와 실패율
  • 고아 객체 수, 미완료 multipart 용량과 정리 결과
  • 다운로드 권한 거부와 서명 URL 발급량
  • 테넌트별 저장량·전송량과 비용

upload session ID, object ID, tenant ID와 처리 job ID를 로그와 trace에 연결하면 “업로드는 성공했는데 파일이 안 보인다”는 문의를 빠르게 추적할 수 있습니다. 서명 URL 원문과 민감한 파일 이름은 로그에서 제거합니다.

반드시 테스트해야 할 실패 시나리오

  • 업로드 중 네트워크가 끊기고 앱이 재시작된다.
  • 일부 part만 성공한 뒤 서명 URL이 만료된다.
  • 같은 완료 요청과 스토리지 이벤트가 여러 번 도착한다.
  • 선언한 크기·형식과 실제 파일이 다르다.
  • DB에는 세션이 있지만 객체가 없거나 그 반대다.
  • 검사 서비스가 느려지거나 완전히 중단된다.
  • 압축 폭탄, 거대한 이미지와 손상된 파일이 들어온다.
  • 업로드 직후 사용자의 권한이 회수된다.
  • 다른 테넌트의 object ID로 다운로드를 요청한다.
  • 삭제 작업 일부만 성공하고 재시도된다.

안전한 구현 순서

  1. 파일 정책과 상태 머신을 먼저 정의합니다. 크기, 형식, 신선도, 보존과 공개 조건을 정합니다.
  2. 서버가 object key와 짧은 서명을 발급합니다. 권한·할당량과 정확한 경로를 검증합니다.
  3. 직접 업로드와 무결성 확인을 구현합니다. 큰 파일에는 multipart와 재개 기능을 추가합니다.
  4. 격리 후 비동기 검사 파이프라인을 만듭니다. READY 전에는 다운로드와 업무 연결을 막습니다.
  5. 다운로드 권한을 별도로 검증합니다. 비공개 저장소와 짧은 signed URL을 사용합니다.
  6. 고아 정리와 멱등성을 추가합니다. 만료 세션, 중복 이벤트와 부분 삭제를 복구합니다.
  7. 진행률과 운영 지표를 연결합니다. 사용자 상태와 내부 처리 상태가 같은 사실을 보여주게 합니다.

안전한 업로드는 전송이 아니라 생명주기 설계다

대용량 파일을 오브젝트 스토리지로 직접 보내면 애플리케이션 서버의 병목을 크게 줄일 수 있습니다. 하지만 파일을 받은 순간과 사용자에게 제공할 수 있는 순간은 같지 않습니다. 권한이 제한된 임시 업로드, 서버의 완료 검증, 격리 검사와 명시적인 게시 단계를 거쳐야 합니다.

핵심을 요약하면 이렇습니다. 서버는 바이트 대신 권한과 상태를 통제하고, 스토리지는 기본 비공개로 두며, 모든 파일을 의심한 채 검증하고, 실패와 미완료 객체를 자동으로 정리하라. 이 원칙을 지키면 파일 크기와 사용자가 늘어나도 확장성과 보안을 함께 유지할 수 있습니다.

#파일업로드 #대용량파일 #PresignedURL #MultipartUpload #ObjectStorage #클라우드스토리지 #파일보안 #악성코드검사 #AWS #Azure #GCP #백엔드개발 #클라우드아키텍처

AWS Azure GCP

/ 다음 단계

이런 문제, 직접 겪고 계신가요?

글로 정리한 방법을 실제 환경에 맞춰 적용하는 일까지 함께 하겠습니다.

영업일 기준 3일 이내 회신 · 상담 및 범위 견적 무료