login register Sysop! about ME  
qrcode
    최초 작성일 :    2015년 04월 17일
  최종 수정일 :    2015년 04월 17일
  작성자 :    Taeyo
  편집자 :    Taeyo (김 태영)
  읽음수 :    14,150

강좌 목록으로 돌아가기

필자의 잡담~

안녕하세요, Taeyo입니다.

이번 컬럼은 살짝 긴데 그래도 생각보다는 금새 번역 및 정리해서 올리게 되었네요. Web API 2와 관련한 세번째 글로서 새로 등장한 어트리뷰트 라우팅에 대한 이야기랍니다. 앞서 말씀드렸다시피 이게 공식적으로 순서가 있는 상태가 아니라서 제가 알아서 판단해서 일단 시리즈처럼 번역해 올리고 있습니다.
본 번역문서는 ASP.NET 기술을 널리 알리고자 하는 개인적인 취지로 번역되어 제공되는 문서로, 원문을 비롯한 모든 저작권은 마이크로소프트사에 있습니다. 마이크로소프트사의 요청이 있을 경우, 언제라도 게시가 중단될 수 있습니다. 본 번역문서에는 오역이 포함되어 있을 수 있으며 주석도 번역자 개인의 견해일뿐입니다. 마이크로소프트사는 본 문서의 번역된 내용에 대해 일체의 보장을 하지 않습니다. 번역이 완료된 뒤에도 제품이 업그레이드 되거나 기능이 변경됨에 따라 원문도 변경되거나 보완되었을 수 있으므로 참고하시기 바랍니다.

라우팅(Routing)은 Web API가 URI를 액션에 연결하는 방법을 말한다. Web API 2 는 어트리뷰트 라우팅이라는 새로운 형식의 라우팅을 지원하는데, 이는 그 이름이 의미하는 것처럼 어트리뷰트를 사용하여 라우트를 정의할 수 있게 하는 방법이다. 어트리뷰트 라우팅은 여러분의 Web API 안에서 URI를 좀 더 세밀하게 조정할 수 있게 해준다. 예를 들자면 어떤 리소스의 계층구조를 나타내는 URI를 쉽게 생성할 수 있다는 것이다.

이전의 라우팅 즉, 규약(Convention) 기반의 라우팅도 여전히 완벽하게 지원된다. 게다가, 하나의 프로젝트에서 기존의 규약 기반 라우팅과 어트리뷰트 라우팅을 섞어 사용할 수도 있다.

이번 컬럼에서는 어트리뷰트 라우팅을 사용하게끔 설정하는 방법과 어트리뷰트 라우팅이 제공하는 다양한 옵션들에 대해서 설명할 예정이다. 이보다 더 구체적으로 어트리뷰트 라우팅을 사용하는 방법을 알고 싶다면 Create a REST API with Attribute Routing in Web API 2 컬럼을 살펴보기 바란다.

사전준비

Visual Studio 2013 혹은 Visual Studio Express 2013

필요한 패키지를 설치하려면 NuGet 패키지 관리자를 사용해야 한다. 비주얼 스튜디오의 [도구] 메뉴에서 [Nuget 패키지 관리자]를 선택하고 [패키지 관리자 콘솔]을 선택하자. 그리고 패키지 관리자 콘솔에서 다음의 명령을 입력하도록 하자.

Install-Package Microsoft.AspNet.WebApi.WebHost

왜 어트리뷰트 라우팅을 사용하는가?

Web API의 첫 번째 버전 즉, 1.0은 규약 기반의 라우팅(Convention-based routing)을 사용했었다. 규약 기반의 라우팅에서는 하나 이상의 라우트 템플릿을 정의해야 하는데, 이 템플릿은 보통 매개변수로 구성된 문자열이다. 이 경우, 프레임워크가 요청을 받게 되면 URI를 이러한 라우트 템플릿과 비교하여 검토하게 된다(이에 대한 좀 더 자세한 이야기는 Routing in ASP.NET Web API를 참고하자).

