파일 업로드 기능은 겉보기에 단순해 보입니다. 사용자가 파일을 선택하고, 서버에 전송하고, 저장하면 끝이라고 생각하기 쉽습니다. 하지만 실제 운영 환경에서는 보안 취약점, 성능 병목, 비용 폭발, 확장성 제한 등 수많은 엣지 케이스가 숨어 있습니다.
파일 업로드 하나를 제대로 설계하려면 클라이언트 검증부터 스토리지 아키텍처, 접근 제어, 이미지 최적화, 비용 관리까지 전체 파이프라인을 고려해야 합니다. 프로덕트 메이커가 다양한 프로젝트를 통해 축적한 파일 업로드 설계의 핵심 원칙을 공유합니다.
파일 크기와 타입 검증: 보안의 첫 번째 관문
클라이언트 사이드 검증은 사용자 편의를 위한 것이지, 보안 수단이 절대 아닙니다. 브라우저의 JavaScript 검증은 개발자 도구(F12)로 10초 만에 우회할 수 있습니다. 따라서 반드시 서버 사이드에서 이중으로 검증해야 합니다.
파일 확장자만 확인하는 것도 위험합니다. malware.php를 malware.php.jpg로 이름만 바꾸면 확장자 검증을 통과합니다. 백엔드에서는 파일의 실제 MIME 타입(매직 넘버)을 검사해야 합니다. 각 언어 진영마다 이를 위한 도구(Python의 python-magic, Node.js의 file-type, JVM 진영의 Apache Tika 등)가 마련되어 있습니다.
이미지 파일 안에 악성 JavaScript나 PHP 코드를 삽입하는 공격도 실존하며, 이런 파일이 실행 가능한 디렉토리에 저장되면 서버 전체가 위험해집니다. 저희는 일반적으로 기본 파일 크기 제한을 20MB 안팎으로 설정하고, 업로드 가능한 MIME 타입을 명시적 화이트리스트로 관리합니다.
대용량 파일이 필요한 경우에는 청크 업로드를 구현합니다. 100MB 이상의 동영상 파일은 5MB 단위로 분할 전송하고, 서버에서 모든 청크를 수신한 후 조합하는 방식입니다. 네트워크 불안정으로 중간에 끊겨도 이어서 업로드할 수 있어 사용자 경험이 크게 개선됩니다.
스토리지 설계의 핵심 원칙: 애플리케이션 서버에 파일 저장 금지
절대로 애플리케이션 서버의 로컬 디스크에 파일을 저장하면 안 됩니다. 이유는 명확합니다. 서버를 여러 대로 수평 확장할 때 파일 동기화가 불가능해지고, 서버 장애나 재배포 시 파일이 유실됩니다. 반드시 클라우드 오브젝트 스토리지(Google Cloud Storage, AWS S3)를 사용해야 합니다.
GCP 환경을 사용할 때는 Google Cloud Storage 를, AWS 환경에서는 S3 를, 그 외 환경에서는 호환 가능한 오브젝트 스토리지를 기본으로 선택합니다 — 클라이언트 인프라에 맞추는 것이 우선입니다. 파일은 어느 스토리지를 쓰든 날짜와 UUID 기반 경로에 저장합니다. 예를 들어 uploads/2026/03/28/a1b2c3d4-e5f6-7890-abcd-ef1234567890.webp 형태입니다.
원본 파일명은 데이터베이스의 메타데이터 테이블에 별도로 저장하되, 실제 스토리지 경로에는 사용하지 않습니다. 한글 파일명이나 공백, 특수문자로 인한 URL 인코딩 문제를 원천적으로 차단하기 위해서입니다.
서명된 URL과 접근 제어
공개 파일(상품 이미지, 배너 등)은 CDN을 통해 직접 접근하게 하되, 비공개 파일(신분증 사본, 계약서, 개인정보가 포함된 문서)은 반드시 서명된 URL(Signed URL)을 통해 시간 제한이 있는 접근 권한을 부여합니다.
서명된 URL 의 유효 시간은 보통 15분에서 1시간 정도로 설정합니다. 이렇게 하면 URL 이 외부에 유출되더라도 일정 시간이 지나면 접근이 차단되고, 다른 사이트에서 이미지 URL 을 직접 가져다 쓰는 핫링킹(Hotlinking) 도 막혀 불필요한 트래픽 비용도 함께 줄어듭니다.
이 패턴은 사용자가 도구 안에서 만든 작업 결과물을 클라우드에 저장하는 서비스에서 특히 효과적입니다. 3D 도면 작업, 디자인 PSD·AI 파일, 영상 편집 프로젝트, Adobe Creative Cloud 처럼 도구가 자동으로 사용자 작업물을 클라우드에 저장·동기화하는 환경이 대표적인 예이며, 파일 용량이 크고 사용자별로 빠르게 쌓이는 만큼 비공개 자료가 많아질수록 보안과 트래픽 비용 양쪽이 동시에 커집니다. 같은 자료를 정적 공개 URL 위에 올려두면 검색 엔진·악성 봇·외부 사이트 모두에게 노출될 수 있는데, 서명된 URL 로 바꾸면 같은 인프라 위에서 보안과 트래픽 비용을 한 번에 잡을 수 있습니다.
이미지 최적화와 비용 관리
사용자가 스마트폰으로 찍은 5MB짜리 원본 사진을 업로드하면, 프로덕트 메이커는 서버에서 자동으로 세 가지 버전을 생성합니다. 목록용 썸네일(200x200px, 약 15KB), 상세 페이지용 중간 크기(폭 800px, 약 80KB), 그리고 원본 보관용입니다.
포맷은 WebP로 자동 변환하며, JPEG 대비 25~35%의 용량 절감 효과가 있습니다. 이미지 한 장당 이 정도지만, 서비스에 이미지가 10만 장이 쌓이면 스토리지 비용 차이가 월 수만 원에 달합니다. GCS 수명 주기 정책(Lifecycle Policy)을 설정해서 업로드 후 90일이 지난 파일은 Standard에서 Nearline 스토리지 클래스로 자동 이동시키면 보관 비용을 60% 이상 줄일 수 있습니다.
365일이 지나면 Coldline으로 한 단계 더 내리는 것도 가능합니다.
가장 흔하고 치명적인 실수는 파일을 PostgreSQL 데이터베이스의 BLOB(bytea) 컬럼에 직접 저장하는 것입니다. 이미지 몇백 장만 쌓여도 데이터베이스 백업 크기가 수 GB로 폭발하고, 테이블 쿼리 성능이 눈에 띄게 저하됩니다.
파일은 오브젝트 스토리지에, 파일의 메타데이터(파일명, 크기, MIME 타입, 저장 경로)만 데이터베이스에 저장하는 것이 철칙입니다.