ASP.NET Core MVC와 Visual Studio를 이용한 첫 번째 Web API 구현

등록일시: 2016-07-22 08:00,  수정일시: 2016-09-02 06:31
조회수: 9,946
이 문서는 ASP.NET Core 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 ASP.NET Core MVC와 Visual Studio를 이용해서 간단한 Web API를 만들고 Fiddler를 이용해서 동작을 테스트해 봅니다.

HTTP는 단지 웹 페이지만 서비스하기 위한 프로토콜이 아닙니다. HTTP는 서비스와 데이터를 제공하는 APIs의 구축을 위한 강력한 플랫폼이기도 합니다. HTTP는 단순하고 유연하며 널리 사용되고 있습니다. 생각할 수 있는 거의 모든 플랫폼들이 HTTP 관련 라이브러리를 갖고 있으며, 브라우저나 모바일 장치, 그리고 전통적인 테스크탑 응용 프로그램에 이르기까지 광범위한 클라이언트 영역을 대상으로 HTTP 서비스를 적용할 수 있습니다.

본문에서는 "To-do" 목록을 관리하는 간단한 Web API를 만들어보려고 합니다. 단, UI 부분은 직접 구현하지 않습니다.

ASP.NET Core에서는 MVC가 자체적으로 Web API를 구현하기 위한 기능을 포함하고 있습니다. 이제 두 프레임워크가 통합되어 동일한 코드 기반 및 파이프라인을 공유하고 있으므로, UI (HTML)와 APIs를 모두 포함하고 있는 응용 프로그램의 구현 작업이 더 간단해졌습니다.

노트

기존에 구축한 Web API 응용 프로그램을 ASP.NET Core 기반으로 이전해야 한다면 Migrating from ASP.NET Web API 문서를 참고하시기 바랍니다.

개요

다음 표는 본문에서 구축해보게 될 API를 보여주고 있습니다:

API 설명 요청 본문 응답 본문
GET /api/todo 모든 To-do 항목들을 조회합니다. None To-do 항목들의 배열
GET /api/todo/{id} ID로 To-do 항목을 조회합니다. None To-do 항목
POST /api/todo 새 항목을 추가합니다. To-do 항목 To-do 항목
PUT /api/todo/{id} 기존 항목을 갱신합니다. To-do 항목 None
DELETE /api/todo/{id} 항목을 삭제합니다. None None

그리고 다음 다이어그램은 응용 프로그램의 기본 설계를 보여줍니다.

  • Web API를 소비할 수 있는 모든 장치가 클라이언트가 될 수 있습니다 (브라우저, 모바일 앱 등). 다만, 본문에서는 클라이언트를 구현하지 않습니다.
  • 모델은 응용 프로그램의 데이터를 표현하는 개체입니다. 본문에서 사용하는 유일한 모델은 To-do 항목입니다. 모델은 간단한 C# 클래스(POCOs)로 표현됩니다.
  • 컨트롤러는 HTTP 요청을 처리하고 HTTP 응답을 생성하는 개체입니다. 본문의 예제 응용 프로그램에는 하나의 컨트롤러만 존재합니다.
  • 본문에서는 내용을 간결하게 유지하기 위해서 예제 응용 프로그램에서 데이터베이스를 사용하지는 대신 간단하게 메모리에 To-do 항목들을 보관할 것입니다. 그러나 데이터 접근 레이어는 직접 작성해보면서 Web API와 데이터 레이어의 분리 방법을 살펴보도록 하겠습니다. 데이터베이스를 사용하는 방식의 자습서를 살펴보고 싶다면 Building your first ASP.NET Core MVC app with Visual Studio 자습서 시리즈를 참고하시기 바랍니다.

Fiddler 설치하기

본문에서는 직접 클라이언트를 구현하는 대신, Fiddler를 이용해서 API를 테스트해 볼 것입니다. Fiddler는 HTTP 요청을 구성하거나 원시 HTTP 응답을 살펴볼 수 있는 웹 디버깅 도구입니다.

프로젝트 생성하기

Visual Studio를 실행합니다. 그리고 파일(File) 메뉴에서 새로 만들기(New) > 프로젝트(Project)를 선택합니다.

대화 상자에서 ASP.NET Core Web Application (.NET Core) 프로젝트 템플릿을 선택합니다. 프로젝트의 이름을 TodoApi로 지정한 다음, 확인(OK) 버튼을 누릅니다.

