CRUD 작업을 지원하는 Web API 작성하기

등록일시: 2013-06-14 20:30,  수정일시: 2016-09-02 07:32
조회수: 12,717
이 문서는 ASP.NET Web API 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.

본 자습서에서는 ASP.NET Web API를 사용해서 CRUD 작업을 제공해주는 HTTP 서비스를 작성하는 방법을 살펴봅니다.

여기서 CRUD는 가장 기본적인 네 가지 데이터베이스 작업인, 생성(Create), 조회(Read), 갱신(Update), 그리고 삭제(Delete)를 뜻하며, 대부분의 HTTP 서비스들은 REST 또는 REST-Like API를 통해서 이 CRUD 작업을 구현합니다.

본 자습서에서는 제품들의 목록을 관리하는 매우 간단한 Web API를 구현해봅니다. 그리고, 각각의 제품들은 이름, 가격, 카테고리("toys"나 "hardware" 등과 같은), 그리고 제품 ID를 갖게 될 것입니다.

전체 프로젝트 다운로드

이 제품 관리 Web API는 다음과 같은 메서드들을 노출하게 됩니다.

동작 HTTP 메서드 관련 URI
모든 제품들의 목록을 가져옵니다. GET /api/products
ID를 기준으로 제품을 조회합니다. GET /api/products/id
카테고리를 기준으로 제품들을 조회합니다. GET /api/products?category=category
새로운 제품을 생성합니다. POST /api/products
제품을 갱신합니다. PUT /api/products/id
제품을 삭제합니다. DELETE /api/products/id

일부 URI의 경로에 제품의 ID가 포함되어 있다는 점을 주의하시기 바랍니다. 가령, ID가 28인 제품을 조회하려면, 클라이언트에서는 http://hostname/api/products/28로 GET 요청을 전송해야 합니다.

리소스

이 제품 Web API는 리소스 유형에 따라 다음과 같은 두 가지 형태의 URI를 정의합니다:

리소스 유형 URI 형태
모든 제품들의 목록 /api/products
개별 제품 /api/products/id

메서드

네 가지 핵심 HTTP 메서드들(GET, PUT, POST, 그리고 DELETE)과 CRUD 작업들을 다음과 같이 맵핑할 수 있습니다:

  • GET: 특정 URI로부터 리소스를 조회합니다. 이 GET 메서드는 서버에 다른 영향을 미치지 않습니다.
  • PUT: 특정 URI의 리소스를 갱신합니다. 서버에서 클라이언트가 새로운 URI를 지정할 수 있도록 허용해주는 경우에는, 특정 URI를 통해서 새로운 리소스를 생성할 수도 있습니다. 다만, 본 자습서에서 살펴볼 Web API는 PUT을 사용한 생성을 지원하지 않습니다.
  • POST: 새로운 리소스를 생성합니다. 서버는 생성된 개체에 URI를 할당한 다음, 응답 메시지의 일부로 해당 URI를 반환해줍니다.
  • DELETE: 특정 URI의 리소스를 삭제합니다.

노트: PUT 메서드는 특정 제품 항목 전체를 대체하기 때문에, 클라이언트는 갱신할 제품의 완전한 표현을 전송해야 합니다. 이와 달리, 부분 갱신을 지원하려면 PATCH 메서드를 사용하는 것이 좋습니다. 다만, 본 자습서에서는 PATCH 메서드는 구현하지 않습니다.

새로운 Web API 프로젝트 생성하기

먼저, Visual Studio를 시작한 다음, 시작 (Start) 페이지에서 새 프로젝트 (New Project) 링크를 클릭합니다. 또는, 파일 (File) 메뉴에서 새로 만들기 (New)프로젝트 (Project)를 차례대로 선택해도 됩니다.

