WPF 응용 프로그램에서 Web API 호출하기 (C#)

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

본 자습서에서는 WPF(Windows Presentation Foundation) 응용 프로그램에서 HttpClient를 통해서 Web API를 호출하는 방법을 살펴봅니다.

본문의 주 목적은 HttpClient를 이용한 비동기 작업 방식을 살펴보는 것입니다. 그리고, 본 자습서는 CRUD 작업을 지원하는 Web API 작성하기 자습서에서 살펴본 "ProductStore" API를 조회 대상으로 이용합니다.

본 자습서를 읽기 전에 먼저 .NET 클라이언트에서 Web API 호출하기 (C#) 자습서부터 읽어볼 것을 권합니다. 본문에서 사용되는 몇 가지 기본적인 개념들이 이 자습서에서 다뤄집니다.

비동기 호출

HttpClient는 논-블록킹 처리가 가능하도록 설계되었습니다. 장시간 실행될 가능성이 높은 작업들은 대부분 GetAsync 메서드나 PostAsync 메서드 같이 비동기 메서드로 구현되는 것이 일반적입니다. 이런 메서드들은 작업이 완전히 마무리될 때까지 기다리지 않고 즉시 반환됩니다. 반면, 이전 자습서(.NET 클라이언트에서 Web API 호출하기 (C#))에서는 호출이 블록킹되는 경우만 살펴봤습니다:

HttpResponseMessage response = client.GetAsync("api/products").Result;  // 호출 블록킹!

이 코드는 Result 속성을 읽어오면서 호출 쓰레드를 블록킹됩니다. 콘솔 응용 프로그램에서는 이런 방식도 전혀 문제가 되지 않지만, 사용자 입력에 대한 UI 응답이 블록킹되기 때문에 UI 쓰레드를 사용하는 Windows 응용 프로그램에서는 치명적입니다.

참고로 HttpClient의 비동기 메서드들은 비동기 작업 자체를 나타내는 Task 개체를 반환합니다.

WPF 프로젝트 생성하기

먼저, Visual Studio를 시작합니다. 그리고, 시작 (Start) 페이지에서 새 프로젝트 (New Project) 링크를 클릭합니다. 그런 다음, 템플릿 (Templates) 패인에서 설치됨 (Installed) 노드 하위의 템플릿 (Templates) 노드를 선택하고, 그 하위의 Visual C# 노드를 확장합니다. 프로젝트 템플릿 목록에서 WPF 응용 프로그램 (WPF Application)을 선택하고, 프로젝트 이름을 "WpfProductClient"로 지정한 다음, 확인 (OK) 버튼을 클릭합니다.

MainWindow.xaml 파일을 열고 다음의 XAML 마크업을 Grid 컨트롤 내부에 추가합니다:

<StackPanel Width="250" >
    <Button Name="btnGetProducts" Click="GetProducts">Get Products</Button>
    <ListBox Name="ProductsList">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Margin="2">
                    <TextBlock Text="{Binding Path=Name}" />
                    <TextBlock >Price: $<Run Text="{Binding Path=Price}" />
                        (<Run Text="{Binding Path=Category}" />)</TextBlock>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</StackPanel>

이 마크업은 제품들의 목록과 데이터-바인딩 될 ListBox를 정의합니다. 그리고, DataTemplate은 각각의 제품들이 출력되는 방식을 정의하고 있습니다.

모델 클래스 추가하기

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

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

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

그리고, 이번에는 데이터 바인딩을 위한 Observable 클래스를 추가합니다:

class ProductsCollection : ObservableCollection<Product>
{
    public void CopyFrom(IEnumerable<Product> products)
    {
        this.Items.Clear();
        foreach (var p in products)
        {
            this.Items.Add(p);
        }
    
        this.OnCollectionChanged(
            new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}

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) 버튼을 클릭합니다.

HttpClient 초기화하기

계속해서 솔루션 탐색기에서 MainWindow.xaml.cs 파일을 열고, 다음 코드를 추가합니다.

namespace WpfProductClient
{
    using System;
    using System.Collections.Generic;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Windows;
    
    public partial class MainWindow : Window
    {
        HttpClient client = new HttpClient();
        ProductsCollection _products = new ProductsCollection();
    
        public MainWindow()
        {
            InitializeComponent();
    
            client.BaseAddress = new Uri("http://localhost:9000");
            client.DefaultRequestHeaders.Accept.Add(
                new MediaTypeWithQualityHeaderValue("application/json"));
    
            this.ProductsList.ItemsSource = _products;
        }
    }
}

이 코드에서는 HttpClient의 새로운 인스턴스를 생성합니다. 그런 다음, 기본 URI를 "http://localhost:9000/"로 설정하고, 서버에게 데이터를 JSON 형식으로 전송할 것임을 알려주기 위해 Accept 헤더를 "application/json"으로 설정합니다.

또한, 새로운 ProductsCollection 클래스의 인스턴스를 생성한 다음, 이를 ListBox의 바인딩을 위해 설정하고 있다는 점에 주의하시기 바랍니다.

리소스 가져오기 (HTTP GET)

.NET 프레임워크 4.5를 사용하고 있다면 async 키워드와 await 키워드를 이용해서 손쉽게 비동기 코드를 작성할 수 있습니다.

노트: 만약, .NET 프레임워크 4.0과 Visual Studio 2012를 사용해서 개발하고 있다면, async/await 키워드 지원을 제공해주는 Async Targeting Pack을 설치할 수 있습니다.

다음 코드는 제품들의 목록을 가져오기 위한 API 질의를 수행합니다. 이 코드를 MainWindow 클래스에 추가합니다:

private async void GetProducts(object sender, RoutedEventArgs e)
{
    try
    {
        btnGetProducts.IsEnabled = false;
    
        var response = await client.GetAsync("api/products");
        response.EnsureSuccessStatusCode(); // 오류 코드를 던집니다.
    
        var products = await response.Content.ReadAsAsync<IEnumerable<Product>>();
        _products.CopyFrom(products);
    }
    catch (Newtonsoft.Json.JsonException jEx)
    {
        // 이 예외는 요청 본문을 역직렬화 할 때, 문제가 발생했음을 나타냅니다.
        MessageBox.Show(jEx.Message);
    }
    catch (HttpRequestException ex)
    {
        MessageBox.Show(ex.Message);
    }
    finally
    {
        btnGetProducts.IsEnabled = true;
    }
}

이 예제에서 GetAsync 메서드는 HTTP GET 요청을 전송합니다. HTTP 응답이 성공하면 제품들의 목록이 응답 본문에 JSON 형태로 담겨집니다. 이 목록을 파싱하려면 ReadAsAsync 메서드를 호출하면 되는데, 이 메서드는 응답 본문을 읽고 특정 CLR 형식으로 역직렬화를 시도합니다.

메서드들의 이름에서 짐작할 수 있듯이 GetAsyc 메서드와 ReadAsAsync 메서드는 비동기 메서드로, 이는 작업이 완료되기를 기다리지 않고 즉시 반환한다는 뜻입니다. 여기서, 주목해야 할 부분은 await 키워드인데, 이 키워드는 작업이 완료될 때까지 실행을 연기시켜줍니다. 가령:

var response = await client.GetAsync("api/products");

이 구문 이후에 작성되어 있는 코드들은 HTTP 요청이 완료될 때까지 실행되지 않습니다. 그러나, 이 얘기는 GetAsync 메서드가 완료되기를 기다리는 동안 이벤트 핸들러가 블록된다는 뜻이 아닙니다. 그와는 정 반대로 제어가 호출자에게 반환된다는 뜻입니다. 그리고, HTTP 요청이 완료되면, 그 때 다시 실행이 중단되었던 지점부터 재개됩니다.

메서드 내에서 await 키워드를 사용하려면, 반드시 메서드에 async 변경자를 지정해야만 합니다:

private async void GetProducts(object sender, RoutedEventArgs e)

반면, await 키워드를 사용하지 않으려면 다음과 같이 Task 개체의 ContinueWith 메서드를 호출해야만 합니다:

역주: 다음 코드를 그대로 사용하면 오류가 발생합니다. "api/products/2" 부분을 "api/products"로 변경하십시오.
private void GetProducts(object sender, RoutedEventArgs e)
{
    btnGetProducts.IsEnabled = false;
    
    client.GetAsync("api/products/2").ContinueWith((t) =>
    {
        if (t.IsFaulted)
        {
            MessageBox.Show(t.Exception.Message);
            btnGetProducts.IsEnabled = true;
        }
        else
        {
            var response = t.Result;
            if (response.IsSuccessStatusCode)
            {
                response.Content.ReadAsAsync<IEnumerable<Product>>().
                    ContinueWith(t2 =>
                        {
                            if (t2.IsFaulted)
                            {
                                MessageBox.Show(t2.Exception.Message);
                                btnGetProducts.IsEnabled = true;
                            }
                            else
                            {
                                var products = t2.Result;
                                _products.CopyFrom(products);
                                btnGetProducts.IsEnabled = true;
                            }
                        }, TaskScheduler.FromCurrentSynchronizationContext());
            }
    
        }
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

그러나, 이런 방식의 코드는 올바르게 작성하기가 매우 어렵기 때문에, .NET 4.5를 사용하여 개발하는 것이 가장 좋고, 그게 불가능하다면 Async Targeting Pack을 설치하는 것이 바람직합니다.

리소스 생성하기 (HTTP POST)

다시 MainWindow.xaml 파일로 돌아와서 새로운 제품을 생성하기 위한 UI를 일부 추가합니다:

역주: 아래의 XAML 코드를 그대로 붙여 넣은 다음, 컨트롤들의 레이아웃을 직접 지정해줘야 합니다. 그러지 않으면, 컨트롤들이 창 전체를 덮어 씌웁니다.
<Label FontWeight="ExtraBold">New Product</Label>
<Label>Name</Label>
<TextBox Name="textName"></TextBox>
<Label>Price</Label>
<TextBox Name="textPrice"></TextBox>
<Label>Category</Label>
<TextBox Name="textCategory"></TextBox>
<Button Name="btnPostProduct" Click="PostProduct">Post Product</Button>

그리고, MainWindow 클래스에 다음의 코드를 추가합니다.

역주: 다음 코드를 그대로 사용하면 오류가 발생합니다. 10번째 줄의 decimal.Parse() 메서드를 double.Parse() 메서드로 변경하십시오.
private async void PostProduct(object sender, RoutedEventArgs e)
{
    btnPostProduct.IsEnabled = false;
    
    try
    {
        var product = new Product()
        {
            Name = textName.Text,
            Price = decimal.Parse(textPrice.Text),
            Category = textCategory.Text
        };
        var response = await client.PostAsJsonAsync("api/products", product);
        response.EnsureSuccessStatusCode(); // 오류 코드를 던집니다.
    
        _products.Add(product);
    }
    catch (HttpRequestException ex)
    {
        MessageBox.Show(ex.Message);
    }
    catch (System.FormatException)
    {
        MessageBox.Show("Price must be a number");
    }
    finally
    {
        btnPostProduct.IsEnabled = true;
    }
}

이 코드는 JSON 형식의 Product 인스턴스를 담고 있는 POST 요청을 서버로 전송합니다. 여기에 사용된 PostAsJsonAsync 메서드는 System.Net.Http.HttpClientExtensions 클래스에 정의되어 있는 확장 메서드입니다. 내부적으로 이 메서드는 JSON 미디어 형식 포멧터를 사용해서 Product를 JSON으로 직렬화하고, 이를 요청 본문에 써줍니다. JSON 형식 대신 XML 형식을 사용하려면 PostAsXmlAsync 메서드를 사용하면 됩니다.