ASP.NET MVC 6 - 개발의 변화

마이크로소프트는 제품의 영향력에 비해 브랜딩이 취약하다는 힐난을 종종 받는다. ASP.NET 5에 대해서도 브랜딩에 대한 개발자들의 불평이 많다. ASP.NET 5 라는 작명을 보면 그 엄청난 변화에 대한 임팩트를 담지 못했고, 새로운 버전의 MVC 6를 표현할 때, 서로 다른 숫자에 의한 부자연스러움이 있기 때문이다.

이렇게 브랜딩 이야기를 먼저 꺼낸 것은 MVC 6도 그 밋밋한 이름에 비해 많은 변화가 있기 때문이다. 많은 변화 중에서 큰 의미를 갖는 몇 가지만 간추려서 알아보겠다.

Web API 통합

MVC 6 = MVC + Web API (+ Web Pages)

ASP.NET의 개발 모델 통합계획에는 원래 Web Pages도 포함되어 있었다고 한다. 이번 MVC 6 릴리즈에서는 Web API 만 통합이 이루어지지만 MVC의 향후 마이너 릴리즈 즈음에 Web Pages도 통합될 것으로 예상된다. MVC 6는 결국 Web API 3와 Web Pages 4를 포함하는 새로운 통합 모델이 될 것이다. 물론, Web API 3와 Web Pages 4 라는 버전은 차기 버전을 지칭하려는 의도일 뿐 실재하지 않는다.

MVC가 처음 소개될 당시에는 웹 폼(Web Forms) 개발에 영향을 끼치지 않는 것이 목표였다고 한다. MVC는 새로운 개발 모델이지만 기존 모델(웹 폼)과 ASP.NET 파이프라인 등 기초적인 기반을 공유하기 때문에 새 모델로 인한 System.Web의 변경은 조심스러울 수 밖에 없었다.

MVC 등장 2년 후에는 간단한 웹 애플리케이션을 빠르게 작성하고자 하는 목적의 Web Pages라는 개발 모델이 등장했다. MVC 버전 3에서 새롭게 소개된 Razor 뷰 엔진이 그 배경에 있다.

이 새로운 엔진은 정적인 HTML 페이지에 닷넷 프로그래밍 언어를 사용해 동적인 내용을 추가할 수 있는 기술로서 뷰에 서버 코드를 삽입할 수 있게 해준다. 과거에 클래식 ASP 페이지를 작성해본 경험이 있다면 그 것과 유사한 방식이라고 이해하면 된다. 이런 개발 모델은 하나의 웹 페이지에서 HTML, CSS, JavaScript와 서버 코드를 사용하기 때문에 초보자가 이해하기 쉽고, 또한 그 단순함 덕에 MVC로의 전환도 쉽다.

ASP.NET 개발 모델

<그림 1> 비슷하면서도 다른 ASP.NET의 개발 모델들

스마트폰, 태블릿의 확산으로 모바일 기기가 대중화되고 백엔드 서비스에 대한 수요가 증가하면서 UI 없이 데이터만 서비스하는 Web API가 등장한다. MVC 컨트롤러를 사용해서도 API 제공이 가능하지만 Web API 라는 다른 이름으로 각자의 길을 가게 된 역사적인 배경에는 호스팅 문제가 있었다.

API는 주목 받는 클라우드 환경등을 고려하여, 웹 서버에 독립적인 셀프 호스팅 시나리오를 지원해야 했다. 반면, MVC와 웹 폼을 포함하는 ASP.NET 프레임워크는 요청 처리와 인증 등의 문제로 IIS와 강하게 묶여 있었고 이런 프레임워크로는 셀프 호스팅 시나리오를 구현할 수 없었다. 따라서, MVC 프레임워크의 아키텍트와 상당 부분이 겹치는 중복을 감수하면서도 새로운 기반의 Web API가 탄생한다. Web API는 닷넷 매니지드 코드로 개발된 모든 프로그램에서 호스팅할 수 있도록 설계되었다.

MVC와 Web API가 같은 개념을 공유하면서도 MVC에서 개발한 필터를 Web API에 사용할 수 없다는 것은 상당히 아쉬운 문제였다. 한 프로젝트에서 MVC와 Web API를 함께 사용할 때, 라우트 설정을 각기 해야하고 종속성 주입도 서로 다른 장소에서 해야하는 것은 프로젝트의 구조를 복잡하게 만드는 요인이었다.