템플릿 (Templates) 패인에서 설치됨 (Installed) 노드 하위의 템플릿 (Templates) 노드를 선택하고, 그 하위의 Visual C# 노드를 확장합니다. Visual C# 노드 하위의 웹 (Web) 노드를 선택한 다음, 프로젝트 템플릿 목록에서 ASP.NET MVC 4 웹 응용 프로그램 (ASP.NET MVC 4 Web Application)을 선택하고, 프로젝트 이름을 "ProductStore"로 지정한 다음, 확인 (OK) 버튼을 클릭합니다.

계속해서 새 ASP.NET MVC 4 프로젝트 (New ASP.NET MVC 4 Project) 대화 상자에서 Web API를 선택한 다음, 확인 (OK) 버튼을 클릭합니다.

모델 추가하기

모델은 응용 프로그램의 데이터를 표현하는 개체입니다. ASP.NET Web API에서는 강력한 형식의 CLR 개체를 모델로 사용할 수 있으며, 이 개체들은 클라이언트에 전송될 때 자동으로 XML이나 JSON으로 직렬화됩니다.

본 자습서의 ProductStore API의 데이터는 제품들로 구성되므로, Product라는 이름의 새로운 클래스를 생성하겠습니다.

만약, 솔루션 탐색기가 보이지 않는다면, 보기 (View) 메뉴에서 솔루션 탐색기 (Solution Explorer)를 클릭합니다. 솔루션 탐색기에서 Models 폴더를 마우스 오른쪽 버튼으로 클릭하고, 컨텍스트 메뉴에서 추가 (Add)를 선택한 다음, 클래스 (Class)를 선택합니다. 클래스의 이름은 "Product"로 지정합니다.

그리고, Product 클래스에 다음과 같은 속성들을 추가합니다.

namespace ProductStore.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

리파지터리 추가하기

본 자습서의 Web API를 작성하려면 제품들의 정보를 어딘가에 저장해야만 합니다. 그리고, 이런 정보들과 서비스의 구현 자체를 별도로 분리하는 것은 매우 좋은 접근방식입니다. 그러면, 서비스 클래스를 재작성하지 않고서도 관련 저장소를 변경할 수 있기 때문입니다. 이런 형태의 설계를 리파지터리 (Repository) 패턴이라고 부릅니다. 따라서, 리파지터리에 대한 기본적인 인터페이스를 정의하는 것으로부터 본격적인 작업을 시작해보겠습니다.

다시 솔루션 탐색기에서 Models 폴더를 마우스 오른쪽 버튼으로 클릭합니다. 그리고, 컨텍스트 메뉴에서 추가 (Add)를 선택한 다음, 새 항목 (New Item)을 선택합니다.

템플릿 (Templates) 패인에서 설치됨 (Installed) 노드 하위에 위치한 Visual C# 노드를 확장합니다. 그리고, 그 하위의 코드 (Code) 노드를 선택한 다음, 목록에서 인터페이스 (Interface)을 선택합니다. 인터페이스 이름을 "IProductRepository"로 지정하고 추가 (Add) 버튼을 클릭합니다.

인터페이스 파일이 생성되면 다음과 같은 정의를 추가합니다:

namespace ProductStore.Models
{
    public interface IProductRepository
    {
        IEnumerable<Product> GetAll();
        Product Get(int id);
        Product Add(Product item);
        void Remove(int id);
        bool Update(Product item);
    }
}

계속해서 Models 폴더에 IProductRespository 인터페이스를 구현하기 위한 클래스를 "ProductRepository"라는 이름으로 하나 더 추가합니다. 그리고, 이 클래스에 다음과 같은 구현을 추가합니다:

namespace ProductStore.Models
{
    public class ProductRepository : IProductRepository
    {
        private List<Product> products = new List<Product>();
        private int _nextId = 1;
    
        public ProductRepository()
        {
            Add(new Product { Name = "Tomato soup", Category = "Groceries", Price = 1.39M });
            Add(new Product { Name = "Yo-yo", Category = "Toys", Price = 3.75M });
            Add(new Product { Name = "Hammer", Category = "Hardware", Price = 16.99M });
        }
    
