컨트롤러: 컨트롤러 액션으로 라우팅하기

등록일시: 2016-11-21 08:00,  수정일시: 2016-11-29 11:56
조회수: 12,560
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 전달받은 요청의 URL을 검토하고 액션과 매핑하는 ASP.NET Core MVC의 라우팅 기능이 제공해주는 다양한 장점과 특징, 그리고 사용방법을 알아봅니다.

ASP.NET Core MVC는 라우팅 미들웨어를 이용해서 전달받은 요청의 URL을 검토하고 액션과 매핑합니다. 라우트는 구동 코드나 어트리뷰트를 이용하여 정의되며, URL 경로와 액션이 연결되는 방식을 기술합니다. 라우트는 응답에 포함되어 전달되는 URL을 (즉, 링크를) 생성하는 용도로 사용되기도 합니다.

본문에서는 MVC와 라우팅 간의 상호 작용과, 일반적인 MVC 응용 프로그램에서 라우팅 기능을 활용하는 방법들을 살펴봅니다. 라우팅에 대한 보다 자세한 정보는 Routing 문서를 참고하시기 바랍니다.

라우팅 미들웨어 설정하기

프로젝트 루트에 위치한 Startup.cs 파일의 Configure 메서드를 살펴보면 다음과 비슷한 코드를 확인할 수 있습니다:

