HTML 폼 데이터 전송하기 - 파트 2

등록일시: 2013-09-25 01:11,  수정일시: 2013-09-28 22:31
조회수: 20,595
이 문서는 ASP.NET Web API 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.

파트 2: 파일 업로드와 Multipart MIME

본 자습서에서는 Web API에 파일을 업로드하는 방법을 살펴보고, Multipart MIME 데이터를 처리하는 방법에 관해서도 살펴봅니다.

전체 프로젝트 다운로드 받기

다음은 파일 업로드를 위한 예제 HTML 폼 입니다:

<form name="form1" method="post" enctype="multipart/form-data" action="api/upload">
    <div>
        <label for="caption">Image Caption</label>
        <input name="caption" type="text" />
    </div>
    <div>
        <label for="image1">Image File</label>
        <input name="image1" type="file" />
    </div>
    <div>
        <input type="submit" value="Submit" />
    </div>
</form>

이 예제 폼에는 텍스트 입력 컨트롤과 파일 입력 컨트롤이 함께 존재하는데, 이와 같이 폼에 파일 입력 컨트롤이 존재하는 경우에는 반드시 enctype 어트리뷰트의 값이 "multipart/form-data"로 지정되어야 하며, 이는 폼의 데이터가 Multipart MIME 메시지 형태로 전송되도록 지시합니다.

Multipart MIME 메시지 포멧의 구조는 요청 예제를 한 번 보기만해도 이해할 수 있을 정도로 단순합니다:

POST http://localhost:50460/api/values/1 HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------41184676334
Content-Length: 29278

-----------------------------41184676334
Content-Disposition: form-data; name="caption"

Summer vacation
-----------------------------41184676334
Content-Disposition: form-data; name="image1"; filename="GrandCanyon.jpg"
Content-Type: image/jpeg

(이진 데이터는 생략합니다.)
-----------------------------41184676334--

이 메시지를 살펴보면 각각의 폼 컨트롤에 대응하는 두 부분(Part)으로 나뉘어 있는 것을 알 수 있습니다. 또한, 각 부분들의 경계는 대시(-) 문자들로 구성된 특정 문자열 줄로 구분되어 있습니다.

노트: 부분 경계 표시줄에 포함되어 있는 랜덤 구성 요소(이 예제에서는 "41184676334")는 메시지 내용 중에 경계 표시줄과 동일한 문자열이 우연히 나타나는 경우를 대비하기 위한 것입니다.

각 메시지 부분에는 메시지의 콘텐트에 따라, 다음 중 하나 이상의 헤더들이 포함될 수 있습니다.

  • Content-Disposition 헤더: 컨트롤 이름을 담고 있습니다. 컨트롤이 파일 컨트롤인 경우, 파일명도 함께 포함됩니다.
  • Content-Type 헤더: 데이터 유형을 지정합니다. 이 헤더가 생략되면 기본값으로 text/plain이 사용됩니다.

그러므로 위의 요청 예제를 살펴보면 GrandCanyon.jpg라는 이름의 파일을 image/jpeg 콘텐트 형식으로 업로드했으며, 텍스트 입력 컨트롤에 "Summer Vacation"이라는 값을 입력했다는 것을 알 수 있습니다.

파일 업로드

계속해서 Multipart MIME 메시지에서 파일을 읽어오는 Web API 컨트롤러 예제를 살펴보겠습니다. 이 컨트롤러에서는 파일을 비동기적으로 읽어올 것입니다. 참고로, Web API는 Task-기반 프로그래밍 모델 (Task-Based Programming Model)을 통해서 비동기적 액션을 지원합니다. 먼저, 다음 코드는 async 키워드와 await 키워드가 지원되는 .NET 프레임워크 4.5를 사용하는 예제입니다.

using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
    
public class UploadController : ApiController
{
    public async Task<HttpResponseMessage> PostFormData()
    {
        // 요청이 multipart/form-data를 담고 있는지 확인합니다.
        if (!Request.Content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }
    
        string root = HttpContext.Current.Server.MapPath("~/App_Data");
        var provider = new MultipartFormDataStreamProvider(root);
    
        try
        {
            // 폼 데이터 읽기.
            await Request.Content.ReadAsMultipartAsync(provider);
    
            // 다음 코드를 통해서 파일 이름을 읽어오는 방법을 알 수 있습니다.
            foreach (MultipartFileData file in provider.FileData)
            {
                Trace.WriteLine(file.Headers.ContentDisposition.FileName);
                Trace.WriteLine("Server file path: " + file.LocalFileName);
            }
            return Request.CreateResponse(HttpStatusCode.OK);
        }
        catch (System.Exception e)
        {
            return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
        }
    }
    
}

이 컨트롤러 액션이 매개변수를 받지 않는다는 점에 유의하시기 바랍니다. 그 이유는 미디어-형식 포멧터를 호출하지 않고 직접 액션 내부에서 요청 본문을 처리하기 때문입니다.

IsMultipartContent 메서드는 요청에 Multipart MIME 메시지가 포함되어 있는지 여부를 점검합니다. 그렇지 않다면, 컨트롤러가 HTTP 상태 코드 415, 지원하지 않는 미디어 형식을 반환합니다.