        public IEnumerable<Product> GetAll()
        {
            return products;
        }
    
        public Product Get(int id)
        {
            return products.Find(p => p.Id == id);
        }
    
        public Product Add(Product item)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }
            item.Id = _nextId++;
            products.Add(item);
            return item;
        }
    
        public void Remove(int id)
        {
            products.RemoveAll(p => p.Id == id);
        }
    
        public bool Update(Product item)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }
            int index = products.FindIndex(p => p.Id == item.Id);
            if (index == -1)
            {
                return false;
            }
            products.RemoveAt(index);
            products.Add(item);
            return true;
        }
    }
}

이 예제 리파지터리는 제품들의 목록을 로컬 메모리에 저장합니다. 자습서 수준에서는 이런 방식도 문제가 없지만, 실제 응용 프로그램에서는 데이터베이스나 클라우드 저장소 같은 외부에 저장하는 것이 보다 일반적일 것입니다. 이런 형태의 리파지터리 패턴은 추후에 구현을 손쉽게 변경할 수 있도록 해줍니다.

Web API 컨트롤러 추가하기

만약, ASP.NET MVC로 작업을 해본 경험이 있다면 이미 컨트롤러에 익숙할 것입니다. 컨트롤러는 ASP.NET Web API에서 클라이언트로부터 전달된 HTTP 요청을 처리해주는 클래스입니다. 프로젝트를 생성할 때, 새 프로젝트 (New Project) 마법사가 자동으로 두 개의 컨트롤러를 생성해주는데, 이를 살펴보려면 솔루션 탐색기 (Solution Explorer)에서 Controllers 폴더를 확장해보면 됩니다.

  • HomeController는 전통적인 ASP.NET MVC의 컨트롤러입니다. 이 컨트롤러는 사이트의 HTML 페이지를 제공해주는 작업을 담당하며, Web API와는 직접적인 관련이 없습니다.
  • ValuesController가 바로 예제 WebAPI 컨트롤러입니다.

이제, 솔루션 탐색기 (Solution Explorer)에서 ValuesController 파일을 마우스 오른쪽으로 클릭한 다음, 삭제 (Delete) 메뉴를 선택해서 해당 파일을 삭제합니다. 그리고, 다음과 같은 방법으로 새로운 컨트롤러를 추가합니다:

이번에도 솔루션 탐색기 (Solution Explorer)에서 Controllers 폴더를 마우스 오른쪽 버튼으로 클릭합니다. 그런 다음, 추가 (Add) 하위의 컨트롤러 (Controller) 메뉴를 선택합니다.

컨트롤러 추가 (Add Controller) 마법사에서 컨트롤러의 이름을 "ProductsController"로 지정하고, 템플릿 (Template) 드롭다운 리스트에서 빈 API 컨트롤러 (Empty API Controller)를 선택합니다. 그런 다음, 추가 (Add) 버튼을 클릭합니다.

반드시 컨트롤러를 Controllers 폴더 하위에 위치시켜야만 하는 것은 아닙니다. 폴더 이름은 중요하지 않으며, 이는 단지 소스 파일들의 구조를 편리하게 관리하기 위한 방법일 뿐입니다.

그러면, 컨트롤러 추가 (Add Controller) 마법사가 Controllers 폴더에 ProductsController.cs 라는 이름으로 새로운 파일을 생성해 줄 것입니다. 만약 이 파일이 자동으로 열리지 않는다면, 파일을 더블 클릭해서 엽니다. 그리고, 다음과 같이 using 문을 추가합니다:

using ProductStore.Models;

그리고, 다음과 같이 IProductRepository의 인스턴스를 담고 있을 필드를 추가합니다.

public class ProductsController : ApiController
{
    static readonly IProductRepository repository = new ProductRepository();
}