규약 기반 라우팅의 한 가지 장점은 템플릿들이 단일 위치에 정의되고 라우팅 규칙이 모든 컨트롤러에 걸쳐 일관되게 적용된다는 사실이다. 하지만 불행하게도 규약 기반의 라우팅은 특정한 URI 패턴은 지원하기가 어렵다는 문제가 있다. 사실 RESTful API에서는 이러한 특정 URI 패턴이 보편적임에도 말이다. 예를 들면, 어떤 리소스들은 종종 자식 리소스를 포함해야 하는데 즉, 고객 리소스는 주문정보를 포함해야 하고, 영화는 배우들을, 책은 저자들의 정보를 자식 리소스로 포함해야 하곤 한다는 것이다. 사실 이러한 관계를 반영하는 URI를 만드는 것은 자연스러운 일이다.

/customers/1/orders

하지만, 위와 같은 URI는 규약 기반의 라우팅을 사용해서는 만들기가 쉽지 않다. 애를 써서 간신히 만들었다 하더라도, 여러분이 수 많은 컨트롤러와 수 많은 리소스 유형들을 가지고 있다면 그 템플릿은 확장하여 적용하기가 거의 불가능하다.

하지만, 어트리뷰트 라우팅이라면 이러한 URI를 정의하는 것은 누워서 떡먹기이다. 단순히 컨트롤러 액션에 어트리뷰트를 추가하기만 하면 되니 말이다.

[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }

다음은 어트리뷰트 라우팅이 유용한 몇몇 패턴들을 보여준다.

API 버전 지정하기

다음의 "/api/v1/products"는 "/api/v2/products"와는 다른 컨트롤러로 라우트될 것이다.

/api/v1/products
/api/v2/products

재정의되는 URI 부분들

다음 예에서 1은 순번이지만 "pending"는 콜렉션에 매핑된다.

/orders/1
/orders/pending

다중 매개변수 유형들

다음 예에서 "1"은 순번이지만 "2013/06/16"는 날짜를 지정하고 있다.

/orders/1
/orders/2013/06/16

어트리뷰트 라우팅을 사용하도록 설정하기

어트리뷰트 라우팅을 사용하도록 설정하려면, 설정 작업(configuration) 동안에 MapHttpAttributeRoutes를 호출하면 된다. 이 확장 메서드는 System.Web.Http.HttpConfigurationExtensions클래스 안에 정의되어 있다.

using System.Web.Http;
namespace WebApplication
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API routes
            config.MapHttpAttributeRoutes();
            // Other Web API configuration not shown.
        }
    }
}

어트리뷰트 라우팅은 규약 기반의(convention-based) 라우팅과 혼용하여 사용할 수도 있다. 규약 기반의 라우트를 정의하려면 MapHttpRoute 메서드를 호출하면 된다.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Attribute routing.
        config.MapHttpAttributeRoutes();
        // Convention-based routing.
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Web API를 구성하는 것에 대한 자세한 정보는 Configuring ASP.NET Web API 2를 참고하도록 하자.

참고 : Web API 1에서의 마이그레이션

Web API 2 이전에는 Web API 프로젝트 템플릿이 다음과 같은 코드를 생성했었다.

protected void Application_Start()
{
    // WARNING - Not compatible with attribute routing.
    WebApiConfig.Register(GlobalConfiguration.Configuration);
}

그렇기에 어트리뷰트 라우팅을 사용하도록 설정한다면, 이 코드는 예외를 던질 것이다. 기존 Web API 프로젝트에서 어트리뷰트 라우팅을 사용하도록 업그레이드 하고 싶다면, 코드를 다음과 같이 변경하도록 하자.

protected void Application_Start()
{
    // Pass a delegate to the Configure method.
    GlobalConfiguration.Configure(WebApiConfig.Register);
}

더 자세한 정보는 Configuring Web API with ASP.NET Hosting를 참고하자.

라우트 어트리뷰트 추가하기

다음은 어트리뷰트를 사용하여 정의된 라우트의 예이다.

public class OrdersController : ApiController
{
    [Route("customers/{customerId}/orders")]
    [HttpGet]
    public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}

"customers/{customerId}/orders"라는 문자열은 라우트 용 URI 템플릿이다. Web API는 요청 URI를 이러한 템플릿과 비교하는 작업을 수행한다. 이번 예의 경우 "customers"와 "orders"는 글자 그대로 사용되며 "{customerId}"는 매개변수로 취급된다. 그렇기에, 다음 URI들은 이러한 템플릿과 매치된다.

  • http://localhost/customers/1/orders
  • http://localhost/customers/bob/orders
  • http://localhost/customers/1234-5678/orders

