.NET 클라이언트에서 Web API 호출하기 (C#)

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

본 자습서에서는 콘솔 응용 프로그램에서 HttpClient를 사용해서 Web API를 호출하는 방법을 살펴봅니다.

본 자습서는 CRUD 작업을 지원하는 Web API 작성하기 자습서에서 살펴본 "ProductStore" API를 조회 대상으로 이용합니다.

콘솔 응응 프로그램 생성하기

먼저, Visual Studio를 시작하고 시작 (Start) 페이지에서 새 프로젝트 (New Project) 링크를 클릭합니다. 또는, 파일 (File) 메뉴에서 새 프로젝트 (New Project)를 선택해도 됩니다.

템플릿 (Templates) 패인에서 설치됨 (Installed) 노드 하위의 템플릿 (Templates) 노드를 선택하고, 그 하위의 Visual C# 노드를 확장합니다. 그리고, Visual C# 노드 하위의 Windows 노드를 선택한 다음, 프로젝트 템플릿 목록에서 콘솔 응용 프로그램 (Console Application)을 선택하고, 프로젝트 이름을 "ProductStoreClient"로 지정한 다음, 확인 (OK) 버튼을 클릭합니다.

NuGet 패키지 관리자 설치하기

프로젝트에 Web API 클라이언트 라이브러리를 추가하는 가장 손쉬운 방법은 NuGet 패키지 관리자를 사용하는 것입니다. 만약, NuGet 패키지 관리자가 아직 설치되어 있지 않다면 다음과 같은 방법으로 설치할 수 있습니다.

  1. Visual Studio를 시작합니다.
  2. 도구 (Tools) 메뉴에서, 확장 및 업데이트 (Extensions and Updates)를 선택합니다.
  3. 확장 및 업데이트 (Extensions and Updates) 대화 상자에서 온라인 (Online)을 선택합니다.
  4. 만약, "NuGet Package Manager"를 찾을 수 없다면 검색 상자에 "nuget package manager"를 입력합니다.
  5. 목록에서 NuGet Package Manager를 선택한 다음, 다운로드 (Download) 버튼을 클릭합니다.
  6. 다운로드가 완료되면, 설치를 위한 프롬프트가 나타날 것입니다.
  7. 설치가 완료되고 나면, Visual Studio를 재시작한다는 프롬프트가 나타납니다.

Web API 클라이언트 라이브러리 설치하기

프로젝트에 NuGet 패키지 관리자를 설치한 뒤에는, Web API 클라이언트 라이브러리 패키지를 설치해야 합니다.

  1. 도구 (Tools) 메뉴에서 라이브러리 패키지 관리자를 선택합니다.
    노트: 이 메뉴 항목이 나타나지 않는다면 NuGet 패키지 관리자가 정상적으로 설치되었는지부터 확인하시기 바랍니다.
  2. 솔루션용 NuGet 패키지 관리 (Manage NuGet Packages for Solution...)를 선택합니다.
  3. NugGet 패키지 관리 (Manage NugGet Packages) 대화 상자에서 온라인 (Online)을 선택합니다.
  4. 검색 상자에 "Microsoft.AspNet.WebApi.Client"를 입력합니다.
  5. "Microsoft ASP.NET Web API Client Libraries" 패키지를 선택합니다.
  6. 설치 (Install) 버튼을 클릭합니다.
  7. 패키지 설치를 마쳤으면, 대화 상자의 닫기 (Close) 버튼을 클릭합니다.

모델 클래스 추가하기

다음의 클래스를 응용 프로그램에 추가합니다:

class Product
{
    public string Name { get; set; }
    public double Price { get; set; }
    public string Category { get; set; }
}

이 클래스는 HttpClient가 HTTP 요청 본문에 쓰거나, HTTP 응답 본문에서 읽어들일 데이터 개체를 생성할 때 사용됩니다.

HttpClient 초기화하기

다음과 같이 새로운 HttpClient의 인스턴스를 생성하고 초기화시킵니다:

namespace ProductStoreClient
{
    using System;
    using System.Collections.Generic;
    using System.Net.Http;
    using System.Net.Http.Headers;
    
    class Program
    {
        static void Main(string[] args)
        {
            HttpClient client = new HttpClient();
            client.BaseAddress = new Uri("http://localhost:9000/");
    
            // JSON 형식에 대한 Accept 헤더를 추가합니다.
            client.DefaultRequestHeaders.Accept.Add(
                new MediaTypeWithQualityHeaderValue("application/json"));
        }
    }
}

이 코드는 기본 URI를 "http://localhost:9000/"로 설정한 다음, Accept 헤더를 "application/json"으로 설정해서 데이터를 JSON 형식으로 전송할 것임을 서버에게 알립니다.

리소스 가져오기 (HTTP GET)

다음 코드는 제품들의 목록을 가져오기 위한 API 질의 방법을 보여줍니다:

// 모든 제품들의 목록.
HttpResponseMessage response = client.GetAsync("api/products").Result;  // 호출 블록킹!
if (response.IsSuccessStatusCode)
{
    // 응답 본문 파싱. 블록킹!
    var products = response.Content.ReadAsAsync<IEnumerable<Product>>().Result;
    foreach (var p in products)
    {
        Console.WriteLine("{0}\t{1};\t{2}", p.Name, p.Price, p.Category);
    }
}
else
{
    Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

이 예제에서 GetAsync 메서드는 HTTP GET 요청을 전송합니다. 그리고, 메서드 이름에서 짐작할 수 있듯이 GetAsyc 메서드는 비동기적입니다. 이 메서드는 서버의 응답을 기다리지 않고 즉시 반환하는데, 반환값은 비동기 작업 자체을 의미하는 Task 개체입니다. 작업이 완료되면 Task.Result 속성에 HTTP 응답이 담겨집니다.

그런데, 여기에서 중요한 점은 Result 속성을 읽어오는 행위가 요청이 완료될 때까지 (또는 제한시간이 만료될 때까지) 응용 프로그램의 쓰레드를 블록시킨다는 사실입니다. 이번 예제와 같은 콘솔 응용 프로그램은 쓰레드가 블록킹되도 큰 문제가 없는 반면, Windows 응용 프로그램의 UI 쓰레드에서는 심각한 문제가 되는데, 그 이유는 사용자 입력에 대한 UI 응답이 블록되기 때문입니다. 이어지는 또 다른 자습서에서는 논-블록킹 호출을 작성하는 방법을 살펴봅니다.

HTTP 응답이 성공하면 제품들의 목록이 응답 본문에 JSON 형태로 담겨집니다. 이 목록을 파싱하려면 ReadAsAsync 메서드를 호출하면 되는데, 이 메서드는 응답 본문을 읽고 특정 CLR 형식으로 역직렬화를 시도합니다. 이 메서드도 비동기적인데, 그 이유는 응답의 본문이 상황에 따라서 매우 비대해질 수도 있기 때문입니다. 그리고, 이번에도 Result 속성을 읽어오는 동안 쓰레드가 블록킹됩니다.

다음은 HTTP 세션의 예입니다:

GET http://localhost:9000/api/products HTTP/1.1
Accept: application/json
Host: localhost:9000
Connection: Keep-Alive
    
HTTP/1.1 200 OK
Server: ASP.NET Development Server/11.0.0.0
Date: Mon, 20 Aug 2012 22:14:59 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Content-Length: 183
Connection: Close
    
[{"Id":1,"Name":"Tomato soup","Category":"Groceries","Price":1.39},{"Id":2,"Name":"Yo-yo",
"Category":"Toys","Price":3.75},{"Id":3,"Name":"Hammer","Category":"Hardware","Price":16.99}]

제품 ID를 지정해서 제품을 가져오는 예제도 비슷합니다:

// 특정 ID의 제품 조회하기.
response = client.GetAsync("api/products/1").Result;
if (response.IsSuccessStatusCode)
{
    // 응답 본문 파싱. 블록킹!
    var product = response.Content.ReadAsAsync<Product>().Result;
    Console.WriteLine("{0}\t{1};\t{2}", product.Name, product.Price, product.Category);
}
else
{
    Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

미디어 형식 포멧터

ReadAsAsyncSystem.Net.Http.HttpContentExtensions 클래스에 정의되어 있는 확장 메서드입니다. 이 메서드는 매개변수를 지정하지 않으면 기본 미디어 형식 포멧터들의 모음을 사용해서 응답 본문 파싱을 시도합니다. 기본 포멧터로는 JSON, XML, Form-url-encoded 데이터를 지원합니다. (미디어 형식 포멧터에 관한 더 자세한 정보는 Formats and Model Binding를 참고하시기 바랍니다.)

명시적으로 사용하고 싶은 미디어 형식 포멧터를 지정할 수도 있습니다. 이 방식은 사용자 정의 미디어 형식 포멧터를 사용할 때 유용합니다.

var formatters = new List<MediaTypeFormatter>() {
    new MyCustomFormatter(),
    new JsonMediaTypeFormatter(),
    new XmlMediaTypeFormatter()
};
    
resp.Content.ReadAsAsync<IEnumerable<Product>>(formatters);

리소스 생성하기 (HTTP POST)

다음 코드는 JSON 형식의 Product 인스턴스를 담고 있는 POST 요청을 전송합니다:

// 새로운 제품을 생성합니다.
var gizmo = new Product() { Name = "Gizmo", Price = 100, Category = "Widget" };
Uri gizmoUri = null;
    
response = client.PostAsJsonAsync("api/products", gizmo).Result;
if (response.IsSuccessStatusCode)
{
    gizmoUri = response.Headers.Location;
}
else
{
    Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

PostAsJsonAsyncSystem.Net.Http.HttpClientExtensions 클래스에 정의되어 있는 확장 메서드입니다. 이 메서드는 다음과 비슷한 작업을 수행합니다:

var product = new Product() { Name = "Gizmo", Price = 100, Category = "Widget" };
    
// JSON 포멧터를 생성합니다.
MediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
    
// 요청 본문의 컨텐트를 생성하기 위해 JSON 포멧터를 사용합니다.
HttpContent content = new ObjectContent<Product>(product, jsonFormatter);
    
// 요청을 전송합니다.
var resp = client.PostAsync("api/products", content).Result;

XML 형식을 사용하려면 PostAsXmlAsync 메서드를 대신 호출하면 됩니다.

다음은 HTTP 세션의 예입니다:

POST http://localhost:9000/api/products HTTP/1.1
Accept: application/json
Content-Type: application/json; charset=utf-8
Host: localhost:9000
Content-Length: 50
Expect: 100-continue
    
{"Name":"Gizmo","Price":100.0,"Category":"Widget"}
    
HTTP/1.1 201 Created
Server: ASP.NET Development Server/11.0.0.0
Date: Mon, 20 Aug 2012 22:15:00 GMT
X-AspNet-Version: 4.0.30319
Location: http://localhost:9000/api/products/7
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: application/json; charset=utf-8
Content-Length: 57
Connection: Close
    
{"Id":7,"Name":"Gizmo","Category":"Widget","Price":100.0}

기본적으로 JSON 포멧터는 컨텐트 형식을 "application/json"으로 설정하는데, 명시적으로 미디어 형식을 지정할 수도 있습니다. 가령, Product의 인스턴스에 지정하고자 하는 미디어 형식이 "application/vnd.example.product"라면, 다음과 같이 미디어 형식을 설정할 수 있습니다:

HttpContent content = new ObjectContent<Product>(product, jsonFormatter, 
    "application/vnd.example.product+json");

리소스 갱신하기 (HTTP PUT)

다음 코드는 PUT 요청을 전송합니다.

// 제품을 갱신합니다.
gizmo.Price = 99.9;
response = client.PutAsJsonAsync(gizmoUri.PathAndQuery, gizmo).Result;
Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);

PutAsJsonAsync 메서드는 POST 대신 PUT 요청을 전송한다는 점만 빼면 PostAsJsonAsync 메서드와 비슷하게 동작합니다.

리소스 삭제하기 (HTTP DELETE)

아마 이쯤되면 DELETE 요청을 전송하는 방법도 예상할 수 있을 것입니다:

// 제품을 삭제합니다.
response = client.DeleteAsync(gizmoUri).Result;
Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);

DELETE 요청은 GET과 동일하게 요청 본문을 갖고 있지 않으므로 JSON이나 XML 형식을 지정할 필요가 없습니다.

오류 처리

HttpClient는 오류 코드를 담고 있는 HTTP 응답을 받아도 예외를 던지지 않습니다. 대신, 응답의 StatusCode 속성에 상태 코드가 담겨집니다. 더불어, 상태가 성공 코드인 경우 (200-299 범위의 상태 코드) IsSuccessStatusCode 속성도 true로 설정됩니다.

본문의 예제들도 이런 패턴을 사용했습니다:

HttpResponseMessage response = client.GetAsync("api/products").Result;
if (response.IsSuccessStatusCode)
{
     // ....
}

만약, 오류 코드를 예외로 다루는 방식을 더 선호한다면 EnsureSuccessStatusCode 메서드를 호출하면 됩니다. 이 메서드는 응답 코드가 성공 코드가 아니면 예외를 던집니다.

try
{
    var resp = client.GetAsync("api/products").Result;
    resp.EnsureSuccessStatusCode();    // 성공 코드가 아니면 예외를 던집니다.
    
    // ...
}
catch (HttpRequestException e)
{
    Console.WriteLine(e.Message);
}

물론, 제한시간 만료 등의 다른 이유로도 HttpClient에서 예외가 던져질 수 있습니다.

HttpClient 구성하기

HttpClient를 구성하려면 WebRequestHandler의 인스턴스를 생성한 다음, 속성들을 설정해서, 이를 HttpClient 생성자에 전달하면 됩니다:

WebRequestHandler handler = new WebRequestHandler()
{
    AllowAutoRedirect = false,
    UseProxy = false
};
HttpClient client = new HttpClient(handler);

WebRequestHandler 클래스는 HttpMessageHandler 클래스를 상속 받습니다. 직접 HttpMessageHandler 클래스를 상속 받아서 구현한 사용자 정의 메시지 헨들러를 끼워 넣을 수도 있습니다. 보다 자세한 정보는 HTTP Message Handlers를 참고하시기 바랍니다.

참고 목록

콘솔 응용 프로그램을 이용하면 보다 손쉽게 코드의 흐름을 살펴볼 수 있습니다. 그러나, 그래픽 UI 환경 응용 프로그램에서 호출이 블록킹되는 것은 바람직하지 않습니다. 블록킹 없이 비동기적으로 HttpClient를 이용한 작업을 수행하는 방법에 관한 더 자세한 내용은 WPF 응용 프로그램에서 Web API 호출하기 (C#)를 살펴보시기 바랍니다.