컨트롤러 내부에서 new ProductRepository()를 직접 호출하는 방식은 그리 좋은 설계라고는 볼 수 없는데, 이는 컨트롤러를 IProductRepository의 특정 구현과 강력하게 결합시키기 때문입니다. 보다 바람직한 접근방식에 대해서는 Using the Web API Dependency Resolver 문서를 참고하시기 바랍니다.

리소스 가져오기

본 자습서의 ProductStore API 예제에서는 HTTP GET 메서드를 통해서 몇 가지 "조회" 동작들을 제공해주며, 이 각각의 동작들은 다음과 같이 ProductsController 클래스의 특정 메서드에 대응됩니다.

동작 HTTP 메서드 관련 URI
모든 제품들의 목록을 가져옵니다. GET /api/products
ID를 기준으로 제품을 조회합니다. GET /api/products/id
카테고리를 기준으로 제품들을 조회합니다. GET /api/products?category=category

먼저, ProductsController 클래스에 모든 제품들의 목록을 가져오기 위한 메서드를 다음과 같이 추가합니다:

public class ProductsController : ApiController
{
    public IEnumerable<Product> GetAllProducts()
    {
        return repository.GetAll();
    }
    // ....
}

이 메서드의 이름은 "Get"으로 시작하므로, 관례에 따라 GET 요청에 맵핑됩니다. 또한, 이 메서드에는 매개변수가 하나도 없으므로, 경로 중에 "id" 세그먼트를 포함하지 않는 URI에 맵핑됩니다.

이번에는 ID를 기준으로 제품을 조회하기 위한 다음과 같은 메서드를 추가합니다:

public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    return item;
}

이 메서드의 이름도 "Get"으로 시작하지만, 이번에는 id라는 이름의 매개변수를 갖고 있습니다. 따라서, 이 매개변수는 URI 경로의 "id" 세그먼트와 맵핑됩니다. 또한, ASP.NET Web API 프레임워크가 자동으로 이 ID를 매개변수에 적합한 데이터 형식(int)으로 변환해줍니다.

이 GetProduct 메서드는 id가 유효하지 않은 경우, HttpResponseException 형식의 예외를 던집니다. 그리고, 이 예외는 프레임워크에 의해 404 (Not Found) 오류로 변환됩니다.

마지막으로, 카테고리를 기준으로 제품들을 조회하기 위한 메서드를 추가합니다:

public IEnumerable<Product> GetProductsByCategory(string category)
{
    return repository.GetAll().Where(
        p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase));
}

Web API는 요청 URI에 쿼리스트링이 존재하는 경우, 이 쿼리 매개변수를 컨트롤러 메서드의 매개변수와 매치하려고 시도합니다. 따라서, 이 메서드는 "api/products?category=category" 형태의 URL와 맵핑됩니다.

리소스 생성하기

계속해서 이번에는 새로운 제품을 생성하기 위한 메서드를 ProductsController 클래스에 추가해보도록 하겠습니다. 다음은 이 메서드의 간단한 구현을 보여줍니다:

// 최종 구현이 아닙니다!
public Product PostProduct(Product item)
{
    item = repository.Add(item);
    return item;
}

이 메서드에 관해서 두 가지 사항에 주목하시기 바랍니다:

  • 이 메서드는 이름이 "Post..."로 시작됩니다. 따라서, 클라이언트는 새로운 제품을 생성하려면 HTTP POST 요청을 전송해야만 합니다.
  • 이 메서드는 매개변수로 Product 형식을 받습니다. Web API에서 복합 형식으로 이루어진 매개변수는 요청 본문으로부터 역직렬화됩니다. 그러므로, 클라이언트는 XML이나 JSON 형식으로 직렬화된 제품 개체의 표현을 전송해야 합니다.