제약(constraints)을 사용하여 이러한 매칭에 제한을 줄 수도 있는데, 이 부분은 곧 다룰 예정이다.

라우트 템플릿 안의 "{customerId}" 매개변수 명칭과 API 메서드의 매개변수 이름(customerid)이 일치한다는 부분에 주목하자. Web API는 컨트롤러 액션을 호출하는 경우 라우트 매개변수에 대해 바인드 작업을 수행한다. 예를 들자면, URI가 http://example.com/customers/1/orders 일 경우, Web API는 "1"이라는 값을 액션 안에 있는 customerid 매개변수에 바인드한다는 것이다.

URI 템플릿은 여러 개의 매개변수를 가질 수도 있다.

[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }

라우트 어트리뷰트를 갖지 않는 모든 컨트롤러 메서드들은 규약 기반의 라우팅을 사용한다. 그렇기에 동일 프로젝트 안에서 두 가지 형식의 라우팅을 섞어서 사용할 수 있다.

HTTP 메서드들

또한 Web API는 요청의 HTTP 메서드(GET, POST 등)를 기반으로 액션을 선택한다. 기본적으로 Web API는 컨트롤러 메서드 이름의 앞 부분이 HTTP 메서드와 일치하는지를 확인한다(대,소문자 구분없이). 예를 들자면, 컨트롤러 메서드의 이름이 PutCustomers 라면 이는 HTTP PUT 요청과 매치된다는 것이다.

기본적으로는 그렇지만 다음과 같은 어트리뷰트를 메서드에 추가하여 기본 동작을 변경할 수도 있다.

  • [HttpDelete]
  • [HttpGet]
  • [HttpHead]
  • [HttpOptions]
  • [HttpPatch]
  • [HttpPost]
  • [HttpPut]

다음 예제는 CreateBook 메서드를 HTTP POST 요청에 연결하는 예이다.

[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }

비표준 HTTP 메서드를 포함하여 모든 그 밖의 HTTP 메서드를 사용하고자 한다면, 그런 메서드의 목록을 AcceptVerbs 어트리뷰트를 사용하여 지정하면 된다.

// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }

라우트 접두어

종종 컨트롤러 안에 있는 라우트들은 모두 동일한 접두어로 시작하곤 한다. 예를 들면 다음의 예에서는 모두 "api/books"로 시작하고 있음을 볼 수 있다.

public class BooksController : ApiController
{
    [Route("api/books")]
    public IEnumerable<Book> GetBooks() { ... }
    [Route("api/books/{id:int}")]
    public Book GetBook(int id) { ... }
    [Route("api/books")]
    [HttpPost]
    public HttpResponseMessage CreateBook(Book book) { ... }
}

이런 경우에는 [RoutePrefix] 어트리뷰트를 사용하여 전체 컨트롤러에 대한 공용 접두어를 지정할 수도 있다.

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET api/books
    [Route("")]
    public IEnumerable<Book> Get() { ... }
    // GET api/books/5
    [Route("{id:int}")]
    public Book Get(int id) { ... }
    // POST api/books
    [Route("")]
    public HttpResponseMessage Post(Book book) { ... }
}

메서드 어트리뷰트에 틸드(~)를 사용한다면 라우트 접두어를 재정의하는 것도 가능하다.

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET /api/authors/1/books
    [Route("~/api/authors/{authorId:int}/books")]
    public IEnumerable<Book> GetByAuthor(int authorId) { ... }
    // ...
}

라우트 접두어는 매개변수를 가질 수도 있다.

[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
    // GET customers/1/orders
    [Route("orders")]
    public IEnumerable<Order> Get(int customerId) { ... }
}

라우트 제약들

라우트 제약을 사용하면 라우트 템플릿 안에서 매개변수들이 매치되는 방식을 제어할 수 있다. 일반적인 구문은 "{parameter:constraint}"이다.

