multipart/form-data 타입의 HTTP 메시지 구성 방법

파일 업로드를 구현할 때, 클라이언트가 웹브라우저라면 폼을 통해서 파일을 등록해서 전송하게 됩니다. 이때 웹브라우저가 보내는 HTTP 메시지는 Content-Type 속성이 multipart/form-data로 지정되며, 정해진 형식에 따라 메시지를 인코딩하여 전송합니다. 이를 처리하기 위한 서버는 멀티파트 메시지에 대해서 각 파트별로 분리하여 개별 파일의 정보를 얻게 됩니다.

만약 서버사이드가 이러한 방식으로 동작할 때, 웹브라우저처럼 파일을 멀티파트 메시지로 만들어서 업로드하는 것을 별도의 앱에서 구현하려면 어떻게해야 할까요? 그 방법을 알기 위해서 멀티파트 http 메시지가 어떻게 생겼는지를 살펴보면 거기에 해답이 있을 것 같습니다. 먼저 http 메시지의 구조를 보겠습니다.

HTTP 메시지는 기본적으로 헤더와 본문(payload)으로 구성됩니다. 헤더는 기본적으로 ascii 코드로만 작성되는 것으로 간주하며, 양 끝단에서는 해당 메시지의 앞 부분을 텍스트 데이터로 해석합니다. 기본적으로 헤더와 본문은 빈 줄 하나로 구분되며, 헤더는 다시 각각의 라인으로 구분됩니다.

헤더는 맨처음 method 타입과 URI 그리고 규약의 종류(http/https/ftp….)를 명시합니다. 이후에 호스트, 사용자 에이전트, 인코딩, 타입 등의 여러 정보를 추가로 넣어줍니다. 그런 다음 빈 줄 다음 부터는 메시지의 payload로 해석합니다.

웹브라우저가 멀티파트 요청을 보낼때에는 헤더에 Content-type 필드 값이 “multipart/form-data”로 명시됩니다. 이때, 세미콜론으로 구분한 다음 boundary값을 넣어줍니다. 이 바운더리 문자열은 다시 메시지 페이로드를 각 파트로 구분하는 구분자가 됩니다. 관례적으로 연속된 하이픈으로 시작하며, 임의의 데이터를 넣어주면 됩니다.

엄밀하게는 (개행)바운더리문자열(개행)을 기준으로 구분하게 됩니다. 또, 이때의 개행은 플랫폼에 상관없이 CRLF로 \r\n을 사용해야 합니다.

바운더리 값으로 구분된 각각의 데이터는 다시 헤더와 페이로드로 나눠질 수 있으며, 헤더와 페이로드는 빈줄로 구분합니다. 보통 각 파트에는 콘텐츠 타입, 디스포지션등이 명시됩니다. 예를 들어 이미지 파일을 전송한 경우에는 다음과 같은 내용이 들어가겠죠.

--------12345678MYBOUNDARY
Content-Disposition: form-data; name="formfield_name"; filename="a.png"
Content-Type: image/png

......

이 때 유의할 것은 각 파트와 파트는 (개행된 후) 바운더리문자열로만 구분하며 파트 사이에는 빈줄이 포함되지 않는다는 것입니다.

마지막 파트의 끝에도 바운더리 값이 들어가는데, 다른 경우와 달리 이 때엔 바운더리값 뒤에 하이픈 2개를 추가해줍니다. (그래서 보통 바운더리를 —— 로 시작합니다.)

다음은 파이어폭스를 통해서 다중 파일 업로드를 요청했을 때, 서버가 수신하는 HTTP 메시지의 내용입니다.

POST /submit HTTP/1.1
Host:my.server.com
User-Agent: Mozilla/5.0 Gecko/2009042316 FireFox/3.0.10
Accept: text/html,application/xhtml+html,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://my.server.com/test/index.html
Content-type: multipart/form-data; boundary=--------287032381131322
Content-length: 514

----------287032381131322
Content-Disposition: form-data; name="datafiled1"; filename="r.gif"
Content-Type: image/gif

GIF871...............................D..;
----------287032381131322
Content-Disposition: form-data; name="datafiled1"; filename="g.gif"
Content-Type: image/gif

GIF87a...............................D..;
----------287032381131322
Content-Disposition: form-data; name="datafiled1"; filename="b.gif"
Content-Type: image/gif

GIF87a...............................D..;
----------287032381131322--