지금처럼 간단하게 구현된 메서드도 동작이야 하지만, 매우 부실한 편입니다. 이상적인 구현이 되려면 HTTP 응답에 다음과 같은 내용들이 포함되는 것이 좋습니다:

  • 응답 코드: 기본적으로 Web API 프레임워크는 응답 상태 코드를 200 (OK)으로 설정합니다. 그러나, HTTP/1.1 프로토콜에 따르면 리소스를 생성하기 위해서 POST 요청을 전송한 경우, 서버는 상태 코드 201 (Created)을 결과로 반환해야 합니다.
  • 위치: 서버에서 리소스를 생성한 경우, 응답의 Location 헤더에 새로운 리소스의 URI를 포함시켜야 합니다.

ASP.NET Web API를 사용하면 HTTP 응답 메시지를 손쉽게 다룰 수 있습니다. 다음은 개선된 메서드 구현입니다:

public HttpResponseMessage PostProduct(Product item)
{
    item = repository.Add(item);
    var response = Request.CreateResponse<Product>(HttpStatusCode.Created, item);
   
    string uri = Url.Link("DefaultApi", new { id = item.Id });
    response.Headers.Location = new Uri(uri);
    return response;
}

반환 형식이 HttpResponseMessage로 변경되었다는 점에 주목하십시요. Product 대신 HttpResponseMessage를 반환하기 때문에, 상태 코드 및 Location 헤더를 포함한 HTTP 응답 메시지의 부분들을 세밀하게 제어할 수 있습니다.

이 코드에 사용된 CreateResponse 메서드는 HttpResponseMessage 개체를 생성하고 자동으로 응답 메시지의 본문에 직렬화된 Product 개체의 표현식을 써줍니다.

본 자습서의 예제에서는 Product 개체의 유효성을 검사하지 않고 있습니다. 모델 유효성 검사에 관한 더 자세한 정보는 Model Validation in ASP.NET Web API를 참고하시기 바랍니다.

리소스 갱신하기

PUT으로 제품을 갱신하는 메서드는 다음과 같이 간단합니다:

public void PutProduct(int id, Product product)
{
    product.Id = id;
    if (!repository.Update(product))
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
}

이 메서드의 이름은 "Put..."으로 시작하므로, Web API는 이 메서드와 PUT 요청을 매치시킵니다. 이 메서드는 두 개의 매개변수를 받는데, 제품의 ID와 갱신할 제품이 바로 그것입니다. 그 중, id 매개변수는 URI 경로로부터 가져오고, product 매개변수는 요청 본문으로부터 역직렬화됩니다. 기본적으로 ASP.NET Web API 프레임워크는 라우트로부터 단순한 매개변수를 가져오고, 요청 본문으로부터 복합 형식 매개변수를 가져옵니다.

리소스 삭제하기

리소스를 삭제하려면 "Delete..." 메서드를 정의합니다.

public void DeleteProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    
    repository.Remove(id);
}

DELETE 요청이 성공하는 경우, 엔티티 본문과 함께 상태를 전달하기 위해 코드 200 (OK)를 반환할 수 있으며, 삭제가 아직 진행 중인 경우에는 상태 코드 202 (Accepted)를, 엔티티 본문이 없는 경우에는 상태 코드 204 (No Content)를 반환할 수 있습니다. 예를 들어서, 본 자습서의 예제에서는 DeleteProduct 메서드가 void 형식을 반환하므로, ASP.NET Web API가 자동으로 상태 코드 204 (No Content)를 반환해줍니다.

다음 과정

Web API를 호스팅 공급자에 배포해서 인터넷을 통해서 Web API를 사용할 수 있도록 구성할 수도 있습니다. 마이크로소프트는 최대 10개의 웹 사이트를 무료로 호스팅 할 수 있는 Free Windows Azure trial account를 제공해줍니다. Visual Studio 웹 프로젝트를 Windows Azure 웹 사이트에 배포하는 방법에 대한 더 자세한 정보는 Deploying an ASP.NET Web Application to a Windows Azure Web Site를 참고하시기 바랍니다.