[Route("users/{id:int}"]
public User GetUserById(int id) { ... }
[Route("users/{name}"]
public User GetUserByName(string name) { ... }

상기 예에서 첫 번째 라우트는 URI의 "id" 요소가 integer인 경우에만 선택될 것이고 그 밖의 경우는 두 번째 라우트가 선택될 것이다.

다음 표는 지원되는 제약들의 목록이다.

제약설명
alpha대문자나 소문자 라틴 알파벳 문자와 매치된다(a-z, A-Z)){x:alpha}
boolBoolean 값과 매치된다{x:bool}
datetimeDateTime 값과 매치된다{x:datetime}
decimaldecimal 값과 매치된다{x:decimal}
double64비트 부동소수점 값과 매치된다{x:double}
float32비트 부동소수점 값과 매치된다{x:float}
guidGUID 값과 매치된다{x:guid}
int32비트 integer 값과 매치된다{x:int}
length지정된 길이를 갖는 문자열 혹은 지정된 길이 범위 안에 속하는 문자열과 매치된다{x:length(6)}
{x:length(1,20)}
long64비트 integer 값과 매치된다{x:long}
max최대 값을 갖는 integer와 매치된다{x:max(10)}
maxlength최대 길이를 갖는 문자열과 매치된다{x:maxlength(10)}
min최소 값을 갖는 integer와 매치된다{x:min(10)}
minlength최소 길이를 갖는 문자열과 매치된다{x:minlength(10)}
range값 범위 안에 속하는 integer와 매치된다{x:range(10,50)}
regex정규 표현식과 매치된다{x:regex(^\d{3}-\d{3}-\d{4}$)}

"min"과 같은 몇몇 제약들은 괄호 안에 인자를 갖는다는 점에 주목하자. 그리고, 여러 개의 제약들을 콜론으로 구분하여 매개변수에 적용할 수도 있는데 그 예는 다음과 같다.

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }

사용자 정의 라우트 제약

사용자 정의 라우트 제약은 IHttpRouteConstraint 인터페이스를 구현하면 작성할 수 있다. 예를 들면 다음의 제약은 매개변수가 0이 아닌 integer 값이도록 제약하는 예이다.

public class NonZeroConstraint : IHttpRouteConstraint
{
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, 
        IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            long longValue;
            if (value is long)
            {
                longValue = (long)value;
                return longValue != 0;
            }
            string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
            if (Int64.TryParse(valueString, NumberStyles.Integer, 
                CultureInfo.InvariantCulture, out longValue))
            {
                return longValue != 0;
            }
        }
        return false;
    }
}

이러한 제약을 등록하는 방법은 다음과 같다.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var constraintResolver = new DefaultInlineConstraintResolver();
        constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));
        config.MapHttpAttributeRoutes(constraintResolver);
    }
}

등록을 했다면 이제 여러분은 라우트에 이 제약을 적용할 수 있다.

[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }

더불어, IInlineConstraintResolver 인터페이스를 직접 구현하여 DefaultInlineConstraintResolver 클래스를 완전히 대체할 수도 있다. 다만, 이렇게 하게 되면 여러분이 작성한 IInlineConstraintResolver 구현을 명확하게 지정하지 않을 경우 내장되어 있는 모든 제약들이 대체될 수 있다.

선택적인 URI 매개변수들과 기본 값

라우트 매개변수에 물음표를 추가하여 URI 매개변수를 선택적인 것으로 만들 수도 있다. 만일, 라우트 매개변수가 선택적이라면 메서드 매개변수에는 반드시 기본 값을 정의해야 한다.

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int?}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}

상기 예제에서는 /api/books/locale/1033/api/books/locale이 동일한 리소스를 반환한다.

또는, 라우트 템플릿 안에서 다음과 같이 기본 값을 지정할 수도 있다.

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int=1033}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}

이는 앞의 예제와 거의 비슷하지만, 기본 값이 적용되었을 경우 약간 다르게 동작하는 부분이 있으며 그는 다음과 같다.

  • 첫 번째 예("{lcid?}")에서 기본 값 1033은 메서드 매개변수에 직접적으로 할당된다. 그렇기에 매개변수는 명확하게 이 값을 가질 것이다.
  • 두 번째 예("{lcid=1033}")에서 기본 값 "1033"은 모델 바인딩 처리를 거치게 된다. 기본적인 모델 바인더는 처리과정에서 문자열 "1033"을 숫자 값인 1033으로 변환할 것이다. 원한다면 사용자 정의 모델 바인더를 적용하여 약간 다르게 동작하도록 만들 수도 있다.