그리고, MultipartFormDataStreamProvider 클래스는 업로드 된 파일들에 대해 파일 스트림을 할당해주는 도우미 개체입니다. Multipart MIME 메시지는 ReadAsMultipartAsync 메서드를 호출해서 읽을 수 있습니다. 이 메서드는 메시지 부분들을 모두 추출해서 MultipartFormDataStreamProvider가 제공해주는 스트림에 기록합니다.

이 메서드의 호출이 완료되면 MultipartFileData 개체들의 컬렉션인 FileData 속성을 이용해서 파일들의 정보를 읽어올 수 있습니다.

  • MultipartFileData.FileName 속성은 파일이 저장된 서버의 로컬 파일 이름을 담고 있습니다.
  • MultipartFileData.Headers 속성은 부분 헤더들을 담고 있습니다 (주의, 요청 자체의 헤더가 아닙니다). 이 속성을 통해서 Content-Disposition 헤더나 Content-Type 헤더에 접근할 수 있습니다.

메서드 이름에서 짐작할 수 있듯이 ReadAsMultipartAsync 메서드는 비동기 메서드입니다. 이 메서드가 완료된 뒤에 후속 작업을 계속해서 수행하려면, .NET 4.0을 사용하고 있다면 연속 작업(Continuation Task)을, .NET 4.5를 사용하고 있다면 await 키워드를 사용하면 됩니다.

다음은 이번 예제 코드의 .NET 프레임워크 4.0 버전입니다:

public Task<HttpResponseMessage> PostFormData()
{
    // 요청이 multipart/form-data를 담고 있는지 확인합니다.
    if (!Request.Content.IsMimeMultipartContent())
    {
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }
   
    string root = HttpContext.Current.Server.MapPath("~/App_Data");
    var provider = new MultipartFormDataStreamProvider(root);
   
    // 폼 데이터 읽고 비동기 Task를 반환합니다.
    var task = Request.Content.ReadAsMultipartAsync(provider).
        ContinueWith<HttpResponseMessage>(t =>
        {
            if (t.IsFaulted || t.IsCanceled)
            {
                Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception);
            }
   
            // 다음 코드를 통해서 파일 이름을 읽어오는 방법을 알 수 있습니다.
            foreach (MultipartFileData file in provider.FileData)
            {
                Trace.WriteLine(file.Headers.ContentDisposition.FileName);
                Trace.WriteLine("Server file path: " + file.LocalFileName);
            }
            return Request.CreateResponse(HttpStatusCode.OK);
        });
   
    return task;
}

폼 컨트롤 데이터 읽어오기

본문의 앞 부분에서 살펴본 HTML 폼을 다시 살펴보면 파일 입력 컨트롤 외에도 텍스트 입력 컨트롤이 존재한다는 것을 알 수 있습니다.

<div>
    <label for="caption">Image Caption</label>
    <input name="caption" type="text" />
</div>

MultipartFormDataStreamProviderFormData 속성을 이용하면 컨트롤의 값을 읽어올 수 있습니다.

public async Task<HttpResponseMessage> PostFormData()
{
    if (!Request.Content.IsMimeMultipartContent())
    {
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }
   
    string root = HttpContext.Current.Server.MapPath("~/App_Data");
    var provider = new MultipartFormDataStreamProvider(root);
   
    try
    {
        await Request.Content.ReadAsMultipartAsync(provider);
   
        // 모든 키-값 쌍들을 출력합니다.
        foreach (var key in provider.FormData.AllKeys)
        {
            foreach (var val in provider.FormData.GetValues(key))
            {
                Trace.WriteLine(string.Format("{0}: {1}", key, val));
            }
        }
   
        return Request.CreateResponse(HttpStatusCode.OK);
    }
    catch (System.Exception e)
    {
        return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
    }
}

FormData 속성은 폼 컨트롤들에 대한 키/값 쌍을 담고 있는 NameValueCollection 개체인데, 이 컬렉션은 중복되는 키들을 포함할 수도 있습니다. 가령, 다음과 같은 폼을 가정해보겠습니다:

<form name="trip_search" method="post" enctype="multipart/form-data" action="api/upload">
    <div>
        <input type="radio" name="trip" value="round-trip"/>
        Round-Trip
    </div>
    <div>
        <input type="radio" name="trip" value="one-way"/>
        One-Way
    </div>
   
    <div>
        <input type="checkbox" name="options" value="nonstop" />
        Only show non-stop flights
    </div>
    <div>
        <input type="checkbox" name="options" value="airports" />
        Compare nearby airports
    </div>
    <div>
        <input type="checkbox" name="options" value="dates" />
        My travel dates are flexible
    </div>
   
    <div>
        <label for="seat">Seating Preference</label>
        <select name="seat">
            <option value="aisle">Aisle</option>
            <option value="window">Window</option>
            <option value="center">Center</option>
            <option value="none">No Preference</option>
        </select>
    </div>
</form>

이 폼의 요청 본문은 다음 예제와 비슷할 형태일 것입니다:

-----------------------------7dc1d13623304d6
Content-Disposition: form-data; name="trip"

round-trip
-----------------------------7dc1d13623304d6
Content-Disposition: form-data; name="options"

nonstop
-----------------------------7dc1d13623304d6
Content-Disposition: form-data; name="options"

dates
-----------------------------7dc1d13623304d6
Content-Disposition: form-data; name="seat"

window
-----------------------------7dc1d13623304d6--

이 경우, FormData 컬렉션에는 다음과 같은 키/값 쌍들이 포함됩니다:

  • trip: round-trip
  • options: nonstop
  • options: dates
  • seat: window