계속해서 New ASP.NET Core Web Application (.NET Core) - TodoApi 대화 상자가 나타나면 Web API 템플릿을 선택합니다. 그리고 다시 확인(OK) 버튼을 누릅니다.

모델 클래스 추가하기

모델은 응용 프로그램의 데이터를 표현하는 개체입니다. 본문에서 사용하는 유일한 모델은 To-do 항목입니다.

먼저 "Models"라는 이름의 폴더를 추가해야 합니다. 솔루션 탐색기(Solution Explorer)에서 마우스 오른쪽 버튼으로 프로젝트를 클릭합니다. 그리고 추가(Add) > 새 폴더(New Folder)를 선택합니다. 폴더명을 Models 로 지정합니다.

노트

프로젝트 상의 어떤 위치에도 모델 클래스를 생성할 수 있지만 규약에 따라 대부분 Models 폴더가 사용됩니다.

계속해서 TodoItem 클래스를 추가합니다. Models 폴더를 마우스 오른쪽 버튼으로 클릭한 다음 추가(Add) > 새 항목(New Item)을 선택합니다.

새 항목 추가(Add New Item) 대화 상자가 나타나면 클래스(Class) 템플릿을 선택합니다. 클래스의 이름을 TodoItem으로 지정하고 추가(Add) 버튼을 누릅니다.

자동으로 생성된 클래스의 코드를 다음 코드로 대체합니다:

namespace TodoApi.Models
{
    public class TodoItem
    {
        public string Key { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
    }
}

리파지터리 클래스 추가하기

리파지터리는 데이터 레이어를 캡슐화하는 개체로, 데이터를 조회하고 이를 엔터티 모델과 매핑하기 위한 로직을 포함하고 있습니다. 비록 본문의 예제 응용 프로그램에서는 데이터베이스를 사용하고 있지는 않지만 컨트롤러에 리파지터리를 주입할 수 있는 방법을 살펴볼 수 있습니다. Models 폴더에 리파지터리 코드를 생성해보도록 하겠습니다.

먼저 ITodoRepository라는 이름으로 리파지터리 인터페이스를 정의합니다. 클래스 템플릿을 활용하여 이 작업을 처리합니다 (새 항목 추가(Add New Item) > 클래스(Class)).

using System.Collections.Generic;

namespace TodoApi.Models
{
    public interface ITodoRepository
    {
        void Add(TodoItem item);
        IEnumerable<TodoItem> GetAll();
        TodoItem Find(string key);
        TodoItem Remove(string key);
        void Update(TodoItem item);
    }
}

이 인터페이스는 기본적인 CRUD 작업을 정의하고 있습니다.

계속해서 이번에는 이 ITodoRepository 인터페이스를 구현하는 TodoRepository 클래스를 추가합니다:

using System;
    using System.Collections.Generic;
    using System.Collections.Concurrent;

    namespace TodoApi.Models
    {
        public class TodoRepository : ITodoRepository
        {
            private static ConcurrentDictionary<string, TodoItem> _todos = 
                new ConcurrentDictionary<string, TodoItem>();

            public TodoRepository()
            {
                Add(new TodoItem { Name = "Item1" });
            }

            public IEnumerable<TodoItem> GetAll()
            {
                return _todos.Values;
            }

            public void Add(TodoItem item)
            {
                item.Key = Guid.NewGuid().ToString();
                _todos[item.Key] = item;
            }

            public TodoItem Find(string key)
            {
                TodoItem item;
                _todos.TryGetValue(key, out item);
                return item;
            }

            public TodoItem Remove(string key)
            {
                TodoItem item;
                _todos.TryGetValue(key, out item);
                _todos.TryRemove(key, out item);
                return item;
            }

            public void Update(TodoItem item)
            {
                _todos[item.Key] = item;
            }
        }
    }

이제 응용 프로그램을 빌드해서 컴파일러 오류가 발생하는지 확인해봅니다.

리파지터리 등록하기

이렇게 리파지터리 인터페이스를 정의하면 리파지터리 클래스와 이를 사용하는 MVC 컨트롤러를 서로 분리할 수 있습니다. 컨트롤러 내부에서 TodoRepository의 인스턴스를 생성하는 대신, ASP.NET Core의 내장 의존성 주입 기능을 이용해서 ITodoRepository를 주입할 것입니다.

이런 접근방식을 사용하면 컨트롤러의 단위 테스트가 손쉬워집니다. 단위 테스트에서는 ITodoRepository의 목(Mock)이나 스텁(Stub) 버전을 주입하게 됩니다. 그 결과, 데이터 접근 레이어의 영향을 최소화한 상태로 컨트롤러의 로직을 보다 면밀하게 테스트할 수 있습니다.

컨트롤러에 리파지터리를 주입하려면 DI 컨테이너를 이용해서 리파지터리를 등록해야 합니다. Startup.cs 파일을 열고, 다음의 using 구문을 추가합니다:

using TodoApi.Models;

그런 다음, ConfigureServices 메서드에 다음에 강조된 코드를 추가합니다:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc();