app.UseMvc(routes =>
{
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

이 코드의 UseMvc 메서드 호출 내부에서는 MapRoute 메서드를 이용해서 default라는 단일 라우트를 생성하고 있습니다. 대부분의 MVC 응용 프로그램들은 이 default 라우트와 비슷한 템플릿을 라우트로 사용하게 됩니다.

"{controller=Home}/{action=Index}/{id?}"라는 라우트 템플릿은 /Products/Details/5 등의 URL 경로와 일치하며, 경로의 토큰화를 통해서 { controller = Products, action = Details, id = 5 } 같은 라우트 값들을 추출합니다. 그에 따라 MVC는 ProductsController라는 이름의 컨트롤러를 찾아서 Details 액션을 실행하려고 시도하게 됩니다:

public class ProductsController : Controller
{
    public IActionResult Details(int id) { ... }
}

이번 예제에서 한 가지 유념해야 할 부분은, 액션이 실행될 때 모델 바인딩이 id = 5 값을 이용해서 id 매개변수의 값을 5로 설정해준다는 사실입니다. 보다 자세한 정보는 모델 바인딩 문서를 참고하시기 바랍니다.

이렇게 default 라우트를 사용할 경우:

routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

라우트 템플릿의:

  • {controller=Home} 부분은 Home을 기본 controller 값으로 정의하고,
  • {action=Index} 부분은 Index를 기본 action 값으로 정의하고,
  • {id?} 부분은 id를 선택적 값으로 정의합니다.

기본 라우트 매개변수나 선택적 라우트 매개변수로 정의된 세그먼트는 매칭을 위해서 URL 경로에 반드시 제공돼야 할 필요가 없습니다. 라우트 템플릿 구문에 대한 보다 자세한 설명은 Routing 문서를 참고하시기 바랍니다.

예를 들어서, "{controller=Home}/{action=Index}/{id?}" 라우트 템플릿은 URL 경로 /와 일치하며 { controller = Home, action = Index }라는 라우트 값들을 만들어내게 됩니다. 즉, controlleraction 값으로는 기본값이 사용되고, id는 해당하는 세그먼트가 URL 경로에 존재하지 않으므로 값이 생성되지 않습니다. MVC는 이 라우트 값들을 이용해서 HomeControllerIndex 액션을 선택하게 됩니다:

public class HomeController : Controller
{
    public IActionResult Index() { ... }
}

지금까지 살펴본 컨트롤러 정의와 라우트 템플릿을 사용할 경우, 다음과 같은 URL 경로들 중 한 가지가 요청되면 HomeController.Index 액션이 실행됩니다:

  • /Home/Index/17
  • /Home/Index
  • /Home
  • /

또한, 다음과 같이 UseMvcWithDefaultRoute 메서드를 사용하면:

app.UseMvcWithDefaultRoute();

간편하게 다음 코드를 대체할 수 있습니다:

app.UseMvc(routes =>
{
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

UseMvc 메서드와 UseMvcWithDefaultRoute 메서드는 모두 미들웨어 파이프라인에 RouterMiddleware의 인스턴스를 추가합니다. MVC는 미들웨어와 직접적으로 상호 작용하지 않으며, 라우팅을 통해서 요청을 처리합니다. MVC는 MvcRouteHandler의 인스턴스를 통해서 라우트와 연결됩니다. UseMvc 메서드의 내부 코드는 다음과 비슷하게 구현되어 있습니다:

var routes = new RouteBuilder(app);

// Add connection to MVC, will be hooked up by calls to MapRoute.
routes.DefaultHandler = new MvcRouteHandler(...);

// Execute callback to register routes.
// routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

// Create route collection and add the middleware.
app.UseRouter(routes.Build());

UseMvc 메서드는 직접적으로 어떠한 라우트도 정의하지 않으며, 어트리뷰트 라우트를 지원하기 위한 라우트 컬렉션의 자리 표시자(Placeholder)를 추가합니다. 재정의된 UseMvc(Action<IRouteBuilder>) 메서드를 사용하면 필요한 라우트를 추가하고 어트리뷰트 라우팅도 지원할 수 있습니다. UseMvc 메서드 및 모든 변형 메서드들은 내부적으로 어트리뷰트 라우트를 지원하기 위한 자리 표시자를 추가해주기 때문에, UseMvc 메서드의 구성 방식과는 무관하게 언제나 어트리뷰트 라우팅을 사용할 수 있습니다. UseMvcWithDefaultRoute 메서드는 기본 디폴트 라우트를 정의하고 어트리뷰트 라우팅에 대한 지원을 추가합니다. 어트리뷰트 라우팅에 대한 보다 자세한 내용들은 어트리뷰트 라우팅 절에서 살펴보겠습니다.

기본 라우팅

이전 절에서 살펴본 default 라우트는 기본 라우팅(Conventional Routing)의 한 예입니다:

역주

본문에서는 'Conventional Routing'을 '기본 라우팅'으로 번역합니다. 일반적으로 마이크로소프트의 기술문서에서는 'Conventional'을 '기본'으로 번역하는 경우가 많을 뿐만 아니라, 본문에서는 어트리뷰트 라우팅에 반대되는 개념으로서 '기본적인' 방식의 라우팅이란 의미도 내포하고 있습니다. 또한, 바로 다음 설명에서처럼 '규약(Convention)'으로서의 라우팅이라는 의미도 담고 있습니다.

routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

이런 형태의 라우팅을 기본(규약적) 라우팅이라고 부르는데, 그 이유는 다음과 같이 URL 경로에 대한 규약(Convention)을 규정하기 때문입니다:

  • 첫 번째 경로 세그먼트를 컨트롤러 이름에 매핑합니다.
  • 두 번째 경로 세그먼트를 액션 이름에 매핑합니다.
  • 세 번째 세그먼트는 모델 엔터티에 매핑되는 선택적 id에 사용됩니다.

default 라우트를 사용하면, /Products/List라는 URL 경로는 ProductsController.List 액션에 매핑되고, /Blog/Article/17이라는 URL 경로는 BlogController.Article 액션에 매핑됩니다. 이 매핑은 오직 컨트롤러와 액션의 이름을 기반으로 성립되며, 네임스페이스나 소스 파일의 위치, 혹은 메서드의 매개변수와는 아무런 상관이 없습니다.

기본 라우팅 방식을 이용해서 default 라우트를 정의해서 사용하면, 정의되는 각각의 액션들마다 새로운 URL 패턴을 고민하지 않고도 신속하게 응용 프로그램을 구축할 수 있습니다. 다수의 CRUD 형태의 액션들로 구성된 응용 프로그램의 경우, 컨트롤러들 간의 URL에 대한 일관성이 확보됨으로써 코드를 단순화시키고 보다 예상 가능한 UI를 만드는데 도움이 됩니다.

주의

이 라우트 템플릿에는 id가 선택적 값으로 정의되었으며, 이는 ID를 URL의 일부로 제공받지 않고도 액션이 실행될 수 있다는 뜻입니다. 일반적으로 URL에서 id가 생략될 경우, 모델 바인딩에 의해 값이 0으로 설정되므로, 결과적으로 데이터베이스에 id == 0을 만족하는 데이터가 존재하지 않는한 반환되는 엔터티가 존재하지 않습니다. 어트리뷰트 라우팅을 사용하면 일부 액션은 ID를 필수 값으로 설정하고 나머지 액션은 선택적 값으로 지정하는 등, 보다 세밀한 제어가 가능합니다. 따라서, 라우트를 올바르게 사용하기 위해서 id 같은 선택적 매개변수가 필요하다면, 관례에 따라 해당 매개변수에 대한 설명을 문서에 포함시켜야 합니다.

다중 라우트

다음과 같이 UseMvc 메서드 내부에 더 많은 MapRoute 메서드 호출을 추가해서 다중 라우트를 추가할 수 있습니다. 그러면 다중으로 규약을 정의하거나, 특정 액션 전용의 기본 라우팅를 추가할 수 있습니다:

app.UseMvc(routes =>
{
    routes.MapRoute("blog", "blog/{*article}",
           defaults: new { controller = "Blog", action = "Article" });
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

이 코드의 blog 라우트는 전용 기본 라우트(Dedicated Conventional Route)로, 이는 기본 라우팅 시스템을 이용하기는 하지만, 특정 액션 전용의 라우트라는 뜻입니다. 라우트 템플릿에 controlleraction이 매개변수로 지정되지 않았기 때문에 항상 기본값이 사용되며, 그 결과 이 라우트는 항상 BlogController.Article 액션으로만 매핑됩니다.

라우트 컬렉션의 라우트들은 순서를 갖고 있으며, 코드로 추가된 순서대로 처리됩니다. 따라서 이번 예제에서는 blog 라우트가 default 라우트보다 먼저 시도됩니다.

노트

전용 기본 라우트에서는 URL 경로의 나머지 부분을 캡처하기 위해서 {*article} 같은 포괄적 라우트 매개변수가 사용되는 경우가 많습니다. 그러나 이 방법은 라우트를 '너무 탐욕스럽게' 만들어서, 다른 라우트와 일치하도록 의도된 URL까지 이 라우트에 일치될 수도 있습니다. 이런 문제를 피하려면 '탐욕스러운' 라우트를 라우트 테이블의 뒷부분에 위치시키는 것이 좋습니다.

폴백

MVC는 요청 처리 과정의 일부로 라우트 값들이 응용 프로그램 내의 컨트롤러와 액션을 찾기 위해서 사용될 수 있는지를 확인합니다. 만약 라우트 값들이 액션과 일치하지 않으면, 해당 라우트는 일치하지 않는 것으로 간주되고, 다음 라우트가 시도됩니다. 이 과정을 폴백(Fallback)이라고 하며, 기본 라우트들이 겹치는 경우를 단순화시키기 위한 것입니다.

모호한 액션 해결하기

라우트가 동시에 두 가지 액션과 일치할 경우, MVC는 모호함을 제거하고 '최적'의 액션을 선택해야만 하며, 그렇지 않으면 예외가 던져집니다. 예를 들어서:

public class ProductsController : Controller
{
    public IActionResult Edit(int id) { ... }

    [HttpPost]
    public IActionResult Edit(int id, Product product) { ... }
}

이 예제 컨트롤러는 URL 경로 /Products/Edit/17 및 라우트 데이터 { controller = Products, action = Edit, id = 17 }와 일치하는 두 개의 액션을 정의하고 있습니다. 이 코드는 제품 편집 양식을 출력하는 Edit(int) 액션과 제출된 양식을 처리하는 Edit(int, Product) 액션으로 이루어진 MVC 컨트롤러의 보편적인 패턴을 보여줍니다. 이런 컨트롤러의 구현이 가능하려면 MVC가 HTTP POST로 요청이 전송된 경우에는 Edit(int, Product) 액션을 선택하고, 그 외의 다른 HTTP 동사가 사용된 경우에는 Edit(int) 액션을 선택할 수 있어야 합니다.

HttpPostAttribute ([HttpPost] 어트리뷰트)는 IActionConstraint 인터페이스의 구현으로, HTTP 동사가 POST인 경우에만 적용된 액션이 선택되도록 제약합니다. 이 IActionConstraint의 구현이 지정됐기 때문에 Edit(int, Product) 액션이 Edit(int) 액션보다 '최적'으로 일치하여, Edit(int, Product) 액션이 첫 번째로 시도됩니다. 보다 자세한 내용은 IActionConstraint 이해하기 절을 참고하시기 바랍니다.

일반적으로 IActionConstraint 인터페이스의 사용자 지정 구현은 특수한 시나리오에서만 작성하면 됩니다. 그러나 HttpPostAttribute 같은 어트리뷰트들의 역할을 이해하는 것은 매우 중요합니다. 참고로 다른 HTTP 동사들에 대해서도 비슷한 어트리뷰트들이 정의되어 제공됩니다. 기본 라우팅에서는 '양식 출력' -> '양식 제출' 같은 작업 흐름을 구성하는 액션들이 동일한 액션 이름을 사용하는 경우가 많습니다. 이런 패턴의 편리함은 URL 생성 절을 살펴보고 나면 더욱 여실하게 느껴질 것입니다.

만약 하나 이상의 라우트가 일치하지만, MVC가 '최적'의 라우트를 찾아내지 못한다면 AmbiguousActionException 예외가 던져지게 됩니다.

라우트 이름

다음 예제 코드에서 "blog""default"라는 문자열은 라우트 이름입니다:

app.UseMvc(routes =>
{
    routes.MapRoute("blog", "blog/{*article}",
        defaults: new { controller = "Blog", action = "Article" });
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

라우트 이름은 라우트에 논리적 이름을 부여하며, 이렇게 이름이 지정된 라우트는 URL 생성 시에 활용할 수 있습니다. 이는 라우트들의 순서로 인해서 복잡해질 수도 있는 URL 생성 작업을 극적으로 단순하게 바꿔줍니다. 라우트 이름은 응용 프로그램 수준에서 유일해야 합니다.

라우트 이름은 URL의 일치 여부나 요청 처리에 어떠한 영향도 주지 않으며, 오직 URL 생성 시에만 사용됩니다. Routing 문서에서 MVC 전용 헬퍼를 이용한 URL 생성을 비롯한 URL 생성에 관한 보다 자세한 정보를 살펴볼 수 있습니다.

어트리뷰트 라우팅

어트리뷰트 라우팅은 어트리뷰트들의 모음을 사용해서 액션을 라우트 템플릿과 직접 매핑합니다. 다음 예제 코드는 Configure 메서드에서 app.UseMvc();를 호출해서 아무런 라우트도 전달하지 않았다고 가정합니다. 그럼에도 불구하고 이 HomeControllerdefault 라우트 템플릿인 {controller=Home}/{action=Index}/{id?} 템플릿과 일치하는 URL들과 동일한 일련의 URL들과 일치하게 됩니다:

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    public IActionResult Index()
    {
        return View();
    }
    [Route("Home/About")]
    public IActionResult About()
    {
        return View();
    }
    [Route("Home/Contact")]
    public IActionResult Contact()
    {
        return View();
    }
}

가령 HomeController.Index() 액션은 /, /Home, 또는 /Home/Index URL 경로 중 하나가 요청되면 실행됩니다.

노트

이번 예제는 어트리뷰트 라우팅과 기본 라우팅 간의 핵심적인 프로그래밍 상의 차이점을 확연히 보여줍니다. 어트리뷰트 라우팅을 사용할 경우, 라우트를 지정하기 위해서 보다 많은 키 입력이 필요한 반면, 기본 디폴트 라우트를 사용하면 훨씬 간결하게 라우트를 처리할 수 있습니다. 그러나 어트리뷰트 라우팅을 사용하면 각 액션마다 어떤 라우트 템플릿을 적용할 것인지 정밀하게 제어할 수 있습니다 (또한 그래야만 합니다).

어트리뷰트 라우팅에서는 컨트롤러 이름과 액션 이름은 액션을 선택할 때 아무런 역할을 수행하지 않습니다. 다음 예제 코드는 이전 예제 코드와 동일한 URL들과 일치합니다:

public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    public IActionResult MyIndex()
    {
        return View("Index");
    }
    [Route("Home/About")]
    public IActionResult MyAbout()
    {
        return View("About");
    }
    [Route("Home/Contact")]
    public IActionResult MyContact()
    {
        return View("Contact");
    }
}

노트

위의 예제에 적용된 라우트 템플릿에서는 action, area, 그리고 controller에 대한 라우트 매개변수가 정의되지 않았습니다. 사실, 어트리뷰트 라우트에는 이런 라우트 매개변수들이 허용되지 않습니다. 라우트 템플릿이 이미 액션에 연결되어 있기 때문에, URL에서 액션의 이름을 파싱한다는 것은 이치에 맞지 않기 때문입니다.

어트리뷰트 라우팅은 HttpPostAttribute 같은 HTTP[Verb] 어트리뷰트를 통해서도 사용이 가능합니다. 이 유형의 모든 어트리뷰트들은 라우트 템플릿을 전달받을 수 있습니다. 다음 예제 코드는 동일한 라우트 템플릿과 일치하는 두 가지 액션을 보여줍니다:

[HttpGet("/products")]
public IActionResult ListProducts()
{
    // ...
}

[HttpPost("/products")]
public IActionResult CreateProduct(...)
{
    // ...
}

즉, /products라는 URL 경로에 대해서, HTTP 동사가 GET인 경우에는 ProductsApi.ListProducts 액션이 실행되고, HTTP 동사가 POST인 경우에는 ProductsApi.CreateProduct 액션이 실행됩니다. 어트리뷰트 라우팅은 먼저 라우트 어트리뷰트에 의해서 정의된 라우트 템플릿들의 모음을 대상으로 URL이 일치하는지 확인합니다. 만약 라우트 템플릿이 일치하면 IActionConstraint 제약이 적용되어 실행될 액션이 결정됩니다.

REST API를 구축할 경우, 액션 메서드에 [Route(...)] 어트리뷰트를 적용해야 하는 경우는 드뭅니다. 그보다는 API의 기능을 더 정확하게 기술하는 보다 구체적인 Http*Verb*Attributes를 사용하는 것이 좋습니다. REST API의 클라이언트는 어떤 경로 및 HTTP 동사가 특정 논리적 작업과 매핑되는지 알고 있다고 간주합니다.

어트리뷰트 라우트는 특정 액션에 적용되기 때문에, 라우트 템플릿 정의에서 필수 매개변수를 지정하기가 더 쉽습니다. 다음 예제의 경우, id 매개변수는 URL 경로에 필수로 지정되어야 합니다:

public class ProductsApiController : Controller
{
    [HttpGet("/products/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id) { ... }
}

ProductsApi.GetProducts(int) 액션은 /products/3 같은 URL 경로가 요청된 경우에는 실행되지만, /products 같은 경로가 요청된 경우에는 실행되지 않습니다. 라우트 템플릿과 관련 옵션들에 대한 전체 설명은 Routing 문서를 참고하시기 바랍니다.

이 라우트 어트리뷰트는 Products_List라는 라우트 이름을 함께 정의하고 있습니다. 라우트 이름은 특정 라우트에 기반한 URL을 생성할 때 사용됩니다. 라우트 이름은 라우팅의 URL 일치 확인 작업에는 아무런 영향도 미치지 않으며, 오직 URL 생성 시에만 사용됩니다. 라우트 이름은 응용 프로그램 수준에서 유일해야 합니다.

노트

이런 특징은 id 매개변수를 선택적으로({id?}) 정의하는 기본 디폴트 라우트와는 대조적입니다. 이는 /products/products/5를 서로 다른 액션으로 전달할 수 있는 것처럼, API를 보다 정확하게 지정할 수 있다는 장점을 갖고 있습니다.

라우트 결합하기

컨트롤러에 적용된 라우트 어트리뷰트는 개별 액션에 적용된 라우트 어트리뷰트와 결합되어, 어트리뷰트 라우팅 시 반복적인 작업을 줄여줍니다. 컨트롤러에 정의된 모든 라우트 템플릿은 액션들에 적용된 라우트 템플릿에 접두어로 덧붙여집니다. 컨트롤러에 라우트 어트리뷰트를 적용하는 것은 해당 컨트롤러의 모든 액션들이 어트리뷰트 라우팅을 사용하도록 만드는 결과를 가져옵니다.

[Route("products")]
public class ProductsApiController : Controller
{
    [HttpGet]
    public IActionResult ListProducts() { ... }

    [HttpGet("{id}")]
    public ActionResult GetProduct(int id) { ... }
}

이 예제 코드에서 URL 경로 /productsProductsApi.ListProducts 액션과 일치하고, URL 경로 /products/5ProductsApi.GetProduct(int) 액션과 일치합니다. 이 두 액션들은 모두 HTTP GET 동사로 요청된 경우에만 일치하는데, HttpGetAttribute가 적용되어 있기 때문입니다.

그러나 액션에 적용된 라우트 템플릿이 /로 시작하는 경우에는 컨트롤러에 적용된 라우트 템플릿과 결합되지 않습니다. 다음 예제 코드의 액션들은 기본 디폴트 라우트와 일치하는 URL 경로들과 동일한 URL 경로들의 모음과 일치합니다.

[Route("Home")]
public class HomeController : Controller
{
    [Route("")]      // Combines to define the route template "Home"
    [Route("Index")] // Combines to define the route template "Home/Index"
    [Route("/")]     // Does not combine, defines the route template ""
    public IActionResult Index()
    {
        ViewData["Message"] = "Home index";
        var url = Url.Action("Index", "Home");
        ViewData["Message"] = "Home index" + "var url = Url.Action; =  " + url;
        return View();
    }

    [Route("About")] // Combines to define the route template "Home/About"
    public IActionResult About()
    {
        return View();
    }   
}

어트리뷰트 라우트 순서 지정하기

정의된 순서대로 실행되는 기본 라우트와는 대조적으로, 어트리뷰트 라우팅은 트리를 구성하고 모든 라우트의 일치 여부를 동시에 확인합니다. 그 결과, 마치 라우트 항목들이 이상적인 순서로 배치되어 있는 것처럼 동작하며, 보다 제한적인 라우트가 보다 일반적인 라우트보다 먼저 실행될 기회를 갖게 됩니다.

이를테면, blog/search/{topic} 같은 라우트는 blog/{*article} 같은 라우트보다 상대적으로 제한적입니다. 논리적으로 볼 때, blog/search/{topic} 라우트가 기본적으로 먼저 '실행'되며, 이것이 유일한 합리적인 순서입니다. 반면 기본 라우팅을 사용하는 경우에는 개발자가 원하는 순서대로 라우트를 배치해야할 책임을 갖고 있습니다.

그러나 어트리뷰트 라우트도 프레임워크에서 제공되는 모든 라우트 어트리뷰트들에 존재하는 Order 속성을 이용해서 순서를 구성할 수 있습니다. 라우트는 오름차순으로 정렬된 Order 속성을 기준으로 처리됩니다. 기본 라우트 순서는 0입니다. 따라서 Order = -1로 라우트를 설정하면 순서가 지정되지 않은 다른 라우트들 보다 먼저 실행됩니다. 반대로 Order = 1로 라우트를 설정하면 기본 순서를 사용하는 라우트들 보다 나중에 실행됩니다.

불필요하게 Order 속성에 의존하는 것은 좋지 않습니다. 만약, 올바른 라우트를 위해서 URL 공간에 명시적인 순서 값이 필요하다면, 클라이언트까지 혼란스럽게 만들 수도 있습니다. 일반적인 어트리뷰트 라우팅에서는 URL 매칭을 통해서 올바른 라우트를 선택합니다. 만약, URL 생성 시 기본 순서가 문제가 된다면, Order 속성을 설정하는 것보다는 라우트 이름을 사용해서 문제를 해결하는 방법이 더 간단합니다.

라우트 템플릿 토큰 대체 ([controller], [action], [area])

어트리뷰트 라우트는 작업의 편의를 위해 토큰을 대괄호([, ]])로 감싸는 토큰 대체(Token Replacement) 기능을 제공해줍니다. [action], [area], 그리고 [controller] 토큰은 라우트가 정의된 액션에 대응하는 액션 이름, 영역 이름, 그리고 컨트롤러 이름 값으로 대체됩니다. 다음 예제 코드의 액션들은 주석에 설명된 URL 경로들과 일치합니다:

[Route("[controller]/[action]")]
public class ProductsController : Controller
{
    [HttpGet] // Matches '/Products/List'
    public IActionResult List() {
        // ...
    }

    [HttpGet("{id}")] // Matches '/Products/Edit/{id}'
    public IActionResult Edit(int id) {
        // ...
    }
}

토큰 대체는 어트리뷰트 라우트를 구성하는 마지막 단계에서 수행됩니다. 위의 예제 코드는 다음 코드와 동일하게 동작합니다:

public class ProductsController : Controller
{
    [HttpGet("[controller]/[action]")] // Matches '/Products/List'
    public IActionResult List() {
        // ...
    }

    [HttpGet("[controller]/[action]/{id}")] // Matches '/Products/Edit/{id}'
    public IActionResult Edit(int id) {
        // ...
    }
}

어트리뷰트 라우트는 상속되는 경우에도 결합이 가능합니다. 이런 특징은 토큰 대체와 함께 사용될 때 특히 강력한 결과를 이끌어냅니다.

[Route("api/[controller]")]
public abstract class MyBaseController : Controller { ... }

public class ProductsController : MyBaseController
{
    [HttpGet] // Matches '/api/Products'
    public IActionResult List() { ... }

    [HttpPost("{id}")] // Matches '/api/Products/{id}'
    public IActionResult Edit(int id) { ... }
}

토큰 대체를 어트리뷰트 라우트에 의해서 정의되는 라우트 이름에도 적용할 수 있습니다. 가령, [Route("[controller]/[action]", Name="[controller]_[action]")]와 같이 어트리뷰트를 지정하면 각각의 액션마다 유일한 라우트 이름이 생성됩니다.

다중 라우트

어트리뷰트 라우팅를 사용하면 동일한 액션에 접근할 수 있는 다중 라우트를 정의할 수 있습니다. 이 기능이 사용되는 가장 보편적인 용도는 다음 예제와 같이 디폴트 기본 라우트의 동작을 모방하는 것입니다:

[Route("[controller]")]
public class ProductsController : Controller
{
    [Route("")]     // Matches 'Products'
    [Route("Index")] // Matches 'Products/Index'
    public IActionResult Index()
}

다중 라우트 어트리뷰트가 컨트롤러에 지정되면, 각각의 해당 어트리뷰트가 액션 메서드에 지정된 각각의 라우트 어트리뷰트들과 결합됩니다.

[Route("Store")]
[Route("[controller]")]
public class ProductsController : Controller
{
    [HttpPost("Buy")]      // Matches 'Products/Buy' and 'Store/Buy'
    [HttpPost("Checkout")] // Matches 'Products/Checkout' and 'Store/Checkout'
    public IActionResult Buy()
}

다중 라우트 어트리뷰트들(IActionConstraint를 구현하는)이 액션에 지정되면, 각각의 액션 제약조건들이 해당 어트리뷰트가 정의하는 라우트 템플릿과 결합됩니다.

[Route("api/[controller]")]
public class ProductsController : Controller
{
    [HttpPut("Buy")]      // Matches PUT 'api/Products/Buy'
    [HttpPost("Checkout")] // Matches POST 'api/Products/Checkout'
    public IActionResult Buy()
}

이렇게 액션에 다중 라우트를 적용하는 방식이 강력하게 보일수도 있지만, 응용 프로그램의 URL 공간을 간단하고 명확하게 유지하는 편이 더 바람직합니다. 기존 클라이언트를 지원하기 위한 경우처럼, 반드시 필요한 경우에만 액션에 다중 라우트를 지정하시기 바랍니다.

IRouteTemplateProvider를 이용한 사용자 지정 라우트 어트리뷰트

프레임워크에서 제공되는 모든 라우트 어트리뷰트들은 ([Route(...)], [HttpGet(...)] 등) IRouteTemplateProvider 인터페이스를 구현하고 있습니다. MVC는 응용 프로그램 구동 시에 컨트롤러 클래스 및 액션 메서드들에 적용된 어트리뷰트들을 찾아서, 그 중 IRouteTemplateProvider 인터페이스를 구현하고 있는 어트리뷰트들을 이용해서 최초 라우트 집합을 구성합니다.

직접 IRouteTemplateProvider 인터페이스를 구현해서 필요한 라우트 어트리뷰트를 정의할 수도 있습니다. 각각의 IRouteTemplateProvider 인터페이스 구현을 통해서 사용자 지정 라우트 템플릿, 정렬, 그리고 이름으로 구성된 단일 라우트를 정의할 수 있습니다:

public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider
{
    public string Template => "api/[controller]";

    public int? Order { get; set; }

    public string Name { get; set; }
}

가령, 이 예제 코드의 어트리뷰트는 [MyApiController] 어트리뷰트가 적용되면 자동으로 Template 속성을 "api/[controller]"로 설정합니다.

응용 프로그램 모델을 이용한 사용자 지정 어트리뷰트 라우트

응용 프로그램 모델(Application Model)은 액션을 라우트하고 실행하기 위해서 MVC가 사용하는 모든 메타데이터와 함께 구동 시에 생성되는 개체 모델입니다. 응용 프로그램 모델은 라우트 어트리뷰트로부터 수집된 모든 데이터를 포함하고 있습니다 (IRouteTemplateProvider 인터페이스를 통해서). 필요한 경우, 구동 시에 응용 프로그램 모델을 수정하는 규약(Conventions)을 작성해서 라우팅 동작을 사용자 지정할 수 있습니다. 다음 코드는 응용 프로그램 모델을 이용해서 라우팅을 사용자 지정하는 간단한 예제를 보여줍니다.

using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Linq;
using System.Text;

public class NamespaceRoutingConvention : IControllerModelConvention
{
    private readonly string _baseNamespace;

    public NamespaceRoutingConvention(string baseNamespace)
    {
        _baseNamespace = baseNamespace;
    }

    public void Apply(ControllerModel controller)
    {
        var hasRouteAttributes = controller.Selectors.Any(selector =>
                                                selector.AttributeRouteModel != null);
        if (hasRouteAttributes)
        {
            // This controller manually defined some routes, so treat this 
            // as an override and not apply the convention here.
            return;
        }

        // Use the namespace and controller name to infer a route for the controller.
        //
        // Example:
        //
        //  controller.ControllerTypeInfo ->    "My.Application.Admin.UsersController"
        //  baseNamespace ->                    "My.Application"
        //
        //  template =>                         "Admin/[controller]"
        //
        // This makes your routes roughly line up with the folder structure of your project.
        //
        var namespc = controller.ControllerType.Namespace;

        var template = new StringBuilder();
        template.Append(namespc, _baseNamespace.Length + 1,
                        namespc.Length - _baseNamespace.Length - 1);
        template.Replace('.', '/');
        template.Append("/[controller]");

        foreach (var selector in controller.Selectors)
        {
            selector.AttributeRouteModel = new AttributeRouteModel()
            {
                Template = template.ToString()
            };
        }
    }
}

혼합 라우팅

MVC 응용 프로그램에서는 기본 라우팅과 어트리뷰트 라우팅을 섞어서 사용할 수 있습니다. 일반적으로 브라우저에 HTML 페이지를 제공하는 컨트롤러에서는 기본 라우팅을 사용하고, REST APIs를 제공하는 컨트롤러에서는 어트리뷰트 라우팅을 사용합니다.

액션은 기본 라우트나 어트리뷰트 라우트 중 한 가지를 사용하게 됩니다. 컨트롤러나 액션에 라우트를 지정하는 것은 어트리뷰트 라우팅을 사용하겠다는 의미가 됩니다. 어트리뷰트 라우트가 정의된 액션은 기본 라우팅을 통해서는 접근이 불가능하며, 그 반대 역시 마찬가지 입니다. 컨트롤러에 지정된 모든 라우트 어트리뷰트는 해당 컨트롤러의 모든 액션들이 어트리뷰트로 라우트 되도록 만듭니다.

노트

이 두 가지 유형의 라우팅 시스템을 구분하는 기준은 URL을 라우트 템플릿과 매치한 이후에 적용되는 처리입니다. 기본 라우팅의 경우, 매치 결과로 얻어진 라우트 값들을 이용해서 모든 기본 라우트 액션들의 조회 테이블에서 액션과 컨트롤러를 선택합니다. 반면, 어트리뷰트 라우팅의 경우에는 각 템플릿들이 이미 특정 액션과 연결되어 있으므로 더 이상의 조회가 필요 없습니다.

URL 생성

MVC 응용 프로그램에서는 라우팅의 URL 생성 기능을 이용해서 액션에 대한 URL 링크를 생성할 수 있습니다. URL 생성 기능을 활용하면 하드코딩 되는 URL들을 줄이면서, 더 견고하며 유지보수가 용이한 코드를 만들 수 있습니다. 이번 절에서는 MVC가 제공하는 URL 생성 기능을 집중적으로 살펴보고, 기초적인 URL 생성 작업 방법을 알아보겠습니다. URL 생성에 대한 더 자세한 설명은 Routing 문서를 참고하시기 바랍니다.

IUrlHelper 인터페이스는 URL을 생성하기 위한 MVC와 라우팅 간의 기반구조를 구성하는 핵심 요소입니다. 컨트롤러, 뷰, 그리고 뷰 구성 요소에서는 Url 속성을 통해서 IUrlHelper의 인스턴스를 사용할 수 있습니다.

다음 예제에서는 IUrlHelper 인터페이스를 구현하고 있는 Controller.Url 속성을 이용해서 다른 액션을 가르키는 URL을 생성하고 있습니다.

using Microsoft.AspNetCore.Mvc;

public class UrlGenerationController : Controller
{
    public IActionResult Source()
    {
        // Generates /UrlGeneration/Destination
        var url = Url.Action("Destination");
        return Content($"Go check out {url}, it's really great.");
    }

    public IActionResult Destination()
    {
        return View();
    }
}

만약 응용 프로그램이 default 기본 라우트를 사용하고 있다면, 이 예제 코드의 url 변수에 /UrlGeneration/Destination URL 경로 문자열이 값으로 할당됩니다. 이 URL 경로는 현재 요청으로부터 가져온 라우트 값들(주변값, Ambient Values)과 Url.Action 메서드에 전달된 값들을 조합한 결과를 이용해서, 라우트 템플릿의 해당 값을 대체하는 방식으로 라우팅에 의해 생성된 것입니다:

주변값: { controller = "UrlGeneration", action = "Source" }
Url.Action에 전달된 값: { controller = "UrlGeneration", action = "Destination" }
라우트 템플릿: {controller}/{action}/{id?}

결과값: /UrlGeneration/Destination

라우트 템플릿의 각 라우트 매개변수들은 이름이 일치하는 전달된 값이나 주변값으로 그 값이 대체됩니다. 값이 존재하지 않는 라우트 매개변수는 기본값이 존재할 경우 기본값이 사용되며, 선택적 매개변수인 경우에는 생략됩니다 (이번 예제의 id 매개변수처럼). 그러나 만약 필수 라우트 매개변수에 대응하는 값이 하나라도 존재하지 않으면 URL 생성은 실패하게 됩니다. 특정 라우트를 이용한 URL 생성이 실패할 경우, 모든 라우트들이 실패하거나 일치하는 라우트가 발견될 때까지 계속해서 다음 라우트가 시도됩니다.

Url.Action 예제는 기본 라우팅을 가정하고 있습니다. 그러나 어트리뷰트 라우팅을 사용하는 경우에도 내부 개념은 다소 다르지만 비슷한 방식으로 URL을 생성할 수 있습니다. 기본 라우팅에서는 라우트 값들을 이용해서 템플릿을 확장하며, 대부분 템플릿에 controlleraction에 대한 라우트 값들이 사용됩니다. 이 방식이 동작하는 이유는 라우팅에 의해 매치되는 URL들이 <>규약을 준수하기 때문입니다. 그러나 어트리뷰트 라우팅에서는 템플릿에 controlleraction에 대한 라우트 값을 사용할 수 없으며, 대신 사용할 템플릿을 찾는데 사용됩니다.

다음은 어트리뷰트 라우팅을 사용하는 예제를 보여줍니다:

// In Startup class
public void Configure(IApplicationBuilder app)
{
    app.UseMvc();
}
using Microsoft.AspNetCore.Mvc;

public class UrlGenerationController : Controller
{
    [HttpGet("")]
    public IActionResult Source()
    {
        var url = Url.Action("Destination"); // Generates /custom/url/to/destination
        return Content($"Go check out {url}, it's really great.");
    }

    [HttpGet("custom/url/to/destination")]
    public IActionResult Destination() {
        return View();
    }
}

MVC는 어트리뷰트 라우트를 사용하는 모든 액션들의 조회 테이블을 구성해서, URL 생성에 사용할 라우트 템플릿을 선택하기 위해 controlleraction 값들의 일치 여부를 확인합니다. 이번 예제에서는 custom/url/to/destination이라는 URL이 생성됩니다.

액션 이름을 이용한 URL 생성

Url.Action (IUrlHelper.Action) 및 관련된 모든 오버로드 버전의 메서드들은 모두 컨트롤러 이름과 액션 이름으로 생성할 링크를 지정한다는 개념을 기반으로 삼고 있습니다.

노트

Url.Action 메서드를 사용하면, controlleraction의 현재 라우트 값이 자동으로 지정됩니다. controlleraction의 값은 주변값메서드에 전달된 값 양쪽 모두에 존재합니다. Url.Action 메서드는 항상 actioncontroller의 현재 값을 사용하며 현재 액션으로 라우트되는 URL 경로를 생성합니다.

라우팅은 URL을 생성할 때, 직접 제공되지 않은 정보 값에 대해 주변값을 사용하려고 시도합니다. 가령, {a}/{b}/{c}/{d}와 같은 라우트를 사용하고 있고 주변값이 { a = Alice, b = Bob, c = Carol, d = David }라면, 모든 라우트 매개변수가 대응하는 값을 갖고 있기 때문에, 라우팅은 더 이상 추가적인 값 없이도 URL을 생성하기에 충분한 정보를 보유하고 있는 셈이 됩니다. 만약 이 상태에서 { d = Donovan }라는 값을 추가한다면, 주변값 중 { d = David }는 무시되고 Alice/Bob/Carol/Donovan이라는 URL 경로가 생성될 것입니다.

경고

URL 경로는 계층적입니다. 만약 위의 예제에서 { c = Cheryl }라는 값을 추가한다면, { c = Carol, d = David } 값이 모두 무시됩니다. 이 경우, 더 이상 d에 대한 값이 존재하지 않기 때문에 URL 생성에 실패하게 됩니다. 따라서 필요한 cd의 값을 모두 지정해야 합니다. 어쩌면 디폴트 라우트에서도 ({controller}/{action}/{id?}) 동일한 문제가 발생할지 모른다고 생각할 수도 있지만, Url.Action 메서드가 항상 명시적으로 controlleraction 값을 지정해주기 때문에 실제로 이런 상황을 접하게 될 가능성은 낮습니다.

Url.Action 메서드의 긴 오버로드 버전은 controlleraction 외에도 라우트 매개변수 값들을 전달할 수 있는 추가적인 라우트 값 개체를 전달 받습니다. 이를 살펴볼 수 있는 가장 보편적인 사례가 바로 Url.Action("Buy", "Products", new { id = 17 })와 같이 id가 사용되는 경우입니다. 라우트 값 개체는 일반적으로 관례에 따라 익명 형식 개체로 전달되지만, IDictionary<> 형식이나 평범한 .NET 개체(Plain Old .NET Object)를 사용할 수도 있습니다. 라우트 매개변수와 일치하지 않는 모든 추가적인 값들은 쿼리 문자열에 추가됩니다.

using Microsoft.AspNetCore.Mvc;

public class TestController : Controller
{
    public IActionResult Index()
    {
        // Generates /Products/Buy/17?color=red
        var url = Url.Action("Buy", "Products", new { id = 17, color = "red" });
        return Content(url);
    }
}

절대 URL을 생성하려면, protocol을 전달받는 오버로드 버전을 사용하면 됩니다: Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme)

라우트를 이용한 URL 생성

이전 절에서는 컨트롤러 및 액션의 이름을 전달해서 URL을 생성하는 코드를 살펴봤습니다. IUrlHelper 인터페이스는 Url.RouteUrl 메서드 및 그 오버로드 버전들도 추가로 제공해줍니다. 이 메서드들은 Url.Action 메서드와 비슷하지만, action이나 controller의 현재 값을 라우트 값에 복사하지는 않습니다. 가장 일반적인 사용법은 컨트롤러나 액션의 이름을 지정하지 않고, 라우트 이름을 전달해서 URL 생성에 사용할 라우트를 지정하는 것입니다.

using Microsoft.AspNetCore.Mvc;

public class UrlGenerationController : Controller
{
    [HttpGet("")]
    public IActionResult Source()
    {
        var url = Url.RouteUrl("Destination_Route"); // Generates /custom/url/to/destination
        return Content($"See {url}, it's really great.");
    }

    [HttpGet("custom/url/to/destination", Name = "Destination_Route")]
    public IActionResult Destination() {
        return View();
    }
}

HTML에서 URL 생성하기

IHtmlHelper 인터페이스는 <form> 요소를 생성하는 Html.BeginForm HtmlHelper 메서드와 <a> 요소를 생성하는 Html.ActionLink HtmlHelper 메서드를 제공해줍니다. 이 메서드들은 내부적으로 Url.Action 메서드를 이용해서 URL을 생성하며, 이 메서드와 비슷한 인자들을 전달 받습니다. 반면, 내부적으로 Url.RouteUrl 메서드를 사용하는 HtmlHelper 메서드로는 Html.BeginRouteForm 메서드와 Html.RouteLink 메서드가 있으며, 역시 비슷한 기능을 갖고 있습니다. 보다 자세한 내용은 HTML Helpers 문서를 참고하시기 바랍니다.

태그 헬퍼를 사용할 경우에는 form 태그 헬퍼와 <a> 태그 헬퍼를 사용해서 URL을 생성할 수 있습니다. 이 태그 헬퍼들은 내부적으로 IUrlHelper 인터페이스를 이용해서 구현되어 있습니다. 더 자세한 정보는 태그 헬퍼로 폼 관련 작업하기 문서를 참고하시기 바랍니다.

그 밖에도 뷰 내부에서 애드혹 URL을 생성하기 위해서는 IUrlHelper 인터페이스를 구현하고 있는 Url 속성을 이용해서 수행할 수 있습니다.

액션 결과에서 URL 생성하기

앞의 예제에서는 컨트롤러에서 IUrlHelper 인터페이스를 사용해서 URL을 생성하는 방법을 살펴봤지만, 컨트롤러에서 가장 일반적으로 사용되는 방법은 액션 결과의 일부로 URL을 생성하는 것입니다.

ControllerBaseController 기반 클래스는 다른 액션을 참조하는 액션 결과를 위한 편리한 메서드들을 제공해줍니다. 한 가지 일반적인 사용법은 사용자의 입력을 전달받은 다음 재전송하는 것입니다.

public Task<IActionResult> Edit(int id, Customer customer)
{
    if (ModelState.IsValid)
    {
        // Update DB with new details.
        return RedirectToAction("Index");
    }
}

이런 액션 메서드 팩터리 메서드들도 IUrlHelper 인터페이스의 메서드들과 비슷한 패턴을 따릅니다.

전용 기본 라우트에 대한 특수한 사례

기본 라우팅에서는 전용 기본 라우트라는 특별한 형태의 라우트를 사용할 수 있습니다. 가령 다음 코드에서 blog라고 이름이 지정된 라우트는 전용 기본 라우트입니다.

app.UseMvc(routes =>
{
    routes.MapRoute("blog", "blog/{*article}",
        defaults: new { controller = "Blog", action = "Article" });
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

이런 라우트 정의를 사용할 경우, Url.Action("Index", "Home")라는 메서드 호출은 default 라우트에 의해 /라는 URL을 생성하게 되는데, 생각하기에 따라서는 이 결과가 이상하게 느껴질 수도 있습니다. 다시 말해서, { controller = Home, action = Index }라는 라우트 값들이 blog 라우트를 이용해서 URL을 생성하기에 충분하다고 생각한다면, 그 결과는 /blog?action=Index&controller=Home이 되어야 할 것이기 때문입니다.

전용 기본 라우트는 URL 생성 시, 라우트가 "너무 탐욕스럽게" 동작하는 것을 방지하는, 대응하는 라우트 매개변수가 존재하지 않는 기본 값의 특별한 동작에 의존합니다. 이번 예제의 경우, 기본값은 { controller = Blog, action = Article }이며, controlleraction 중 어느 쪽도 라우트 매개변수로 사용되지 않았습니다. 라우팅이 URL 생성을 수행할 때는, 반드시 제공된 값이 기본값과 일치해야만 합니다. 따라서 제공된 { controller = Home, action = Index } 값과 { controller = Blog, action = Article } 값이 일치하지 않기 때문에 blog 라우트를 이용한 URL 생성은 실패하게 됩니다. 그런 다음, 라우팅은 폴백에 의해서 default를 시도하게 되고, 이 시도는 성공합니다.

영역

영역(Areas)은 관련된 기능들을 분리된 라우팅-이름공간(컨트롤러 액션에 대한)과 폴더 구조(뷰에 대한)의 그룹으로 분할 구성하기 위해서 사용되는 ASP.NET MVC의 기능입니다. 영역을 사용하면 한 응용 프로그램 내부에 서로 다른 영역에 존재하는 동일한 이름을 가진 여러 개의 컨트롤러들이 동시에 존재할 수 있습니다. 영역을 사용하면 controlleraction에 더해서 area라는 또 다른 라우트 매개변수가 추가되어, 라우팅에 대한 계층구조가 만들어집니다. 이번 절에서는 라우팅과 영역이 함께 상호작용하는 방법을 살펴봅니다. 영역을 뷰와 함께 사용하는 보다 자세한 방법은 영역 문서를 참고하시기 바랍니다.

다음 예제는 MVC가 디폴트 기본 라우트와 Blog라는 영역 이름을 가진 영역 라우트(Area Route)를 함께 사용하도록 구성합니다:

app.UseMvc(routes =>
{
    routes.MapAreaRoute("blog_route", "Blog",
        "Manage/{controller}/{action}/{id?}");
    routes.MapRoute("default_route", "{controller}/{action}/{id?}");
});

이 상태에서 /Manage/Users/AddUser라는 URL 경로를 매칭해보면, 첫 번째 라우트에 의해서 { area = Blog, controller = Users, action = AddUser }라는 라우트 값이 얻어집니다. 여기서 area 라우트 값은 area의 기본값에 의해서 만들어지는데, 사실상 위와 같이 MapAreaRoute 메서드로 만든 라우트는 다음 코드로 만든 라우트와 동일합니다:

app.UseMvc(routes =>
{
    routes.MapRoute("blog_route", "Manage/{controller}/{action}/{id?}",
        defaults: new { area = "Blog" }, constraints: new { area = "Blog" });
    routes.MapRoute("default_route", "{controller}/{action}/{id?}");
});

내부적으로 MapAreaRoute 메서드는 전달된 영역의 이름을 이용해서 area에 대한 기본값과 제약조건이 설정된 라우트를 생성하는데, 이번 예제의 경우에는 그 값은 Blog입니다. 그 결과, 설정된 기본값으로 인해서 해당 라우트는 항상 { area = Blog, ... } 값을 만들어내며, 제약조건으로 인해서 URL 생성 시 { area = Blog, ... } 값을 필수로 요구하게 됩니다.

기본 라우팅은 순서에 의존적입니다. 일반적으로 영역이 지정된 라우트는 영역이 지정되지 않은 라우트보다 제한적이므로 라우트 테이블에서 상대적으로 보다 앞부분에 위치해야 합니다.

이번 예제의 라우트 값은 다음 액션과 일치합니다:

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
    [Area("Blog")]
    public class UsersController : Controller
    {
        public IActionResult AddUser()
        {
            return View();
        }        
    }
}

이 컨트롤러 코드에 사용된 AreaAttribute는 해당 컨트롤러가 영역에 속해 있음을 나타내주는데, 여기에서는 Blog 영역에 컨트롤러가 속해 있다는 것을 말해줍니다. [Area] 어트리뷰트가 지정되지 않은 컨트롤러는 어떠한 영역에도 속하지 않으며, 그런 컨트롤러는 라우팅에서 area 라우트 값이 제공되는 경우에는 일치하지 않습니다. 가령, 다음 예제들 중에서 오직 첫 번째 컨트롤러만 { area = Blog, controller = Users, action = AddUser } 라우트 값과 일치합니다.

using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace1
{
    [Area("Blog")]
    public class UsersController : Controller
    {
        public IActionResult AddUser()
        {
            return View();
        }        
    }
}
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace2
{
    // Matches { area = Zebra, controller = Users, action = AddUser }
    [Area("Zebra")]
    public class UsersController : Controller
    {
        public IActionResult AddUser()
        {
            return View();
        }        
    }
}
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace3
{
    // Matches { area = string.Empty, controller = Users, action = AddUser }
    // Matches { area = null, controller = Users, action = AddUser }
    // Matches { controller = Users, action = AddUser }
    public class UsersController : Controller
    {
        public IActionResult AddUser()
        {
            return View();
        }
    }
}

노트

이번 예제 코드에서는 만전을 기하기 위해 각 컨트롤러의 네임스페이스까지 모두 제시하고 있습니다. 네임스페이스를 구분하지 않으면 컨트롤러들 간에 이름 충돌이 발생해서 컴파일러 오류가 만들어지기 때문입니다. 클래스의 네임스페이스는 MVC의 라우팅에 아무런 영향을 미치지 않습니다.

첫 번째와 두 번째 컨트롤러는 영역에 속해 있으며, area 라우트 값으로 각 컨트롤러가 속한 영역 이름이 제공되는 경우에만 일치합니다. 반면, 세 번째 컨트롤러는 어떠한 영역에도 속해 있지 않으며, 라우팅에서 area 값이 전달되지 않는 경우에만 일치합니다.

노트

존재하지 않는 값을 매칭할 때, area 값이 null 이거나 빈 문자열이면 area 값이 존재하지 않는 것으로 간주합니다.

영역 내부에서 액션을 실행하는 경우에는, area의 라우트 값을 URL 생성에 사용할 라우팅의 주변값으로 사용할 수 있습니다. 이는 다음 예제에서 볼 수 있는 것처럼 URL 생성 시에 기본적으로 영역이 자동으로 적용된다는 것을 의미합니다.

app.UseMvc(routes =>
{
    routes.MapAreaRoute("duck_route", "Duck",
        "Manage/{controller}/{action}/{id?}");
    routes.MapRoute("default", "Manage/{controller=Home}/{action=Index}/{id?}");
});
using Microsoft.AspNetCore.Mvc;

namespace MyApp.Namespace4
{
    [Area("Duck")]
    public class UsersController : Controller
    {
        public IActionResult GenerateURLInArea()
        {
            // Uses the 'ambient' value of area
            var url = Url.Action("Index", "Home"); 
            // returns /Manage
            return Content(url);
        }

        public IActionResult GenerateURLOutsideOfArea()
        {
            // Uses the empty value for area
            var url = Url.Action("Index", "Home", new { area = "" }); 
            // returns /Manage/Home/Index
            return Content(url);
        }
    }
}

역주

마지막 예제 코드의 두 액션에 작성된 반환값에 대한 주석이 서로 뒤바뀐 것으로 보입니다. 예제 프로젝트를 다운로드 받아서 직접 실행해보면 간단히 확인이 가능합니다.

IActionConstraint 이해하기

노트

이번 절은 프레임워크 내부에 대한 심층적인 설명과 MVC가 실행할 액션을 선택하는 방법을 살펴봅니다. 일반적인 응용 프로그램에서는 사용자 지정 IActionConstraint 인터페이스가 필요하지 않습니다.

비록 IActionConstraint 인터페이스에 친숙하지 않더라도 여러분은 이미 은연중 이 인터페이스를 사용해본 적이 있을 확율이 높습니다. [HttpGet] 어트리뷰트나 비슷한 다른 [Http-VERB] 어트리뷰트들이 액션 메서드의 실행을 제한하기 위해서 IActionConstraint 인터페이스를 구현하고 있기 때문입니다.

public class ProductsController : Controller
{
    [HttpGet]
    public IActionResult Edit() { }

    public IActionResult Edit(...) { }
}

디폴트 기본 라우트를 사용하고 있다고 가정할 경우, /Products/Edit라는 URL 경로는 { controller = Products, action = Edit }라는 값들을 만들어내며, 이 값들은 이 예제 코드의 두 액션 모두와 일치합니다. IActionConstraint 인터페이스의 관점에서 볼 때, 두 액션은 모두 라우트 데이터와 일치하므로 후보로 간주된다고 말할 수 있습니다.

HttpGetAttribute가 실행되면 Edit() 액션은 HTTP GET 동사에 대해서만 일치하고 다른 모든 HTTP 동사들에 대해서는 일치하는 않는 것으로 지정됩니다. 반면 Edit(...) 액션에는 아무런 제약조건도 정의되어 있지 않기 때문에 모든 HTTP 동사와 일치합니다. 결과적으로 POST 동사는 Edit(...) 액션에 대해서만 일치한다고 봐도 무방합니다. GET 동사의 경우에는 여전히 두 액션 모두와 일치하지만, IActionConstraint 인터페이스가 적용된 액션은 그렇지 않은 액션보다 항상 우선순위가 높은 것으로 간주됩니다. 따라서 두 액션이 모두 일치하더라도Edit() 액션에는 [HttpGet] 어트리뷰트가 지정되어 있기 때문에 더 적합한 것으로 판단되어 이 액션이 선택됩니다.

개념적으로 IActionConstraint 인터페이스는 오버로딩의 한 형태이긴 하지만, 동일한 이름을 가진 메서드들 간의 오버로딩이 아닌, 동일한 URL과 일치하는 액션들 간의 오버로딩입니다. 어트리뷰트 라우팅에서도 IActionConstraint 인터페이스가 사용되므로, 결과적으로 서로 다른 컨트롤러에 위치한 액션들이 동시에 후보로 간주될 수도 있습니다.

IActionConstraint 구현하기

가장 간단하게 IActionConstraint 인터페이스를 구현하는 방법은 System.Attribute를 상속받는 클래스를 생성한 다음, 이를 액션이나 컨트롤러에 적용하는 것입니다. MVC는 어트리뷰트의 형태로 적용된 모든 IActionConstraint 인터페이스들을 자동으로 검색합니다. 응용 프로그램 모델을 사용해서 제약조건을 적용할 수도 있는데, 아마도 이 방법이 제약조건이 적용되는 방법을 메타프로그램 할 수 있는 가장 유연한 방식일 것입니다.

다음 예제 코드는 라우트 데이터 중 국가 코드를 기반으로 액션을 선택하는 제약조건을 보여줍니다. 전체 예제는 GitHub에서 살펴보실 수 있습니다.

public class CountrySpecificAttribute : Attribute, IActionConstraint
{
    private readonly string _countryCode;

    public CountrySpecificAttribute(string countryCode)
    {
        _countryCode = countryCode;
    }

    public int Order
    {
        get
        {
            return 0;
        }
    }

    public bool Accept(ActionConstraintContext context)
    {
        return string.Equals(
            context.RouteContext.RouteData.Values["country"].ToString(),
            _countryCode,
            StringComparison.OrdinalIgnoreCase);
    }
}

액션의 제약조건을 어트리뷰트로 구현하기 위해서는 Accept 메서드를 구현해야 하고 제약조건이 실행될 '순서'를 결정해야 합니다. 이 예제 코드의 Accept 메서드에서는 country 라우트 값이 일치할 경우, 해당 액션이 일치함을 의미하는 true를 반환합니다. 이 어트리뷰트가 RouteValueAttribute와 다른 점은 어트리뷰트가 지정되지 않은 액션으로 폴백이 가능하다는 것입니다. 가령, 이 예제의 전체 코드는 특정 액션이 en-US로 정의된 상태에서 국가 코드로 fr-FR가 전달될 경우, [CountrySpecific(...)] 어트리뷰트가 적용되지 않은 보다 일반적인 컨트롤러로 폴백되는 것을 보여줍니다.

Order 속성은 제약조건이 포함될 단계(Stage)를 결정합니다. 액션 제약조건은 Order 속성에 기반한 그룹별로 실행됩니다. 예를 들어, 프레임워크에서 제공되는 모든 HTTP 메서드 어트리뷰트들은 동일한 Order 값을 사용하므로 모두 같은 단계에서 실행됩니다. 이런 단계들은 원하는 정책을 구현하기 위해서 필요한 만큼 얼마든지 존재할 수 있습니다.

Order 속성을 결정하는 요령 중 하나는 해당 제약조건이 HTTP 메서드보다 먼저 적용되야 하는지 여부를 생각해보는 것입니다. 낮은 값이 먼저 실행됩니다.