
서론
파일을 업로드할 때, 서버를 거쳐서 업로드하고 있거나 413 Request Entity Too Large를 만난 적이 있다면 한번 읽어보시면 좋을 것 같습니다.
프로젝트 소개
그 전에 어떤 프로젝트인지, 가볍게 소개하고 넘어가면 좋을 것 같습니다.

히어릿은 IT 팟캐스트 앱으로, 가볍게 CS/IT트렌드/프레임워크 등등 정보를 팟캐스트로 들을 수 있습니다.
원하는 팟캐스트를 편하게 찾을 수 있도록 추천 숏폼도 제공합니다.
(관심 있으신 분들은 다운 받아보세요. 개발자를 위한 앱이랍니다..!!)
Google PlayStore
iOS AppStore
문제
팟캐스트 서비스이므로 팟캐스트를 업로드하는 관리자 페이지가 있습니다.
초기 구조는 평범한 API처럼 Nginx(리버스 프록시 서버) -> 애플리케이션 서버를 거쳐 S3에 업로드하는 방식을 사용했습니다.
여기서 2가지의 문제가 발생했습니다.
1. 설정 관리 값의 한계와 보안 취약점
대용량 파일을 업로드할 때 413 Request Entity Too Large 에러가 발생하여 Nginx의 client_max_body_size를 기본 설정 값인 1MB에서 30MB로 또 100MB로 지속적으로 상향 조정해야했습니다.
Request body의 허용 크기를 올릴수록, 대용량 데이터 전송을 통한 DDoS 공격이나 서버 리소스 고갈 위험이 발생합니다. 주 20회 미만의 업로드 때문에 전체 서비스의 보안 수준을 낮춰야 했습니다.
2. Disk Spooling으로 인한 성능 저하
아래는 Nginx AccessLog의 일부입니다.
{
"req_time":10.630,
"upstream_time":1.019,
}
업로드 요청 전체 소요시간 10.6초 중 애플리케이션 처리 시간(upstream_time)은 1초에 불과했습니다.
즉, Nginx 처리시간 (req_time - upstream_tim)이 9초 이상 소요되었습니다.
Nginx 로그 분석 결과, 클라이언트로부터 받은 request body(파일들)를 메모리로 처리하지 못해 디스크(EBS)로 스풀링(buffering)했다는 경고 로그 발생하였습니다.
[warn]: *7341 a client request body is buffered to a temporary file /var/lib/nginx/body/0000000026, server: hearit.o-r.kr, request: "POST /api/v1/admin/hearits HTTP/2.0", host: "hearit.o-r.kr", referrer: "https://hearit.o-r.kr/admin/hearits-create"
Nginx가 메모리 버퍼 (client_body_buffer_size) 를 초과하는 요청을 처리하기 위해 디스크 I/O를 수행하는 스풀링 작업에서 큰 병목이 발생했습니다.
버퍼 사이즈를 늘리는 것은 결국 메모리 부족이 발생하므로 근본적인 해결책이 될 수 없었습니다.
의사결정 과정
서버 설정을 변경하지 않고, 업로드 확장성을 가지기 위해 3가지 해결책을 생각했습니다.
생각한 해결책들
1. 별도의 업로드 전용 서버 구축.
파일 처리를 담당하는 별도의 인스턴스를 증설하여 메인 API 서버와 트래픽을 완전히 분리합니다.
- 장점: 메인 서버의 리소스를 완전히 보호할 수 있으며, 업로드 실패 시 롤백 등 트랜잭션 관리가 용이합니다.
- 단점 : 주 20회 미만의 낮은 업로드 이벤트를 위해 별도의 인프라를 유지보수하는 것은 과하고, 추가적인 비용이 발생합니다.
- 결론 : 탈락
2. 청크 단위 분할 업로드
기존 경로 nginx > 애플리케이션 서버 > S3, DB를 유지하되, 파일을 잘게 쪼개 chunk 업로드하는 방식입니다.
- 장점: Nginx의 BodySize, 메모리 버퍼 사이즈 제한 문제를 해결할 수 있으며, 애플리케이션 레벨에서 대용량 파일 제어가 가능합니다.
- 단점: 업로드 파일이 Nginx와 애플리케이션 서버를 거친다는 본질적인 구조는 그대로입니다. 파일이 서버의 네트워크 대역폭과 스레드를 점유하므로 여전히 무거운 작업을 애플리케이션 서버에서 분리하지 못했습니다.
- 결론 : 탈락
3. Presigned URL을 이용한 S3 Direct Upload
서버를 거치지 않고, 클라이언츠가 AWS에 S3에 직접 파일을 업로드하도록 인증된 URL을 발급하는 방식입니다.
- 장점
- 병목 완전 제거 : 파일 데이터가 Nginx와 애플리케이션 서버를 전혀 통과하지 않으므로 Disk Spooling 문제를 완전히 해결합니다.
- 리소스 절약 : 애플리케이션 서버는 가벼운 메타데이터(json)만 처리하므로 부하가 없습니다.
- 유지보수 & 비용 : 추가 인프라 비용이 0에 가깝고, 간단한 방식으로 쉽게 유지보수할 수 있습니다.
- 단점
- 데이터 정합성 관리가 복잡합니다. 클라이언트단에서 업로드 후 서버에 알리는 구조이므로, 파일 업로드 중단 시 S3에 파일이 남고 DB에는 기록이 없는 고아객체가 발생할 수 있습니다.
- 결론 : 선택
배치 작업을 통해 DB와 S3의 싱크를 맞추는 방식으로 단점을 해결하기로 하고, 비용과 유지보수 관점에서 가장 효율적이고 애플리케이션 서버의 리소스와 보안을 지킬 수 있는 3번째 방식을 선택하였습니다.
행동
아래는 기존 업로드 구조와 개선한 업로드 구조 입니다.
AS-IS