    services.AddLogging();

    // Add our repository type
    services.AddSingleton<ITodoRepository, TodoRepository>();
}

컨트롤러 추가하기

다시 솔루션 탐색기(Solution Explorer)에서 마우스 오른쪽 버튼으로 Controllers 폴더를 클릭합니다. 그리고 추가(Add) > 새 항목(New Item)을 선택합니다. 새 항목 추가(Add New Item) 대화 상자가 나타나면 Web API 컨트롤러 클래스(Web API Controller Class) 템플릿을 선택합니다. 클래스의 이름을 TodoController로 지정합니다.

자동으로 생성된 클래스의 코드를 다음 코드로 대체합니다:

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using TodoApi.Models;

namespace TodoApi.Controllers
{
    [Route("api/[controller]")]
    public class TodoController : Controller
    {
        public TodoController(ITodoRepository todoItems)
        {
            TodoItems = todoItems;
        }
        public ITodoRepository TodoItems { get; set; }
    }
}

이 코드는 빈 컨트롤러 클래스를 정의하고 있습니다. 다음 절들에서는 메서드들을 추가해서 API를 구현해 볼 것입니다.

To-do 항목들 조회하기

방금 생성한 TodoController 클래스에 다음 메서드를 추가해서 To-do 항목들을 조회하는 API를 구현합니다.

[HttpGet]
public IEnumerable<TodoItem> GetAll()
{
    return TodoItems.GetAll();
}

[HttpGet("{id}", Name = "GetTodo")]
public IActionResult GetById(string id)
{
    var item = TodoItems.Find(id);
    if (item == null)
    {
        return NotFound();
    }
    return new ObjectResult(item);
}

이 메서드들은 다음과 같은 두 가지 GET 메서드들을 구현합니다:

  • GET /api/todo
  • GET /api/todo/{id}

다음은 GetAll 메서드 호출에 대한 HTTP 응답의 사례입니다:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/10.0
Date: Thu, 18 Jun 2015 20:51:10 GMT
Content-Length: 82

[{"Key":"4f67d7c5-a2a9-4aae-b030-16003dd829ae","Name":"Item1","IsComplete":false}]

잠시 후에 Fiddler 도구를 이용해서 HTTP 응답을 살펴보는 방법을 알아보겠습니다.

라우팅과 URL 경로

[HttpGet] 어트리뷰트는 이 메서드들이 HTTP GET 메서드임을 지정합니다. 각 메서드들의 URL 경로는 다음과 같은 방식으로 구성됩니다:

  • 컨트롤러의 라우트 어트리뷰트, [Route("api/[controller]")]에서 템플릿 문자열을 가져옵니다.
  • 그런 다음, "[Controller]" 부분을 컨트롤러의 이름으로 대체하는데, 여기서 컨트롤러의 이름은 컨트롤러 클래스의 이름에서 "Controller"라는 접미사를 제외한 나머지 문자열을 말합니다. 가령, 본문의 예제에서 컨트롤러 클래스의 이름은 TodoController이고 루트 이름은 "todo"입니다. 참고로 ASP.NET MVC Core는 대소문자를 구분하지 않습니다.
  • 만약 [HttpGet] 어트리뷰트에도 템플릿 문자열이 지정되어 있다면 경로에 추가됩니다. 본문의 예제에서는 템플릿 문자열을 사용하고 있지 않습니다.

또한 GetById 메서드에 지정된 "{id}"는 자리 표시자(Placeholder) 변수입니다. 실제 HTTP 요청에서 클라이언트는 이 변수에 To-do 항목의 ID를 지정하게 됩니다. 그러면 런타임 시에 MVC가 GetById 메서드를 호출하면서 URL에서 가져온 "{id}"의 값을 메서드의 id 매개변수에 전달해줍니다.

반환 값

