파일 업로드를 구현할 때, 클라이언트가 웹브라우저라면 폼을 통해서 파일을 등록해서 전송하게 됩니다. 이때 웹브라우저가 보내는 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--
정리
HTTP요청의 multipart/formdata를 수동으로 구성하기 위해서는
- 헤더에 Content-type=multipart/formdata으로 콘텐츠타입을 줍니다. 이 때 각 파트간의 경계를 위한 구분자를 추가합니다.
Content-type=multipart/formdata; boundary=----myboundary
- 구분자는
--
으로 시작해야 합니다 - 각각의 파트에는 헤더가 들어가며,
Content-Disposition: form-data; name=" ... "
과 같은 폼필드의 정보를 추가합니다. - 모든 파트가 끝나면 구분자 뒤에
--
을 추가해줍니다.