TO-BE

흐름을 설명하자면
- 파일들에 대한 S3 presigned URL을 발급합니다.
- 관리자 페이지에서 S3로 바로 3개를 동시에 업로드합니다.
- 메타데이터정보를 애플리케이션 서버로 전송하여 DB에 저장합니다.
- 만약 메타데이터 저장이 실패하면 트랜잭션 롤백을 수행하고, 이 때 s3에 요청을 보내서 해당 파일들을 전부 삭제합니다.
+ 매일 새벽 4시에 AWS 람다로 메타데이터가 존재하지 않는 S3 파일들 삭제 작업을 수행합니다. 3개의 파일을 저장하다가 중간에 실패하는 경우, 메타데이터 저장에 실패해서 파일 삭제 요청을 했는데 이 요청이 실패한 경우 2가지에 대한 데이터정합성을 맞추기 위한 작업입니다.
실제로 코드 흐름 (sequence diagram) 입니다.

코드를 보고 싶으신 분들이 있다면 아래 PR을 참고해주세요!
https://github.com/woowacourse-teams/2025-hEARit/pull/758
[BE] refactor: 관리자 페이지 업로드 로직과 UI 개선 #745 by gabean13 · Pull Request #758 · woowacourse-teams/2025
📌 변경 내용 & 이유 업로드 로직 문제점 업로드 전체 응답 시간의 리버스 프록시 서버의 스풀링에 90% 이상 소요. 기존의 업로드 작업은 리버시프록시 서버에서 파일을 디스크로 스풀링하는 과
github.com
결과
가장 큰 문제였던 Nginx Spooling 병목을 완전히 제거하여 기존 업로드 시간 10s에서 800ms로 개선하였습니다. 또한 불필요한 서버 리소스 낭비와 보안 위협(BodySize 제한 해제, OutOfMemory 위험)를 해결하였습니다.
데이터 정합성 문제는 애플리케이션 로직으로 제어하였고, 인프라 증설이 없는 비용과 유지보수 효율적인 구조라는 점에서 좋은 해결책이었다고 생각합니다.
느낀 점
이런 문제들을 해결하다 보면, "왜 처음에 이렇게 안했을까?" 라는 자책을 종종하게 됩니다. 하지만 돌이켜 보면, 당시에 다른 기능 구현들을 하는 시간에 짬을 내서 관리자 페이지 업로드를 빠르게 구현하기 위해 고민을 하고 최선의 선택을 한 것이었습니다. 그때는 그게 최선이었습니다. 지금의 방식도 나중에 서비스가 커지면 또 다른 문제가 발생해서 새로운 구조를 선택해야할지도 모릅니다.
결국 모든 미래를 예측해서 처음부터 완벽한 설계는 불가능한 것 같습니다. 중요한 건 현재 상황과 가까운 미래를 위해 가장 효율적인 선택을 하고, 문제가 생겼을 때 유연하게 개선해나가는 태도라고 생각합니다.
S3 presigned URL도 처음에는 "그냥 도입하면 되지 뭐"라고 가볍게 생각했다가, 데이터 정합성 처럼 고려해야할 디테일이 많아 꽤 애를 먹었습니다. 하지만 그 덕분에 깊이 있게 고민해보고 여러 방식을 생각해 볼 수 있어서 재미있는 경험이었습니다.
이 글이 비슷한 고민을 하시는 분들께 조금이라도 도움이 되길 바랍니다. 모두 즐거운 코딩하시길! 즐코
'Review > 우아한테크코스7기' 카테고리의 다른 글
| 백엔드 개발자가 아무것도 안하고 스트리밍 하는 법 (with HTTP Range, Progressive Download) (0) | 2025.11.06 |
|---|---|
| [우아콘 2025] 후기 (0) | 2025.10.29 |
| [Level3] Level 3 회고 (25.07.12~25.08.22) (12) | 2025.08.29 |
| [Level3] Sprint 1 회고 (25.07.08~25.07.11) (3) | 2025.07.27 |
| [우아한테크코스 7기] - 레벨 1을 마치며.. (5) | 2025.04.14 |