이 코드의 GetAll 메서드는 CLR 개체를 반환합니다. MVC는 개체를 자동으로 JSON 형식으로 직렬화시킨 다음, JSON을 응답 메시지의 본문에 작성합니다. 처리되지 않은 예외가 없을 경우, 이 메서드의 응답 코드는 200입니다. (처리되지 않은 예외는 5xx 오류로 변환됩니다.)

반면, GetById 메서드는 일반적인 결과 형식을 나타내는 보다 범용적인 IActionResult 형식을 반환합니다. 그 이유는 GetById 메서드가 두 가지 다른 반환 형식을 갖고 있기 때문입니다:

  • 이 메서드는 요청된 ID와 일치하는 항목이 존재하지 않으면 404 오류를 반환합니다. 이 작업은 NotFound를 반환함으로써 이루어집니다.
  • 반면 일치하는 항목이 존재하면 이 메서드는 JSON 응답 본문과 함께 200을 반환합니다. 이 작업은 ObjectResult를 반환함으로써 이루어집니다.

Fiddler를 이용해서 API 호출하기

이번 절은 선택적인 절이지만 Web API로부터 반환되는 원시 HTTP 응답의 구체적인 내용을 살펴볼 수 있는 유용한 방법을 소개하고 있습니다. 먼저 Visual Studio에서 컨트롤+F5 키를 눌러서 Web API 응용 프로그램을 실행합니다. 그러면 Visual Studio가 브라우저를 실행한 다음, http://localhost:port/api/todo로 이동시켜 줄 것입니다 (이 URL에서 port 부분은 무작위로 선택된 포트 번호입니다). 만약 여러분이 Chrome이나 Edge, 또는 Firefox를 사용하고 있다면 브라우저에 데이터가 바로 출력됩니다. 그러나 IE를 사용하고 있다면 todo.json 파일을 열거나 혹은 저장할 것인지를 물어보는 프롬프트가 나타납니다.

역주: 본문의 이후 과정들을 따라해보기 위해서 브라우저를 계속 실행하고 있을 필요는 없습니다. 프롬프트가 나타나면 그냥 닫아버리십시오. 작업 표시줄에 IIS Express 아이콘이 실행 중이기만 하면 됩니다. 그러기 위해서는 반드시 컨트롤+F5 키를 눌러서 프로젝트를 디버그하지 않고 시작해야 합니다. 사용 중인 포트 번호도 이 아이콘을 마우스 오른쪽 버튼으로 클릭해서 확인할 수 있습니다.

그런 다음, Fiddler를 실행합니다. 그리고 File 메뉴에서 Capture Traffic 옵션의 체크를 해제합니다. 이 옵션을 해제하면 HTTP 트래픽 캡쳐 기능이 비활성화 됩니다.

그런 다음, Composer 탭을 선택합니다. 다시 하위의 Parsed 탭에 http://localhost:port/api/todo를 입력합니다 (이때 port 부분에는 여러분의 실제 포트 번호를 지정해야 합니다). 마지막으로 Execute 버튼을 눌러서 요청을 전송합니다.

그러면 세션 목록에 결과가 나타날 것입니다. 이때, 응답 코드는 200이어야 합니다. 이제 Inspectors 탭을 이용해서 응답 본문에 포함된 응답 내용을 살펴봅니다.

나머지 CRUD 작업 구현하기

남은 과정은 컨트롤러에 Create, Update, 그리고 Delete 메서드를 추가하는 것입니다. 이 메서드들은 대동소이하기 때문에, 간단하게 코드를 제시하고 주요 차이점들만 살펴보도록 하겠습니다.

Create

[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
    if (item == null)
    {
        return BadRequest();
    }
    TodoItems.Add(item);
    return CreatedAtRoute("GetTodo", new { id = item.Key }, item);
}

이 메서드는 [HttpPost] 어트리뷰트에 의해서 HTTP POST 메서드로 지정되어 있습니다. 그리고 [FromBody] 어트리뷰트는 To-do 항목의 값을 HTTP 요청의 본문에서 가져오도록 MVC에게 지시합니다.

이 메서드에 사용된 CreatedAtRoute 메서드는 서버에서 새로운 리소스를 생성하는 작업을 수행하는 HTTP POST 메서드의 표준 응답인 201 응답을 반환합니다. 또한 CreateAtRoute 메서드는 응답에 Location 헤더도 추가해줍니다. 이 Location 헤더는 새로 생성된 To-do 항목을 반환해주는 URI를 지정합니다. 더 자세한 정보는 10.2.2 201 Created를 참고하시기 바랍니다.