또한, 마이크로소프트 입장에서도 기능을 제공함에 있어 MVC와 Web API를 각각 지원하기 위해 두 번 구현해야 하는 비효율적인 문제도 있었다. 이번 통합은 많은 개발자들이 바랐던 ASP.NET 개발 모델의 통합이라는 점에서 의미가 크다.

MVC는 미들웨어

ASP.NET Core 미들웨어 라는 글에서 잠깐 다루었지만 MVC는 애플리케이션 역할을 하는 미들웨어다. 여기서 애플리케이션의 의미는 최종 응답을 처리하는 역할을 맡기 때문이다. 또한, MVC는 이제 일종의 종속성으로 별도 설치가 필요한 NuGet 패키지이다.

MVC를 사용하기 위해서는 project.json 파일에서 MVC에 대한 종속성을 선언함으로써 NuGet 패키지를 설치하고, Startup 클래스에서 미들웨어로 구성해야 한다.

<리스트 1> project.json 파일에 MVC 종속성 추가

"dependencies": { "Microsoft.AspNet.Server.IIS": "1.0.0-beta4", "Microsoft.AspNet.Server.WebListener": "1.0.0-beta4", "Microsoft.AspNet.Mvc": "6.0.0-beta4" }

<리스트 2> Startup.cs 파일에서 MVC를 사용하는 미들웨어 구성

public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerfactory) { app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller}/{action}/{id?}", defaults: new { controller = "Home", action = "Index" }); }); }

Startup.cs 파일에 있는 두 개의 메서드는 Startup 클래스의 핵심 메서드들로 런타임에 의해 ConfigureServices, Configure 순서대로 호출된다. ConfigureServices 메서드에서 필요한 서비스를 추가해 두고 Configure 메서드에서 사용 관련 설정을 한다.

설정을 마쳤으면, MVC 컨벤션에 따라 컨트롤러, 모델, 뷰를 추가하는 것으로 기존처럼 MVC 애플리케이션을 작성할 수 있다.

라우팅

라우트를 관리하는 방식에 개발자들의 호불호가 갈리고 있다. 이전 방식처럼 RouteConfig 클래스에서 중앙 집중형으로 관리하는 방식이 한 가지이고, 어트리뷰트 라우팅을 활용하여 컨트롤러와 액션에서 즉, 실제 사용처와 가장 가까운 곳에서 관리하는 방식이 다른 한 가지이다.

이전 버전의 MVC에서는 RouteConfig 클래스를 사용하여 라우팅을 중앙 관리했었는데 이 클래스가 없어졌다. 라우트를 구성하려면 <리스트 2>에서처럼 UseMvc 오버라이드 메서드를 사용해서 RouteConfig 클래스에서 했던 작업을 할 수 있다. 라우팅은 요청 파이프라인과 깊은 연관이 있기 때문에 미들웨어를 설정하는 시점에서 구성하는 것이 맥락상 들어 맞는다.

반면, 어트리뷰트 라우팅의 기능이 계속 확장되면서 라우팅 구성을 직관적으로 할 수 있게 되었다. controller, action 이라는 토큰이 추가되어 어트리뷰트 수준에서 템플릿을 유연하게 구성할 수 있고 컨트롤러에서 액션 메서드로 흐르는 상속 메커니즘도 구현할 수 있다.

<리스트 3> 어트리뷰트 라우팅

[Route("[controller]")] public class HomeController : Controller { [Route("[action]/{id:int?}")] public IActionResult Index(int? id) { return View(); } [Route("/welcome")] public IActionResult Hello(int? id) { return Content("Welcome"); } }

Index 액션 메서드는 /Home/Index/1 과 같은 URL 패턴에 대응하는데 여기서 [controller], [action] 토큰의 상속관계를 볼 수 있다. Hello 액션 메서드는 /welcome URL에 바로 대응하는데 이렇게 상속관계를 무시할 수도 있다.

어트리뷰트 라우팅의 기능이 많이 보강되면서 라우팅을 편하고 직관적으로 설정할 수 있게 되었지만, 사용자 정의 라우트 핸들러까지 지정할 정도로 복잡한 설정을 지원하지는 못한다.

