ASP.NET MVC 6에서 Web API 작성하기
- 본 번역문서의 원문은 Create a Web API in MVC 6 www.asp.net 입니다.
ASP.NET 5.0의 중요한 목표 중 하나는 MVC 프레임워크와 Web API 프레임워크를 통합하는 것입니다.
본문에서는 다음과 같은 내용들을 살펴봅니다:
- ASP.NET MVC 6를 이용해서 간단한 Web API를 작성하는 방법.
- Empty 프로젝트 템플릿을 기반으로 응용 프로그램을 생성하고 필요한 구성 요소를 추가하는 방법.
- ASP.NET 5.0 파이프라인을 구성하는 방법.
- 응용 프로그램을 IIS에 의존하지 않고 자체-호스트 방식으로 실행하는 방법.
본문의 예제에서는 ASP.NET MVC 6 Web API 응용 프로그램을 빈 프로젝트 상태에서부터 한 단계씩 구현해봅니다. 반면, Starter Web 템플릿을 이용해서 작업을 시작할 수도 있는데, 이 템플릿에는 MVC 6, 인증, 로깅, 기타 다른 기능들에 대한 구성을 비롯해서 예제 컨트롤러와 뷰도 함께 포함되어 있습니다. 상황에 따라 이 두 가지 접근 방식 중 어떤 방식을 선택하더라도 무방합니다.
빈 ASP.NET 5 프로젝트 생성하기
먼저 Visual Studio 2015 Preview를 실행하고, 파일(File) 메뉴에서 새로 만들기(New) > 프로젝트(Project)를 선택합니다.
그러면 새 프로젝트(New Project) 대화 상자가 나타나는데, 템플릿(Templates) > Visual C# > 웹(Web)을 선택하고 ASP.NET 웹 응용 프로그램(ASP.NET Web Application) 프로젝트 템플릿을 선택한 다음, 프로젝트 이름을 "TodoApi"로 지정하고 확인(OK) 버튼을 클릭합니다.
계속해서 New ASP.NET Project 대화 상자에서 "ASP.NET 5 Empty" 템플릿을 선택하고 확인(OK) 버튼을 클릭합니다.
다음 스크린 샷은 이렇게 생성된 프로젝트의 구조를 보여줍니다.
생성된 프로젝트에는 다음과 같은 파일들이 만들어져 있습니다:
- global.json 파일은 솔루션 수준의 설정을 담고 있으며, 프로젝트와 프로젝트 간의 참조를 가능하게 해줍니다.
- project.json 파일은 해당 프로젝트의 설정을 담고 있습니다.
- Project_Readme.html 파일은 추가 정보 파일입니다.
- Startup.cs 파일에는 구동(Startup) 및 구성 코드를 작성하게 됩니다.
이 중, Startup.cs 파일에 정의되어 있는 Startup
클래스는 ASP.NET 요청 파이프라인을 구성합니다.
Empty 프로젝트 템플릿을 이용해서 프로젝트를 생성한 경우, Startup
클래스는 템플릿의 이름 그대로 파이프라인에 아무 것도 추가하지 않은 채 시작됩니다:
public class Startup { public void Configure(IApplicationBuilder app) { // Nothing here! } }
이 상태에서도 응용 프로그램을 실행할 수는 있지만, 아직 아무런 기능도 구현되어 있지 않은 상태입니다. 본문을 진행해나가면서 단계별로 기능들을 추가해보도록 하겠습니다. 이에 반해, "Starter Web" 템플릿을 선택한 경우에는 MVC 6, Entity Framework, 인증, 로깅 등 프레임워크의 다양한 부분들이 미리 구성됩니다.
환영 페이지 추가하기
이번에는 project.json 파일을 엽니다.
이 파일에는 응용 프로그램에 대한 프로젝트 설정이 담겨 있습니다.
가령 dependencies
섹션에는 필요한 NuGet 패키지들과 클래스 라이브러리들의 목록이 지정되어 있습니다.
여기에 Microsoft.AspNet.Diagnostics 패키지를 추가합니다:
"dependencies": { "Microsoft.AspNet.Server.IIS": "1.0.0-beta1", // Add this: "Microsoft.AspNet.Diagnostics": "1.0.0-beta1" },
패키지 이름을 한 글자씩 입력할 때마다 Visual Studio가 사용 가능한 패키지들의 목록을 인텔리센스로 제공해주는 것을 알 수 있습니다.
또한 패키지의 버전 역시 인텔리센스로 함께 제공됩니다:
이번에는 Startup.cs 파일을 열고 다음과 같이 코드를 추가합니다:
using System; using Microsoft.AspNet.Builder; namespace TodoApi { public class Startup { public void Configure(IApplicationBuilder app) { // New code app.UseWelcomePage(); } } }
작업을 마쳤으면 F5 키를 눌러서 디버깅을 시작합니다. 그러면, Visual Studio가 브라우저를 실행하고 http://localhost:port/로 이동하는데, 여기서 port 부분에는 Visual Studio가 할당한 임의의 포트 번호가 사용됩니다. 다음과 같은 환영 페이지가 나타날 것입니다:
이 환영 페이지는 응용 프로그램 코드를 작성하지 않고 무언가 실행되는 모습을 확인할 수 있는 가장 간단한 방법입니다.
Web API 작성하기
이번 절에서는 ToDo 항목들의 목록을 관리하기 위한 Web API를 작성해보겠습니다. 그러기 위해서는, 우선 응용 프로그램에 ASP.NET MVC 6를 추가해야 합니다.
먼저 project.json 파일의 의존성 목록에 MVC 6 패키지를 추가합니다:
"dependencies": {
"Microsoft.AspNet.Server.IIS": "1.0.0-beta1",
"Microsoft.AspNet.Diagnostics": "1.0.0-beta1",
// New:
"Microsoft.AspNet.Mvc": "6.0.0-beta1"
},
그런 다음, MVC를 요청 파이프라인에 추가합니다. Startup.cs 파일에
-
Microsoft.Framework.DependencyInjection
에 대한using
구문을 추가합니다. -
그리고
Startup
클래스에 다음의 메서드를 추가합니다.public void ConfigureServices(IServiceCollection services) { services.AddMvc(); }
이 코드는 MVC 6에 필요한 모든 의존성들을 추가해주는데, 구동 시에 프레임워크가 자동적으로 이
ConfigureServices
메서드를 호출하게 됩니다. -
마지막으로 Configure 메서드에 다음 코드를 추가합니다.
이
UseMvc
메서드는 파이프라인에 MVC 6를 추가합니다.public void Configure(IApplicationBuilder app) { // New: app.UseMvc(); }
다음은 모든 작업이 완료된 Startup
클래스입니다:
using System; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; // New using: using Microsoft.Framework.DependencyInjection; namespace TodoApi { public class Startup { // Add this method: public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } public void Configure(IApplicationBuilder app) { // New: app.UseMvc(); app.UseWelcomePage(); } } }
모델 추가하기
모델(model)은 응용 프로그램에서 도메인 데이터를 표현하는 클래스입니다. 이 예제에서 모델은 ToDo 항목입니다. 따라서 프로젝트에 다음의 클래스를 추가합니다:
using System.ComponentModel.DataAnnotations; namespace TodoApi.Models { public class TodoItem { public int Id { get; set; } [Required] public string Title { get; set; } public bool IsDone { get; set; } } }
본문에서는 프로젝트를 구조적으로 유지하기 위해, 먼저 Models 폴더를 생성하고 이 폴더에 모델 클래스를 작성하고 있습니다. 그러나, 이 규약을 반드시 따라야만 하는 것은 아닙니다.
컨트롤러 추가하기
컨트롤러(controller)는 HTTP 요청을 처리하는 클래스입니다. 프로젝트에 다음과 같은 클래스를 추가합니다:
using Microsoft.AspNet.Mvc; using System.Collections.Generic; using System.Linq; using TodoApi.Models; namespace TodoApi.Controllers { [Route("api/[controller]")] public class TodoController : Controller { static readonly List<TodoItem> _items = new List<TodoItem>() { new TodoItem { Id = 1, Title = "First Item" } }; [HttpGet] public IEnumerable<TodoItem> GetAll() { return _items; } [HttpGet("{id:int}", Name = "GetByIdRoute")] public IActionResult GetById (int id) { var item = _items.FirstOrDefault(x => x.Id == id); if (item == null) { return HttpNotFound(); } return new ObjectResult(item); } [HttpPost] public void CreateTodoItem([FromBody] TodoItem item) { if (!ModelState.IsValid) { Context.Response.StatusCode = 400; } else { item.Id = 1+ _items.Max(x => (int?)x.Id) ?? 0; _items.Add(item); string url = Url.RouteUrl("GetByIdRoute", new { id = item.Id }, Request.Scheme, Request.Host.ToUriComponent()); Context.Response.StatusCode = 201; Context.Response.Headers["Location"] = url; } } [HttpDelete("{id}")] public IActionResult DeleteItem(int id) { var item = _items.FirstOrDefault(x => x.Id == id); if (item == null) { return HttpNotFound(); } _items.Remove(item); return new HttpStatusCodeResult(204); // 201 No Content } } }
역시 이번에도 컨트롤러를 작성하기 위해 먼저 Controllers라는 폴더를 생성하고 있습니다. 그러나 다시 한 번 얘기하지만, 반드시 이 규약을 준수해야만 하는 것은 아닙니다.
이 클래스의 코드들은 다음 절에서 보다 자세하게 살펴보게 될텐데, 기본적인 CRUD 동작들을 구현하고 있습니다:
요청 (HTTP 메서드 + URL) | 설명 |
---|---|
GET /api/todo | 모든 ToDo 항목들을 반환합니다. |
GET /api/todo/id | URL로 지정한 ID의 ToDo 항목을 반환합니다. |
POST /api/todo | 새로운 ToDo 항목을 생성합니다. 클라이언트는 요청 본문을 통해서 ToDo 항목을 전송해야 합니다. |
DELETE /api/todo/id | 지정한 ToDo 항목을 삭제합니다. |
가령, 다음은 ToDo 항목들의 목록을 가져오는 HTTP 요청의 한 예입니다:
GET http://localhost:5000/api/todo HTTP/1.1
User-Agent: Fiddler
Host: localhost:5000
그리고 다음은 그에 따른 응답입니다:
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Server: Microsoft-HTTPAPI/2.0
Date: Thu, 30 Oct 2014 22:40:31 GMT
Content-Length: 46
[{"Id":1,"Title":"First Item","IsDone":false}]
코드 살펴보기
이번 절에서는 TodoController
클래스의 일부 코드를 간단히 살펴보도록 하겠습니다.
라우팅
[Route] 어트리뷰트는 해당 컨트롤러에 대한 URL 템플릿을 정의합니다:
[Route("api/[controller]")]
이 어트리뷰트에 지정된 템플릿과 일치하는 모든 HTTP 요청들은 이 컨트롤러로 라우트됩니다.
이 코드에서 "[controller]"는 "Controller"라는 접미사를 제외한 컨트롤러 클래스의 이름을 의미합니다.
따라서, TodoController
클래스의 경우에 라우트 템플릿은 "api/todo"가 됩니다.
HTTP 메서드
[HttpGet], [HttpPost], 그리고 [HttpDelete] 어트리뷰트들은 컨트롤러 액션들에 대한 HTTP 메서드들을 정의합니다. (본문의 예제에서 사용되지 않은 [HttpPut] 어트리뷰트와 [HttpPatch] 어트리뷰트도 존재합니다.)
[HttpGet] public IEnumerable<TodoItem> GetAll() {} [HttpGet("{id:int}", Name = "GetByIdRoute")] public IActionResult GetById (int id) {} [HttpPost] public void CreateTodoItem([FromBody] TodoItem item) {} [HttpDelete("{id:int}")] public IActionResult DeleteItem(int id) {}
또한 GetById
메서드와 DeleteItem
메서드에는 라우트 어트리뷰트에 추가적인 세그먼트를 지정하는 문자열 인자가 추가되어 있습니다.
그 결과 이 메서드들의 완전한 라우트 템플릿은 "api/[controller]/{id:int}"이 됩니다.
이 템플릿의 "{id:int}" 세그먼트에서 id 는 변수를 의미하고, ":int"는 이 변수가 정수여야 한다는 제약조건을 나타냅니다. 따라서, 다음과 같은 URL들은 이 템플릿과 일치하지만:
http://localhost/api/todo/1
http://localhost/api/todo/42
다음 URL은 일치하지 않습니다:
http://localhost/api/todo/abc
그리고 GetById
메서드와 DeleteItem
메서드가 id 라는 이름의 메서드 매개변수를 갖고 있다는 점을 주목하시기 바랍니다.
프레임워크는 대응하는 URL 세그먼트로부터 id 매개변수의 값을 가져와서 채워줍니다.
즉, 요청된 URL이 http://localhost/api/todo/42
라면 id 매개변수의 값은 42로 설정될 것입니다.
이 과정을 매개변수 바인딩(Parameter Binding)이라고 합니다.
마지막으로 CreateTodoItem
메서드는 매개변수 바인딩의 또 다른 형태를 보여줍니다:
[HttpPost] public void CreateTodoItem([FromBody] TodoItem item) {}
여기 사용된 [FromBody] 어트리뷰트는 프레임워크에게 TodoItem
매개변수를 요청 본문으로부터 역직렬화하도록 지시합니다.
다음 표는 요청의 몇 가지 사례 및 그와 일치하는 컨트롤러 액션을 보여줍니다:
요청 | 컨트롤러 액션 |
---|---|
GET /api/todo | GetAll |
POST /api/todo | CreateTodoItem |
GET /api/todo/1 | GetById |
DELETE /api/todo/1 | DeleteItem |
GET /api/todo/abc | 없음 – 404가 반환됨 |
PUT /api/todo | 없음 – 404가 반환됨 |
마지막 두 가지 사례는 동일한 404 오류를 반환하지만, 원인은 서로 다릅니다.
가령, 'GET /api/todo/abc'의 경우에는 'abc'라는 세그멘트가 GetById
메서드의 정수 제약조건과 일치하지 않기 때문에 발생합니다.
반면, 'PUT /api/todo'의 경우에는 [HttpPut] 어트리뷰트가 적용된 컨트롤러 액션이 존재하지 않기 때문에 발생합니다.
액션 반환 값들
TodoController
클래스는 컨트롤러 액션에서 값을 반환할 수 있는 몇 가지 방법을 보여주고 있습니다.
먼저 GetAll
메서드는 CLR 개체를 반환합니다.
[HttpGet] public IEnumerable<TodoItem> GetAll() { return _items; }
이렇게 반환된 개체는 응답 메시지의 본문에 직렬화됩니다. 기본 형식은 JSON이지만 클라이언트가 다른 형식을 요청할 수도 있습니다. 가령, 다음은 XML 응답을 요구하는 HTTP 요청입니다.
GET http://localhost:5000/api/todo HTTP/1.1
User-Agent: Fiddler
Host: localhost:5000
Accept: application/xml
이에 대한 응답은 다음과 같습니다:
HTTP/1.1 200 OK
Content-Type: application/xml;charset=utf-8
Server: Microsoft-HTTPAPI/2.0
Date: Thu, 30 Oct 2014 22:40:10 GMT
Content-Length: 228
<ArrayOfTodoItem xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/TodoApi.Models"><TodoItem><Id>1</Id><IsDone>false</IsDone><Title>First Item</Title></TodoItem></ArrayOfTodoItem>
그리고 GetById
메서드는 IActionResult
형식을 반환합니다:
[HttpGet("{id:int}", Name = "GetByIdRoute")] public IActionResult GetById (int id) { var item = _items.FirstOrDefault(x => x.Id == id); if (item == null) { return HttpNotFound(); } return new ObjectResult(item); }
이 메서드는 ID와 일치하는 ToDo 항목을 발견할 경우 ObjectResult
개체를 반환합니다.
이렇게 ObjectResult
개체를 반환해도 CLR 모델을 반환할 때와 동일한 결과를 얻게 되지만, 반환 형식을 IActionResult
로 만들수 있다는 차이점이 존재합니다.
이 방식을 사용하면 하나의 메서드에서 코드 경로에 따라 각각 다른 액션 결과를 반환할 수 있습니다.
만약, ID가 일치하는 항목을 찾지 못하면 이 메서드는 HttpNotFound
를 반환하고, 이는 404 응답으로 변환됩니다.
마지막으로 CreateTodoItem
메서드는 컨트롤러에서 값들을 직접 Response 개체의 속성에 설정하는 방식의 응답 생성 방법을 보여줍니다.
이런 경우, 반환 형식은 void가 됩니다.
[HttpPost] public void CreateTodoItem([FromBody] TodoItem item) { // (some code not shown here) Context.Response.StatusCode = 201; Context.Response.Headers["Location"] = url; }
다만, 이 접근 방식의 단점은 단위 테스트의 수행이 상대적으로 어렵다는 점입니다. (액션 결과를 테스트하는 방법에 관한 논의들은 Unit Testing Controllers in ASP.NET Web API 2 기사를 참고하시기 바랍니다. 이 기사는 ASP.NET Web API에 대해서 논의하고 있지만, 일반적인 개념은 다른 경우에도 적용 가능합니다.)
의존성 주입
MVC 6는 프레임워크에 의존성 주입(dependency injection) 기능을 내장하고 있습니다. 이 기능을 직접 살펴보기 위해서, ToDo 항목들의 목록을 담고 있는 리파지터리 클래스를 생성해보겠습니다.
먼저, 리파지터리에 대한 인터페이스를 정의합니다:
using System.Collections.Generic; namespace TodoApi.Models { public interface ITodoRepository { IEnumerable<TodoItem> AllItems { get; } void Add(TodoItem item); TodoItem GetById(int id); bool TryDelete(int id); } }
그리고, 실제 구현을 정의합니다.
using System; using System.Collections.Generic; using System.Linq; namespace TodoApi.Models { public class TodoRepository : ITodoRepository { readonly List<TodoItem> _items = new List<TodoItem>(); public IEnumerable<TodoItem> AllItems { get { return _items; } } public TodoItem GetById(int id) { return _items.FirstOrDefault(x => x.Id == id); } public void Add(TodoItem item) { item.Id = 1 + _items.Max(x => (int?)x.Id) ?? 0; _items.Add(item); } public bool TryDelete(int id) { var item = GetById(id); if (item == null) { return false; } _items.Remove(item); return true; } } }
작성된 리파지터리를 생성자 주입을 이용해서 컨트롤러에 주입합니다:
[Route("api/[controller]")] public class TodoController : Controller { // Remove this code: //static readonly List<TodoItem> _items = new List<TodoItem>() //{ // new TodoItem { Id = 1, Title = "First Item" } //}; // Add this code: private readonly ITodoRepository _repository; public TodoController(ITodoRepository repository) { _repository = repository; }
그런 다음, 리파지터리를 사용하도록 컨트롤러 메서드들을 수정합니다:
[HttpGet] public IEnumerable<TodoItem> GetAll() { return _repository.AllItems; } [HttpGet("{id:int}", Name = "GetByIdRoute")] public IActionResult GetById(int id) { var item = _repository.GetById(id); if (item == null) { return HttpNotFound(); } return new ObjectResult(item); } [HttpPost] public void CreateTodoItem([FromBody] TodoItem item) { if (!ModelState.IsValid) { Context.Response.StatusCode = 400; } else { _repository.Add(item); string url = Url.RouteUrl("GetByIdRoute", new { id = item.Id }, Request.Scheme, Request.Host.ToUriComponent()); Context.Response.StatusCode = 201; Context.Response.Headers["Location"] = url; } } [HttpDelete("{id}")] public IActionResult DeleteItem(int id) { if (_repository.TryDelete(id)) { return new HttpStatusCodeResult(204); // 201 No Content } else { return HttpNotFound(); } }
이제 Startup
클래스에 다음 코드를 추가해서, 의존성 주입이 동작할 수 있도록 의존성 주입 시스템에 리파지터리를 등록합니다:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); // New code services.AddSingleton<ITodoRepository, TodoRepository>(); }
이제 응용 프로그램이 실행되면 컨트롤러의 인스턴스를 생성할 때마다 프레임워크가 자동으로 TodoRepository
를 컨트롤러에 주입해줍니다.
ITodoRepository
를 등록하기 위해서 AddSingleton
메서드를 사용했기 때문에, 응용 프로그램 생명주기 전체에 걸쳐 동일한 인스턴스가 사용됩니다.
IIS 없이 응용 프로그램 실행하기
기본적으로는 F5 키를 누르면 IIS 익스프레스에서 응용 프로그램이 실행됩니다. 이는 작업 표시줄에 나타난 IIS 익스프레스 아이콘을 통해서 확인할 수 있습니다.
그러나 ASP.NET 5.0은 조립 가능한 서버 계층(pluggable server layer)으로 설계되었습니다. 이 말은 다른 웹 서버로 교체가 가능하다는 뜻입니다. 이번 절에서는 응용 프로그램을 IIS에서 실행하는 대신, Http.Sys 커널 드라이버에서 직접적으로 실행되는 WebListener를 통해서 실행되도록 교체해보도록 하겠습니다.
물론 응용 프로그램을 IIS에서 실행하면 많은 이점이 존재합니다 (보안, 프로세스 관리, GUI 기반의 관리 도구 등). 여기서 전하고자 하는 요점은 ASP.NET 5.0은 IIS에 직접적으로 얽메이지 않는다는 것입니다. 실제로 응용 프로그램을 콘솔 응용 프로그램 내부에서 실행시켜보도록 하겠습니다.
project.json 파일에 Microsoft.AspNet.Server.WebListener 패키지를 추가합니다:
"dependencies": { "Microsoft.AspNet.Server.IIS": "1.0.0-beta1", "Microsoft.AspNet.Diagnostics": "1.0.0-beta1", "Microsoft.AspNet.Mvc": "6.0.0-beta1", // New: "Microsoft.AspNet.Server.WebListener": "1.0.0-beta1" },
그런 다음, project.json 파일에 다음과 같은 최상위 수준 옵션을 추가합니다.
{ // Other sections not shown "commands": { "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000" } }
이 "commands" 옵션은 K 런타임에 전달할 수 있는 미리 정의된 명령들의 목록을 담고 있습니다. 이 예제에서는 "web"이라는 이름으로 명령을 지정하고 있으며, 이 이름은 임의로 지정 가능한 문자열이고 그 값은 실제 명령입니다.
- Microsoft.AspNet.Hosting은 ASP.NET 5.0 응용 프로그램을 호스트하기 위해서 사용되는 어셈블리입니다. 이 어셈블리는 자체 호스팅에 사용되는 진입점을 구현하고 있습니다.
--server
플래그는 서버를 지정하며, 이번 예제에서는 WebListener입니다.--server.urls
플래그는 수신을 대기할 URL을 지정합니다.
변경된 project.json 파일을 저장한 다음, 솔루션 탐색기에서 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 속성(Properties)을 클릭합니다. 그리고, Debug 탭을 클릭한 다음, Debug target 드롭다운 항목을 "IIS Express"에서 "web"으로 변경합니다.
이제 F5 키를 눌러서 응용 프로그램을 실행합니다. 그러면 Visual Studio가 지금까지처럼 IIS 익스프레스를 실행하고 브라우저 창을 여는 대신, WebListener가 실행되는 콘솔 응용 프로그램을 실행하게 됩니다.
계속해서 브라우저를 실행하고 http://localhost:5000
으로 이동해봅니다.
(이 URL은 project.json 파일에 지정한 값입니다.)
그러면 정상적으로 환영 페이지가 나타나는 것을 확인할 수 있을 것입니다.
다시 IIS 익스프레스에서 응용 프로그램을 실행하고 싶다면, 간저 Debug Target 항목을 "IIS Express"로 재설정해주기만 하면 됩니다.
이 기사는 2014년 11월 12일에 최초 작성되었습니다.
- ASP.NET vNext 및 Visual Studio "14" 시작하기 2014-10-02 08:00
- ASP.NET MVC 6 시작하기 2014-10-27 08:00
- ASP.NET MVC 6에서 Web API 작성하기 2014-12-15 08:00
- Grunt와 Bower를 이용한 Visual Studio 2015의 클라이언트 측 웹 개발 관리 2015-01-12 08:00