업로드 API가 서버 메모리와 대역폭을 갉아먹던 것을 presigned URL로 옮긴 과정
문제 발생
이미지 업로드를 평범한 API로 받았습니다.
POST /api/uploads
Content-Type: multipart/form-data; boundary=----X파일이 커지자 문제가 겹쳤습니다 — 앞단 nginx의 client_max_body_size에 막히고, 통과해도 앱 프로세스가 파일 전체를 받는 동안 메모리와 요청 슬롯을 점유했으며, 업로드 대역폭이 애플리케이션 서버에 그대로 얹혔습니다.
원인 분석
형식 자체는 맞습니다. MDN 설명대로 multipart/form-data 인코딩은 폼에 파일이나 많은 데이터가 포함될 때 사용되며, 본문을 boundary 문자열로 각 파트로 구분합니다. 기본값인 application/x-www-form-urlencoded는 바이너리 데이터에 적합하지 않아 이 용도에는 multipart/form-data를 써야 합니다.
문제는 형식이 아니라 경로입니다. 이 방식에서는 파일의 모든 바이트가 클라이언트 → 프록시 → 애플리케이션 → 스토리지로 흐릅니다. 우리 서버는 중계만 하면서 그 시간 내내 연결·메모리·대역폭을 씁니다. 사용자 수가 늘면 애플리케이션이 아니라 파일 전송이 스케일 한계가 됩니다.
해결 방안
- 큰 파일은 스토리지가 직접 받게 합니다. 서버는 업로드 URL만 발급합니다.
POST /api/uploads/sign → { url, fields, key } (짧은 만료)
PUT <url> → 브라우저가 스토리지로 직접 전송
POST /api/uploads/complete { key } → 서버가 검증·DB 기록-
서명 URL에 제약을 담습니다. 만료 시간, 최대 크기, 허용 Content-Type을 서명에 포함시켜 URL을 받은 사람이 임의 파일을 올리지 못하게 합니다.
-
완료 처리를 서버가 검증합니다. 클라이언트가 "올렸다"고 말하는 것을 믿지 않고, 스토리지에서 크기·타입을 확인한 뒤 DB에 기록합니다.
-
작은 파일은 그대로 multipart가 낫습니다. 프로필 이미지 몇백 KB에 3단계 흐름을 만들면 복잡도만 늘어납니다 — 경계를 정해 두 방식을 함께 씁니다.
-
프록시 한도를 함께 맞춥니다. multipart 경로를 유지한다면 nginx의
client_max_body_size와 애플리케이션의 본문 제한, 그리고 사용자에게 보여줄 오류 메시지를 한 세트로 정합니다. -
업로드된 파일을 그대로 서빙하지 않습니다. 확장자와
Content-Type을 서버가 정하고X-Content-Type-Options: nosniff를 붙입니다 — 사용자가 올린 파일이 브라우저에서 스크립트로 해석되는 사고를 막습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.