이번에도 Fiddler를 이용해서 Create 요청을 전송해 볼 수 있습니다:

  1. Composer 탭을 선택하고 드롭다운 목록에서 POST를 선택합니다.
  2. 요청 헤더 텍스트 상자에 Content-Type: application/json 줄을 추가해서 Content-Type 헤더의 값을 application/json으로 설정합니다. Content-Length 헤더는 Fiddler가 자동으로 추가해줍니다.
  3. 요청 본문 텍스트 상자에 다음과 같이 입력합니다: {"Name":"<your to-do item>"}
  4. Execute 버튼을 클릭합니다.

다음은 이 작업에 대한 HTTP 세션의 한 사례입니다. Raw 탭을 이용하면 다음과 같은 형태의 세션 데이터를 확인할 수 있습니다.

요청:

POST http://localhost:29359/api/todo HTTP/1.1
User-Agent: Fiddler
Host: localhost:29359
Content-Type: application/json
Content-Length: 33

{"Name":"Alphabetize paperclips"}

응답:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: http://localhost:29359/api/Todo/8fa2154d-f862-41f8-a5e5-a9a3faba0233
Server: Microsoft-IIS/10.0
Date: Thu, 18 Jun 2015 20:51:55 GMT
Content-Length: 97

{"Key":"8fa2154d-f862-41f8-a5e5-a9a3faba0233","Name":"Alphabetize paperclips","IsComplete":false}

Update

[HttpPut("{id}")]
public IActionResult Update(string id, [FromBody] TodoItem item)
{
    if (item == null || item.Key != id)
    {
        return BadRequest();
    }

    var todo = TodoItems.Find(id);
    if (todo == null)
    {
        return NotFound();
    }

    TodoItems.Update(item);
    return new NoContentResult();
}

Update 메서드는 Create 메서드와 비슷하지만 HTTP PUT을 사용합니다. 그리고 그 응답은 204 (No Content)입니다. 클라이언트에서 PUT 요청을 전송할 때는 HTTP의 명세에 따라서 변경된 부분만 전송하는 대신 갱신된 엔티티 전체를 전송해야 합니다. 부분적 갱신을 지원하려면 HTTP PATCH를 사용하면 됩니다.

Delete

[HttpDelete("{id}")]
public void Delete(string id)
{
    TodoItems.Remove(id);
}

메서드가 void 형식을 반환하면 204 (No Content) 응답을 반환합니다. 이 말은 항목이 이미 삭제되었거나 존재하지 않는 경우에도 클라이언트가 204를 받게 된다는 뜻입니다. 이렇게 존재하지 않는 자원을 삭제하려는 요청에 대해서 두 가지 관점에서 생각해볼 수 있습니다:

  • "삭제"의 의미를 "존재하는 항목을 삭제하는 작업"으로 정의하고, 항목이 존재하지 않으면 404를 반환합니다.
  • "삭제"의 의미를 "컬렉션에 항목이 존재하지 않는 상태"로 정의합니다. 해당 항목이 이미 컬렉션에 존재하지 않으면 204를 반환합니다.

두 가지 관점 모두 나름대로 합리적이라고 볼 수 있습니다. 다만, 404를 반환하는 경우에는 클라이언트 측에서도 그에 대응하는 처리를 수행해야 합니다.

역주: 실제로 DELETE 메서드를 실행해본 결과, 제 환경에서는 204 대신 200이 반환되었습니다. 원문의 Comment를 살펴보면 다른 개발자들 중에서도 동일한 결과를 얻은 사례가 있는 것 같습니다. 참고로 본 문서의 MVC 4 버전에서는 이 부분을 다음과 같이 설명하고 있습니다. "DELETE 요청이 성공하는 경우, 엔티티 본문과 함께 상태를 전달하기 위해 코드 200 (OK)를 반환할 수 있으며, 삭제가 아직 진행 중인 경우에는 상태 코드 202 (Accepted)를, 엔티티 본문이 없는 경우에는 상태 코드 204 (No Content)를 반환할 수 있습니다. 예를 들어서, 본 자습서의 예제에서는 DeleteProduct 메서드가 void 형식을 반환하므로, ASP.NET Web API가 자동으로 상태 코드 204 (No Content)를 반환해줍니다."

다음 과정