Multipart Upload는 텍스트 필드와 바이너리 파일을 포함한 여러 이질적인 데이터 부분을 단일 요청으로 전송할 수 있는 HTTP 메커니즘입니다. 각 부분은 고유한 경계 문자열로 구분되며 자체 Content-Type 헤더를 가집니다. MDN Web Docs, 2025에 따르면, multipart/form-data는 HTML 양식을 통해 파일을 업로드하는 표준 형식이며 이미지, 문서 및 기타 파일을 서버로 전송하기 위해 웹 및 모바일 애플리케이션에서 널리 사용됩니다.
핵심 포인트
Multipart Upload는 요청 본문이 여러 논리적으로 분리된 부분으로 구성된 HTTP 프로토콜을 통한 데이터 전송 방법입니다. 각 부분에는 텍스트 양식 필드, 바이너리 파일, JSON 객체 또는 이미지와 같은 다른 유형의 데이터가 포함될 수 있습니다. 모든 부분은 단일 POST 요청으로 패키징되어 N개의 개별 HTTP 호출을 보낼 필요가 없습니다. Multipart Upload는 웹 양식 및 파일 업로드 API의 필수 부분입니다.
멀티파트 형식은 이메일 메시지용 MIME 표준의 일부로 RFC 2046 사양에서 정의되었으며, 이후 RFC 1867에서 HTTP에 적용되었습니다. 오늘날 웹 개발에서는 파일이 포함된 양식을 위해 설계된 멀티파트 하위 유형 중 하나인 multipart/form-data가 거의 독점적으로 사용됩니다. 다른 하위 유형(multipart/mixed(임의 첨부 파일용) 및 multipart/byteranges(파일 부분 다운로드용))은 훨씬 덜 자주 사용됩니다.
멀티파트와 단순한 application/x-www-form-urlencoded의 근본적인 차이점은 후자가 모든 데이터를 URI 호환 문자열로 인코딩하고 바이너리 파일을 지원하지 않는다는 것입니다. 반면 Multipart/form-data는 각 파일을 인코딩 없이 원래 바이너리 형식으로 전송하므로 더 효율적이고 정밀도를 잃지 않습니다. 요청 크기는 멀티파트의 경우 파트 헤더와 경계의 오버헤드로 인해 파일 크기 합계보다 항상 5-15% 더 큽니다.
Multipart Upload는 파일 업로드가 필요한 모든 곳에서 사용됩니다: 소셜 네트워크의 아바타 및 프로필 사진, 메신저의 첨부 파일, CRM 시스템의 문서, 온라인 스토어의 제품 이미지. 모바일 애플리케이션에서는 장치 카메라의 사진, 음성 녹음, 비디오 클립 등 미디어 파일을 서버로 보내는 데 Multipart Upload가 사용됩니다. Cloudflare Research에 따르면 웹의 모든 POST 요청 중 약 15%가 multipart/form-data를 사용합니다.
Multipart Upload와 Chunked Transfer는 다른 메커니즘입니다. 멀티파트는 요청을 의미 있는 부분(필드 및 파일)으로 나누는 반면, 청크 전송은 총 크기를 알지 못해도 전송할 수 있도록 데이터 스트림을 조각으로 나눕니다. 멀티파트는 청크 전송 내에서 전송될 수 있습니다: 서버는 전체 크기를 알지 못해도 멀티파트 응답을 부분적으로 보냅니다. 이러한 메커니즘은 충돌하지 않으며 다른 수준에서 다른 문제를 해결합니다.
브라우저가 enctype="multipart/form-data" 속성이 있는 양식을 제출하면 멀티파트 형식으로 요청 본문을 구성합니다. 각 양식 필드는 경계 문자열(boundary)로 다른 필드와 구분되는 별도의 블록이 됩니다. 경계는 자동으로 생성되며 데이터 내에 나타나지 않도록 보장된 고유한 문자 시퀀스입니다. 클라이언트는 이 경계를 Content-Type 헤더에 추가합니다: multipart/form-data; boundary=----WebKitFormBoundaryX7K.
각 블록은 --boundary로 시작하며 필드 이름(name)과 파일의 경우 원래 파일 이름(filename)이 포함된 Content-Disposition 헤더를 포함합니다. 빈 줄 다음에 실제 필드 데이터 또는 바이너리 형식의 파일 내용이 옵니다. 요청은 문자열 --boundary--로 끝납니다. 서버는 수신된 스트림을 구문 분석합니다: 먼저 경계를 찾은 다음 각 부분의 헤더를 추출하고 데이터 유형을 결정하여 양식 핸들러 또는 API 컨트롤러에 전달합니다.
IETF RFC 7578에 따르면 multipart/form-data는 각 부분에 대해 charset을 지정할 필요가 없습니다. 텍스트 필드는 UTF-8로 간주되고 바이너리 부분에는 원래 인코딩의 파일이 포함되기 때문입니다. 한 부분의 크기는 프로토콜에 의해 제한되지 않습니다 — 제한은 서버 수준에서 설정됩니다: 예를 들어 Nginx에서는 client_max_body_size를 통해, Spring Boot에서는 spring.servlet.multipart.max-file-size를 통해 설정됩니다.
Boundary는 전송된 데이터에 나타나서는 안 되는 고유 문자열입니다. 일반적으로 접두사(예: ----WebKitFormBoundary 또는 ----Boundary)로 시작하며 임의의 문자를 포함합니다. 브라우저와 HTTP 클라이언트는 자동으로 boundary를 생성합니다. RFC 2046에 따라 boundary의 길이는 70자를 초과하지 않아야 합니다. 각 부분은 문자열 --boundary\r\n으로 구분되며 요청의 끝은 --boundary--\r\n으로 표시됩니다.
멀티파트 요청은 MIME 및 HTTP 표준에 의해 정의된 엄격한 구조를 가집니다. 요청 헤더는 boundary 매개변수와 함께 Content-Type: multipart/form-data를 설정합니다. 요청 본문은 일련의 부분으로 구성되며, 각 부분에는 자체 헤더와 본문이 포함됩니다. 파트 헤더에는 Content-Disposition(필수) 및 Content-Type(선택 사항, 파일용)이 포함됩니다. 파트 헤더와 해당 데이터 사이에 빈 줄이 있어야 합니다.
| 요소 | 예시 | 필수 |
|---|---|---|
| Content-Type | multipart/form-data; boundary=---Bnd123 | 예 |
| 파트 구분자 | ---Bnd123 | 예 (각 파트 앞) |
| Content-Disposition | form-data; name="avatar"; filename="photo.jpg" | 예 |
| 파트 Content-Type | image/jpeg | 파일용 |
| 파트 본문 | [바이너리 이미지 데이터] | 예 |
| 종료 경계 | ---Bnd123-- | 예 (요청 끝) |
텍스트 필드와 이미지 파일을 보내는 멀티파트 요청의 실제 예를 살펴보겠습니다. 클라이언트는 고유한 boundary로 Content-Type 헤더를 형성합니다. 요청 본문에는 모든 양식 필드가 순서대로 포함됩니다. 수신 시 서버는 이러한 부분을 구문 분석하고 개발자에게 각 필드에 개별 객체로 액세스할 수 있도록 합니다. 이 접근 방식을 사용하면 단일 HTTP 호출로 파일이 포함된 복잡한 양식을 처리할 수 있습니다.
import okhttp3.*
import java.io.File
fun uploadFile() {
val client = OkHttpClient()
val imageFile = File("/path/to/photo.jpg")
val requestBody = MultipartBody.Builder()
.setType(MediaType.parse("multipart/form-data"))
.addFormDataPart("username", "john_doe")
.addFormDataPart(
"avatar", "photo.jpg",
RequestBody.create(
MediaType.parse("image/jpeg"), imageFile
)
)
.build()
val request = Request.Builder()
.url("https://api.example.com/upload")
.post(requestBody)
.build()
client.newCall(request).execute().use { response ->
println("업로드됨: ${response.isSuccessful}")
}
}
서버 측에서 멀티파트 요청은 프레임워크에 의해 또는 수동으로 구문 분석됩니다. Spring Boot에서는 @RequestParam("avatar") MultipartFile file 어노테이션만 있으면 프레임워크가 자동으로 멀티파트 요청에서 파일을 추출합니다. Kotlin의 Ktor에서는 receiveMultipart()가 사용되고 Express.js에서는 multer 미들웨어가 사용됩니다. 서버는 각 양식 필드와 각 업로드된 파일에 독립적으로 액세스할 수 있으며 파일을 디스크나 클라우드 스토리지에 저장하고 URL 또는 식별자를 클라이언트에 반환합니다.
Multipart Upload는 대체 데이터 전송 방법에 비해 몇 가지 주요 이점을 제공합니다. 여러 개 대신 하나의 요청 — 모든 양식 필드와 파일이 단일 HTTP 호출로 전송되어 네트워크 및 서버 부하가 줄어듭니다. N개의 파일을 업로드하기 위해 N개의 연결을 열 필요가 없습니다 — 모든 것이 하나의 POST에 패키징됩니다. 이는 각 HTTP 연결이 지연 시간과 배터리 소모를 의미하는 모바일 애플리케이션에서 특히 중요합니다.
인코딩 없는 바이너리 전송 — 바이너리 데이터가 base64로 인코딩되는(크기가 33% 증가) application/x-www-form-urlencoded와 달리 multipart/form-data는 파일을 원래 바이너리 형식으로 전송합니다. 이는 크기와 속도 면에서 더 효율적입니다. 10MB가 넘는 큰 파일의 경우 차이가 중요해집니다: 동일한 파일로 URL 인코딩된 요청보다 멀티파트 요청이 30% 더 작습니다.
임의 구조 — 멀티파트는 다른 유형의 필드를 어떤 순서로든 결합할 수 있습니다. 양식에는 텍스트 필드, 여러 파일, JSON 데이터 및 숨겨진 필드가 동시에 포함될 수 있습니다. 각 부분은 자체 Content-Type을 가지므로 텍스트와 바이너리 데이터를 혼합할 수 있습니다. 비교하자면: base64 인코딩은 크기에 33%를 추가하는 반면 멀티파트는 서비스 헤더에 약 5-15%만 추가합니다.
HTTP Archive, 2025 연구에 따르면 웹에서 파일 업로드의 94%에서 multipart/form-data가 사용됩니다. 대안은 JSON의 base64(4%)와 WebSocket을 통한 직접 전송(2%)입니다. JSON의 base64는 다른 모든 데이터도 JSON 형식인 API에 편리하지만 큰 파일에는 비효율적입니다. WebSocket은 실시간 데이터에 적합하지만 모든 HTTP 인프라에서 지원되는 것은 아닙니다. 멀티파트는 단순성과 효율성 덕분에 파일 업로드의 표준으로 남아 있습니다.
모바일 애플리케이션에서 Multipart Upload는 사용자 장치의 미디어 콘텐츠(갤러리 사진, 카메라 촬영, 음성 녹음, 문서 파일)를 서버로 보내는 데 사용됩니다. Android에서 표준 접근 방식은 MultipartBody.Builder와 함께 OkHttp를 사용하는 것으로 멀티파트 요청을 쉽게 만들 수 있습니다. Retrofit도 @Multipart 및 @Part 어노테이션을 통해 멀티파트를 지원합니다. 개발자가 각 부분의 데이터 유형을 지정하면 HTTP 클라이언트가 자동으로 올바른 헤더를 생성합니다.
iOS에서는 동일한 작업이 사용자 지정 HTTPBodyStream이 있는 URLSession 또는 Alamofire와 multipartFormData를 통해 해결됩니다. Alamofire는 멀티파트 요청을 보내기 위한 편리한 upload(multipartFormData:) 메서드를 제공합니다. 두 플랫폼 모두에서 업로드되는 파일의 크기를 고려하는 것이 중요합니다 — 큰 파일(10-20MB 이상)의 경우 애플리케이션이 최소화될 때 종료되지 않도록 백그라운드 업로드를 사용하는 것이 좋습니다. Android에서는 DownloadManager 또는 WorkManager를 통해, iOS에서는 백그라운드 구성이 있는 URLSession을 통해 수행됩니다.
모바일 애플리케이션에서 파일을 업로드할 때 네트워크 상태를 고려해야 합니다. Android의 Connectivity Manager는 Wi-Fi 또는 모바일 데이터를 사용할 수 있는지 확인하고 업로드에 최적의 시기를 선택하는 데 도움이 됩니다. 비디오와 같은 큰 파일의 경우 사용자의 모바일 데이터를 소모하지 않도록 Wi-Fi에 연결될 때까지 업로드를 지연하는 것이 좋습니다. Android의 WorkManager는 NetworkType.UNMETERED를 통해 이러한 제약 조건을 설정할 수 있습니다.
Multipart Upload를 통해 파일을 보내기 전에 모바일 애플리케이션은 종종 이미지를 압축하고 크기를 조정합니다. JPEG 압축(품질 85%)은 화면 보기를 위한 눈에 띄는 품질 손실 없이 파일 크기를 3-5배 줄입니다. 긴 쪽을 1920px로 크기 조정하면 크기가 더 줄어듭니다. Android에서는 Bitmap.compress()를 사용하고 iOS에서는 압축 매개변수 0.85의 UIImageJPEGRepresentation을 사용합니다. 이러한 최적화는 업로드를 가속화하고 모바일 데이터를 절약합니다.
Multipart Upload에서 가장 흔한 오류는 서버의 요청 크기 제한을 초과하는 것입니다. 기본적으로 Nginx는 요청 본문 크기를 1MB(client_max_body_size)로 제한하고 Tomcat은 2MB(maxSwallowSize)로 제한합니다. 개발자가 이러한 제한을 늘리지 않으면 서버는 413 Request Entity Too Large 오류를 반환합니다. 해결책은 서버에서 최대 업로드 크기를 명시적으로 구성하고 파일이 허용된 크기를 초과하는 경우 클라이언트에 경고를 표시하는 것입니다.
두 번째 문제는 본문 스트리밍 중 멀티파트 요청의 잘못된 처리입니다. 일부 서버는 구문 분석하기 전에 전체 멀티파트 요청을 메모리에 로드하려고 시도하여 큰 파일의 경우 OutOfMemoryError를 초래합니다. 최신 서버(Nginx, Spring Boot, Ktor)는 각 부분이 도착할 때 처리되는 스트리밍 멀티파트 구문 분석을 지원합니다. 개발자는 서버가 멀티파트 요청의 스트리밍 처리를 위해 구성되어 있는지 확인해야 합니다.
세 번째 문제 범주는 큰 파일 업로드 시 시간 초과입니다. HTTP 클라이언트에는 50-100MB가 넘는 파일의 긴 업로드 중에 트리거될 수 있는 readTimeout 및 connectTimeout 설정이 있습니다. 해결책은 업로드 엔드포인트의 시간 초과를 늘리거나 멀티파트 내에서 청크 전송 인코딩을 사용하는 것입니다. 모바일 장치에서는 업로드 중단을 처리하고 연결 끊김 시 재개(resume)를 구현하는 것도 중요합니다.
파일 업로드(멀티파트를 통한)는 웹 애플리케이션의 가장 취약한 엔드포인트 중 하나입니다. 공격자는 실행 가능한 스크립트를 image.jpg로 이름을 변경하여 업로드할 수 있습니다. 서버는 업로드된 파일의 MIME 유형을 확장자가 아닌 내용(매직 바이트)으로 확인하고 허용된 유형을 제한하며 바이러스 백신으로 파일을 검사해야 합니다. 업로드된 파일은 웹 서버의 document-root 외부에 저장하고 액세스 권한 확인이 있는 별도의 컨트롤러를 통해 제공하는 것이 좋습니다.
자주 묻는 질문
multipart/form-data는 각 양식 필드를 자체 헤더가 있는 별도 블록으로 전송하며 인코딩 없이 바이너리 파일을 지원합니다. application/x-www-form-urlencoded는 모든 데이터를 URI 호환 문자열(키=값&키2=값2)로 인코딩하며 파일을 직접 지원하지 않습니다 — base64로 인코딩해야 합니다.
HTTP 프로토콜은 멀티파트 요청의 크기를 제한하지 않지만 실제로는 서버에 의해 제한이 설정됩니다. Nginx는 기본적으로 1MB, Apache는 2MB, Spring Boot는 1MB로 제한합니다. 큰 파일을 업로드하려면 client_max_body_size(Nginx) 또는 spring.servlet.multipart.max-file-size(Spring Boot)를 원하는 값(예: 100MB)으로 구성하세요.
네, multipart/form-data는 단일 요청에서 여러 파일을 지원합니다. 각 파일은 자체 Content-Disposition 및 Content-Type이 있는 별도 부분으로 전송됩니다. HTML 양식은 input type="file"에 multiple 속성을 사용합니다. OkHttp에서는 각 파일에 대해 addFormDataPart가 호출되고 Alamofire에서는 각 파일에 대해 append가 호출됩니다.
Boundary는 복합 요청의 부분을 구분하고 서버가 한 부분의 끝과 다른 부분의 시작을 결정할 수 있도록 하는 고유 문자열입니다. 클라이언트에 의해 생성되며 Content-Type 헤더에 지정됩니다. boundary가 없으면 서버는 다중 구성 요소 요청을 개별 필드와 파일로 구문 분석할 수 없습니다.
파일 확장자나 요청의 Content-Type을 신뢰하지 마세요 — 공격자가 이를 위조할 수 있습니다. 매직 바이트(파일의 처음 몇 바이트)를 통해 MIME 유형을 확인하세요: Java의 Apache Tika, C/C++의 libmagic, Linux의 file 명령 또는 프레임워크 내장 도구(Java의 Files.probeContentType(), Python의 mimetypes)를 사용하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.