뷰 컴포넌트

사용자 인터페이스를 개발하다 보면 Razor 태그들과 HTML 마크업으로 이루어진 동일한 코드를 애플리케이션 전체에 걸쳐 반복적으로 사용하게 되는 경우가 생긴다. 네비게이션 메뉴, 로그인 패널, 쇼핑 카트 등이 이런 경우에 해당한다. 이런 코드를 효율적으로 관리하기 위해 MVC 5에서는 부분 뷰(partial view)와 자식 액션(child action)을 사용했었다.

일반 뷰에서 부분 뷰를 사용하려면 다음과 같이 HTML 헬퍼를 통해 부분 뷰를 불러 들인다.

@Html.Partial(“MyPartial”)

이 헬퍼 메서드의 다른 오버라이드 메서드는 두 번째 인자로 모델 데이터를 받을 수 있어 부분 뷰로 데이터를 전달하여 출력할 수도 있다. 그러나, 이 방식은 호출하는 뷰에 데이터를 의존할 수 밖에 없는 단점이 있다.

데이터베이스와 상호작용이 필요한 경우, 보다 범용적인 솔루션으로 자식 액션을 사용한다. [ChildActionOnly] 라는 어트리뷰트로 액션 메서드를 수식하여 자식 액션을 정의 하는데, 뷰가 아닌 일반적인 요청에는 응답하지 않도록 제한하기 위해서다. 이렇게 정의한 자식 액션을 뷰에서는 아래와 같이 호출할 수 있다.

@Html.Action(“action_name”)

자식 액션이 다른 컨트롤러에 정의되어 있다면,

@Html.Action(“action_name”, “controller_name”)

처럼 컨트롤러를 지정해야 하므로, 자식 액션은 컨트롤러에 종속적인 면이 있다.
반면에 MVC 6에서 도입된 뷰 컴포넌트는 컨트롤러에 독립적으로 정의할 수 있는 미니 컨트롤러라고 생각할 수 있다.

<리스트 4> 뷰 컴포넌트 정의

public class FruitsViewComponent : ViewComponent { public IViewComponentResult Invoke() { var fruits = new List<string>(); fruits.Add("Apple"); fruits.Add("Pear"); fruits.Add("Cherry"); return View(fruits); } }

뷰 컴포넌트를 정의하는 방법은 1) ViewComponent 클래스를 상속하거나 2) [ViewComponent] 어트리뷰트로 클래스를 수식하거나 3) 'ViewComponent'로 끝나는 클래스명을 사용하여 정의한다. Invoke 메서드는 뷰에서 호출되는데 async 버전도 정의할 수 있는 점이 흥미롭다.

뷰 컴포넌트 클래스를 정의했으니 해당 뷰를 추가해야 하는데 이 때, 뷰를 추가하는 위치가 중요하다. 컨트롤러에 종속되지 않게 Views\Shared 폴더를 선택하고 Components 라는 폴더를 반드시 생성해야 한다. 그 하단에 'ViewComponent' 접미사를 제외한 뷰 컴포넌트의 클래스명(Fruits)으로 폴더를 한번 더 생성하고 Default.cshtml 이라는 이름으로 뷰를 추가한다.

<리스트 5> 뷰 컴포넌트 뷰 (Views\Shared\Components\Fruits\Default.cshtml )

{% highlight html %} @model IEnumerable

My Favorite Fruits

    @foreach(var fruit in Model) {
  • @fruit
  • }
```

이렇게 작성한 뷰 컴포넌트는 @Component.Invoke(“Fruits”) 라는 문장으로 어떤 뷰에서든지 호출할 수 있다.

뷰 컴포넌트를 더욱 강력하게 만들어 주는 기능으로 비동기 호출과 종속성 주입이 있다. 자세한 내용은 이 글의 범위를 넘어서니 View components and Inject in ASP.NET MVC 6 글을 참조하자.

마무리

MVC 6에서의 개발 모델 통합과 라우팅 그리고, 뷰 컴포넌트에 대해 알아 보았다. 이 외에도 TagHelers와 종속성 주입이 주목할 만한 주제인데 각각에 대해 심도있게 다루기 위해 별도의 글에서 상세히 소개하겠다.