(요청처리 파이프라인 안에서 사용자 정의 모델 바인더를 사용하지 않는다면, 사실 대부분의 경우 두 방식은 결과적으로 동일하다고 볼 수 있다)

라우트 이름

Web API에서 모든 라우트는 이름을 갖는다. 라우트 이름은 링크를 생성하는 경우에 대단히 유용하며 이를 활용하여 HTTP 응답 안에 링크를 포함시킬 수 있다.

라우트 이름을 지정하려면 어트리뷰트에 Name 속성을 설정하면 된다. 다음 예제는 라우트 이름을 설정하는 방법과 링크를 생성할 때 라우트 이름을 사용하는 방법을 보여주고 있다.

public class BooksController : ApiController
{
    [Route("api/books/{id}", Name="GetBookById")]
    public BookDto GetBook(int id) 
    {
        // Implementation not shown...
    }
    [Route("api/books")]
    public HttpResponseMessage Post(Book book)
    {
        // Validate and add book to database (not shown)
        var response = Request.CreateResponse(HttpStatusCode.Created);
        // Generate a link to the new book and set the Location header in the response.
        string uri = Url.Link("GetBookById", new { id = book.BookId });
        response.Headers.Location = new Uri(uri);
        return response;
    }
}

라우트 순서

프레임워크가 URI와 라우트를 비교할 때, 이는 특정 순서에 따라 라우트들을 살펴보게 된다. 이러한 순서를 조정하려면 라우트 어트리뷰트에 RouteOrder 속성을 설정하면 된다. 낮은 값들이 먼저 검토되고, 기본 순서 값은 0이다.

다음은 전체 순서가 결정되는 방식이다.

  1. 라우트 어트리뷰트의 RouteOrder 속성을 비교한다.
  2. 라우트 템플릿 안에서 각각의 URI 요소들을 살펴보고, 각 요소에 대해서 다음에 따라 순서를 매긴다
    1. 문자열 요소
    2. 제약을 갖는 라우트 매개변수
    3. 제약이 없는 라우트 매개변수
    4. 제약을 갖는 와일드카드(*) 라우트 매개변수
    5. 제약이 없는 와일드카드(*) 라우트 매개변수
  3. 순서가 동일한 경우에는 라우트 템플릿의 대/소문자를 구분하지 않는 서수 문자열 비교(OrdinalIgnoreCase)에 의해서 순서가 결정된다.

다음은 예이다. 다음과 같은 컨트롤러가 있다고 가정해보자.

[RoutePrefix("orders")]
public class OrdersController : ApiController
{
    [Route("{id:int}")] // constrained parameter
    public HttpResponseMessage Get(int id) { ... }
    [Route("details")]  // literal
    public HttpResponseMessage GetDetails() { ... }
    [Route("pending", RouteOrder = 1)]
    public HttpResponseMessage GetPending() { ... }
    [Route("{customerName}")]  // unconstrained parameter
    public HttpResponseMessage GetByCustomer(string customerName) { ... }
    [Route("{*date:datetime}")]  // wildcard
    public HttpResponseMessage Get(DateTime date) { ... }
}

이 라우트들은 다음과 같이 순서가 매겨진다.

  1. orders/details
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. orders/pending

"details"는 문자열 요소이기에 "{id}" 보다 앞서 위치하게 된다. 반면, "pending"은 가장 마지막에 놓여져 있는데 그 이유는 그의 RouteOrder 속성 값이 1이기 때문이다.(이번 예제는 "details" 혹은 "pending"이라는 이름의 고객은 없는 것으로 가정하고 있다. 일반적으로, 애매모호한 라우트는 피하는 것이 좋기에 이번 예제의 GetByCustomer 대한 더 나은 라우트 템플릿은 "customers/{customerName}"이라 할 수 있다).


authored by

  itist
  2015-04-17(15:00)
캐릭 이미지
감사합니다!

 
 
.NET과 Java 동영상 기반의 교육사이트

로딩 중입니다...

서버 프레임워크 지원 : NeoDEEX
based on ASP.NET 3.5
Creative Commons License
{5}
{2} 읽음   :{3} ({4})