"로컬 파일 시스템에서 S3로"
11년차 QA 엔지니어가 처음 파일 업로드 기능을 구현하면서 겪은 환경별 차이와 최적화 과정을 공유하려고 합니다. "그냥 파일 저장하면 되는 거 아닌가?"라고 생각했던 제가 얼마나 순진했는지... 지금 생각해보면 웃음만 나옵니다.
제임스컴퍼니 프로젝트에서 사용자 프로필 이미지 업로드 기능을 만들어야 했습니다. QA 엔지니어로 일하면서 수많은 파일 업로드 버그를 찾아냈던 저는 자신만만했죠.
"내가 그동안 찾았던 버그들만 피하면 완벽한 파일 업로드를 만들 수 있어!"
FastAPI 공식 문서를 열고, 파일 업로드 예제를 찾았습니다. 생각보다 간단하더라고요.
터미널에서 curl로 테스트해보니 잘 동작했습니다. 파일이 uploads 폴더에 저장되고, 프론트엔드에서도 이미지가 잘 보였습니다. "와, 개발 쉽네? 파일 업로드 구현 완료~" 하고 커밋하려던 순간...
QA 엔지니어의 본능이 발동했습니다. 잠깐, 내가 지금까지 찾았던 파일 업로드 버그들이 뭐였더라?
파일명 중복: "profile.jpg"를 올린 사용자가 100명이면?
경로 조작: "../../../etc/passwd" 같은 파일명으로 시스템 파일에 접근?
파일 크기: 10GB짜리 파일을 올려서 서버 디스크를 꽉 채운다면?
파일 타입: .exe, .sh 같은 실행 파일을 올린다면?
"아, 맞다. 내가 이런 버그들을 얼마나 많이 리포팅했는데..." 부끄러워지면서 코드를 다시 작성했습니다.
훨씬 안전해졌지만, 코드도 복잡해졌습니다. 그래도 "이제 내가 찾았던 버그들은 안 나오겠지?" 하는 뿌듯함이 있었죠.
로컬에서 완벽하게 동작하던 파일 업로드 기능을 개발 환경(Vercel)에 배포했습니다. 처음엔 잘 되는 것 같았는데...
[2024-03-15 14:23:45] 파일 업로드 성공: profile_abc123.jpg
[2024-03-15 14:24:12] GET /uploads/profile_abc123.jpg → 200 OK
[2024-03-15 14:35:28] GET /uploads/profile_abc123.jpg → 404 Not Found
"뭐야, 파일이 어디로 간 거야?"
처음엔 코드 버그인 줄 알고 한참을 디버깅했습니다. 로그도 추가하고, 파일 시스템을 직접 확인하는 API도 만들어봤죠.
충격적인 사실을 발견했습니다. Vercel의 Serverless 환경에서는,
파일 시스템이 읽기 전용 (Read-only)
/tmp 폴더만 쓰기 가능하지만 함수 실행이 끝나면 삭제됨.
각 함수 실행은 독립된 환경 (상태 공유 불가)
"아... 이래서 다들 S3를 쓰는구나." 드디어 깨달았습니다.
S3를 도입하기로 결정했지만, 또 다른 고민이 시작되었습니다. 환경을 어떻게 분리할 것인가?
처음엔 단순하게 하나의 버킷에 폴더로 구분하려 했습니다.
jamescompany-uploads/
├── local/
├── development/
└── production/
하지만 AWS 콘솔을 만지작거리다가 깨달았습니다.
IAM 권한을 폴더별로 세밀하게 제어하기 복잡함
비용 추적이 어려움 (개발 환경에서 실수로 대용량 파일을 올렸을 때)
실수로 production 폴더를 건드릴 위험
결국 버킷 자체를 분리하기로 했습니다.
환경별로 다른 저장소를 사용하다 보니, 비즈니스 로직과 저장소 구현을 분리해야 했습니다. 객체지향 설계 원칙이 이럴 때 쓰는 거구나!
S3 직접 URL은 서울 리전에서는 빠르지만, 해외 사용자들에게는 느릴 수 있습니다. QA할 때 VPN으로 다양한 지역에서 테스트해보니 확실히 차이가 났죠. Cloudflare CDN을 연동하기로 했습니다.
처음엔 "그냥 도메인 연결하면 끝 아닌가?" 했는데, 실제로는 훨씬 복잡했습니다.
S3 버킷 정책 설정: CloudFlare IP만 접근 허용
캐시 규칙 설정: 이미지는 오래, 문서는 짧게
캐시 무효화: 같은 파일명 업데이트 시 처리
CDN 설정 후 또 다른 문제가 발생했습니다. 사용자가 프로필 이미지를 변경했는데도 계속 이전 이미지가 보이는 거예요. 브라우저 캐시인 줄 알고 강제 새로고침을 해도 마찬가지... CDN 캐시였습니다.
QA하면서 실제 사용 패턴을 분석해보니 충격적인 사실을 발견했습니다.
사용자의 30%가 10MB 이상의 원본 사진을 그대로 업로드
스마트폰 카메라 기본 설정이 4000x3000 픽셀 이상
프로필 이미지 표시 크기는 겨우 200x200 픽셀
"이건 낭비가 너무 심한데?" S3 스토리지 비용과 CDN 전송 비용을 계산해보니 이미지 최적화가 필수였습니다.
파일 URL이 공개되면서 새로운 보안 문제들이 나타났습니다.
Hotlinking: 다른 사이트에서 우리 이미지를 직접 링크
CORS: 프론트엔드에서 파일 접근 시 CORS 에러
무단 다운로드: 봇이나 크롤러의 대량 다운로드
CORS 설정도 환경별로 달라야 했습니다.
백엔드가 준비됐으니 이제 프론트엔드 차례입니다. 파일 업로드 UX는 생각보다 신경 쓸 게 많았습니다.
드래그 앤 드롭 UI도 추가했습니다.
QA 엔지니어로서 각 환경별로 체계적인 테스트 시나리오를 작성했습니다. 11년차 경험을 살려서 정말 꼼꼼하게 작성했죠.
혼자 개발하면서 테스트하는 중인데도 AWS 청구서를 보고 깜짝 놀랐습니다. "아니, 나 혼자 테스트하는데 이게 왜 이렇게 나와?"
분석해보니,
개발하면서 테스트용으로 올렸다 지웠다 한 파일들이 S3에 계속 쌓임
이미지 최적화 테스트하면서 원본과 여러 버전의 썸네일들이 중복 저장
CloudFront 설정 실수로 캐시가 안 되고 매번 Origin에서 가져옴
잘못된 코드로 인한 무한 업로드 루프 (한 번 실수로 밤새 같은 파일을 수천 번 업로드...)
"아직 서비스 오픈도 안 했는데 이러면 나중에 어떻게 되는 거야?"
급하게 비용 최적화 전략을 세웠습니다.
그리고 개발할 때 실수를 방지하기 위한 안전장치도 추가했습니다.
심지어 개발 중 실수로 무한 루프를 만들어버린 적도 있었습니다.
그래서 프론트엔드에도 안전장치를 추가하였습니다.
이런 실수들을 통해 배운 교훈!!
개발 환경도 비용이 든다: 혼자 테스트하는데도 잘못하면 큰 비용 발생
자동 정리는 필수: 특히 개발 환경에서는 공격적으로 정리
제한과 모니터링: 무한 루프나 실수를 방지하는 안전장치 필요
비용 알림 설정: 예상치 못한 비용 발생을 조기에 감지
아직 서비스 오픈 전인데도 이런 최적화가 필요하다니... 실제 사용자가 들어오면 얼마나 더 복잡해질지 벌써부터 걱정되면서도 기대됩니다!
코드로 처리하는 것도 좋지만, AWS의 기본 기능을 활용하는 것이 더 효율적일 때가 있습니다.
파일 업로드 후 바로 썸네일을 생성하는 기능도 추가했습니다. 처음엔 동기적으로 처리했는데, 대용량 이미지에서 타임아웃이 발생하더라고요.
https://gist.github.com/jamescompany/fedbf3231239346509815f05f6e27281
10MB 이상의 파일을 업로드할 때는 멀티파트 업로드를 사용하여 안정성을 높였습니다.
프론트엔드에서도 청크 단위로 업로드하도록 수정하였습니다.
11년차 QA 엔지니어가 직접 파일 업로드를 구현하면서 깨달은 점들..
로컬: 단순한 파일 시스템
개발: Serverless + S3의 제약사항들
운영: 성능, 비용, 보안의 균형
각 환경마다 완전히 다른 접근이 필요했고, 이를 추상화하는 것이 핵심이었습니다.
파일명 검증 (경로 조작 방지)
MIME 타입 검증 (Magic number 확인)
크기 제한 (DDoS 방지)
접근 제어 (Signed URL, CORS)
QA/Testing할 때 찾았던 보안 버그들이 왜 발생했는지 이제야 이해됩니다.
이미지 리사이징으로 스토리지 80% 절감
CDN 캐싱으로 전송 비용 60% 절감
라이프사이클 정책으로 장기 보관 비용 90% 절감
매달 나가는 AWS 청구서를 보면서 최적화의 중요성을 뼈저리게 느꼈습니다.
업로드 진행률 표시
드래그 앤 드롭
미리보기
에러 메시지의 명확성
"파일 업로드 실패"가 아닌 "10MB를 초과하는 파일은 업로드할 수 없습니다"라는 구체적인 메시지의 중요성.
어떤 파일이 얼마나 업로드되는지
실패율과 원인 분석
비정상적인 사용 패턴 감지
개발자가 되어보니 로그 없이는 문제를 찾을 수도, 해결할 수도 없다는 걸 알게 되었습니다.
"파일 업로드? 그냥 <input type="file">이면 끝 아니야?"라고 생각했던 제가, 이제는 환경별 스토리지 전략, CDN 최적화, 보안 정책, 비용 관리까지 고려하는 개발자가 되었습니다.
무엇보다 중요한 건, QA 엔지니어로서의 경험이 더 견고한 시스템을 만드는 데 큰 도움이 되었다는 점입니다. "이런 케이스는 어떻게 처리하지?"라는 질문을 끊임없이 던지면서 개발했더니, 운영 환경에서 발생하는 이슈가 현저히 줄었습니다.
파일 업로드 하나만으로도 이렇게 배울 게 많다니... 개발의 세계는 정말 깊고도 넓네요.
다음 편에서는 이 모든 것을 모니터링하고 에러를 추적하는 Sentry 도입기를 공유하겠습니다. "에러가 발생했습니다"라는 막연한 메시지에서 "사용자 A가 iOS Safari에서 15MB 파일 업로드 시 타임아웃 발생"이라는 구체적인 정보를 얻기까지의 여정, 기대해 주세요!