ASP.NET Web API 예외 처리

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

본 문서에서는 ASP.NET Web API의 오류 및 예외 처리에 관해서 살펴봅니다.

HttpResponseException

Web API의 컨트롤러에서 처리되지 않은 예외가 발생하면 어떤 일이 벌어질까요? 대부분의 예외는 기본적으로 내부 서버 오류를 뜻하는 상태 코드 500 HTTP 응답으로 변환됩니다.

그러나, HttpResponseException 형식은 별개로 다뤄집니다. 이 예외 형식은 생성자에 지정한 HTTP 상태 코드를 반환합니다. 가령, 다음 메서드는 id 매개변수가 유효하지 않은 경우, HTTP 상태 코드 404, 즉 찾을 수 없음을 반환합니다.

public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    return item;
}

보다 세밀하게 응답을 제어하기 위해서, 응답 메시지 자체를 생성한 다음 HttpResponseException에 담을 수도 있습니다:

public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        var resp = new HttpResponseMessage(HttpStatusCode.NotFound)
        {
            Content = new StringContent(string.Format("No product with ID = {0}", id)),
            ReasonPhrase = "Product ID Not Found"
        }
        throw new HttpResponseException(resp);
    }
    return item;
}

예외 필터

다른 방법으로 예외 필터(Exception Filter)를 작성해서 Web API가 예외를 처리하는 방식을 직접 지정할 수도 있습니다. 예외 필터는 컨트롤러 메서드에서 HttpResponseException제외한 모든 유형의 처리되지 않은 예외가 던져지면 실행됩니다. HttpResponseException 형식은 특별하게 취급되는데, 그 이유는 애초에 이 예외가 HTTP 응답 반환을 위해 설계되었기 때문입니다.

예외 필터를 작성하려면 System.Web.Http.Filters.IExceptionFilter 인터페이스를 구현해야 합니다. 예외 필터를 구현하는 가장 간단한 방법은 System.Web.Http.Filters.ExceptionFilterAttribute 클래스를 상속 받아서 OnException 메서드를 재정의 하는 것입니다.

노트: ASP.NET Web API의 예외 필터와 ASP.NET MVC의 예외 필터는 일견 매우 비슷합니다. 그러나, 이 둘은 서로 다른 네임스페이스에 선언되어 있을 뿐만 아니라 기능 자체도 다릅니다. 가령, MVC의 예외 필터에 사용되는 HandleErrorAttribute 클래스는 Web API 컨트롤러에서 던져지는 예외를 처리하지 않습니다.

다음은 NotImplementedException 예외를 HTTP 상태 코드 501, 구현되지 않음으로 변환해주는 필터의 예제입니다:

namespace ProductStore.Filters
{
    using System;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http.Filters;
    
    public class NotImplExceptionFilterAttribute : ExceptionFilterAttribute
    {
        public override void OnException(HttpActionExecutedContext context)
        {
            if (context.Exception is NotImplementedException)
            {
                context.Response = new HttpResponseMessage(HttpStatusCode.NotImplemented);
            }
        }
    }
}

여기서 HttpActionExecutedContext 개체의 Response 속성은 클라이언트로 전송될 HTTP 응답 메시지를 담고 있습니다.

예외 필터 등록하기

Web API 예외 필터를 등록할 수 있는 방법은 다음과 같습니다:

  1. 액션에 등록
  2. 컨트롤러에 등록
  3. 전역 등록

특정 액션에 예외 필터를 적용하려면 필터를 액션에 어트리뷰트로 추가합니다:

public class ProductsController : ApiController
{
    [NotImplExceptionFilter]
    public Contact GetContact(int id)
    {
        throw new NotImplementedException("This method is not implemented");
    }
}

컨트롤러에 존재하는 모든 액션에 필터를 적용하려면 필터를 컨트롤러 클래스에 어트리뷰트로 추가합니다:

[NotImplExceptionFilter]
public class ProductsController : ApiController
{
    // ...
}

모든 Web API 컨트롤러에 전역으로 필터를 적용하려면 GlobalConfiguration.Configuration.Filters 컬렉션에 예외 필터의 인스턴스를 추가합니다. 이 컬렉션에 존재하는 예외 필터들은 모든 Web API 컨트롤러 액션에 적용됩니다.

GlobalConfiguration.Configuration.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());

만약, 프로젝트를 "ASP.NET MVC 4 웹 응용 프로그램" 프로젝트 템플릿을 선택해서 생성했다면, 다음과 같이 App_Start 폴더에 위치해 있는 WebApiConfig 클래스에 Web API 구성 코드를 추가하면 됩니다:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());
    
        // 다른 구성 코드들...
    }
}

HttpError

HttpError 개체는 응답 본문에 오류 정보를 반환할 수 있는 일관된 방법을 제공해줍니다. 가령, 다음 예제는 HttpError 개체를 이용해서 HTTP 상태 코드 404, 찾을 수 없음을 응답 본문에 반환하는 방법을 보여줍니다:

public HttpResponseMessage GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        var message = string.Format("Product with id = {0} not found", id);
        HttpError err = new HttpError(message);
        return Request.CreateResponse(HttpStatusCode.NotFound, err);
    }
    else
    {
        return Request.CreateResponse(HttpStatusCode.OK, item);
    }
}

이 예제의 메서드는 조회에 성공하면 제품 자체를 HTTP 응답으로 반환합니다. 그러나, 요청된 제품을 찾을 수 없다면 HTTP 응답의 응답 본문에 HttpError가 포함될 것입니다. 그리고, 그 응답은 아마도 다음과 비슷한 형태를 갖고 있을 것입니다:

HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Date: Thu, 09 Aug 2012 23:27:18 GMT 
Content-Length: 51
    
{
  "Message": "Product with id = 12 not found"
}

이 때, HttpError가 JSON 포멧으로 직렬화 된 상태라는 점에 주목하시기 바랍니다. HttpError를 사용할 때 얻을 수 있는 이점 중 하나가 바로 이것인데, 다른 모든 강력한 형식의 모델과 동일하게 내용 협상 (Content-Negotiation) 및 직렬화 처리가 수행된다는 점입니다.

또는 HttpError 개체를 직접 생성하는 대신, CreateErrorResponse 메서드를 사용할 수도 있습니다:

public HttpResponseMessage GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        var message = string.Format("Product with id = {0} not found", id);
        return Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
    }
    else
    {
        return Request.CreateResponse(HttpStatusCode.OK, item);
    }
}

CreateErrorResponse 메서드는 System.Net.Http.HttpRequestMessageExtensions 클래스에 정의되어 있는 확장 메서드입니다. 내부적으로 CreateErrorResponse 메서드는 HttpError의 인스턴스를 생성한 다음, 그 HttpError 개체를 포함하는 HttpResponseMessage 개체를 생성해줍니다.

HttpError 및 모델 유효성 검사

모델의 유효성 검사를 위해서 모델의 상태를 CreateErrorResponse에 전달해서 유효성 검사 오류 내용들을 응답에 포함시킬 수도 있습니다:

public HttpResponseMessage PostProduct(Product item)
{
    if (!ModelState.IsValid)
    {
        return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
    }
    
    // 구현 생략...
}

이 예제는 다음과 같은 응답 형태를 갖게 될 것입니다:

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 320
    
{
  "Message": "The request is invalid.",
  "ModelState": {
    "item": [
      "Required property 'Name' not found in JSON. Path '', line 1, position 14."
    ],
    "item.Name": [
      "The Name field is required."
    ],
    "item.Price": [
      "The field Price must be between 0 and 999."
    ]
  }
}

모델 유효성 검사에 대한 더 자세한 정보는 Model Validation in ASP.NET Web API를 참고하시기 바랍니다.

HttpError에 사용자 정의 키-값 쌍 추가하기

지금까지 살펴본 HttpError 클래스는 키-값 구조를 가진 컬렉션(Dictionary<string, object>에서 파생된)일 뿐입니다. 따라서, 얼마든지 자신이 원하는 키-값 쌍을 추가할 수 있습니다:

public HttpResponseMessage GetProduct(int id)
{
    Product item = repository.Get(id);
    
    if (item == null)
    {
        var message = string.Format("Product with id = {0} not found", id);
        var err = new HttpError(message);
        err["error_sub_code"] = 42;
        return Request.CreateErrorResponse(HttpStatusCode.NotFound, err);
    }
    else
    {
        return Request.CreateResponse(HttpStatusCode.OK, item);
    }
}

HttpError와 HttpResponseException을 동시에 사용하기

직전 예제는 액션에서는 HttpResponseMessage를 반환하고 있지만 HttpResponseException을 사용해서 HttpError를 반환할 수도 있습니다. 이 방식을 사용하면 작업을 성공한 일반적인 경우에는 강력한 형식의 모델을 반환하고, 오류가 발생한 경우에는 지금까지 살펴본 것처럼 HttpError를 반환할 수도 있습니다:

public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        var message = string.Format("Product with id = {0} not found", id);
        throw new HttpResponseException(
            Request.CreateErrorResponse(HttpStatusCode.NotFound, message));
    }
    else
    {
        return item;
    }
}
역주: 직전 예제와 이번 예제의 반환 형식을 비교해보시기 바랍니다.