ASP.NET Core - 8. RESTful Web Service
Web Service는 전체 ASP.NET Core에서 필수를 이루는 한 부분으로서 HTTP 요청을 수용하고 Data를 포함한 응답을 생성합니다. 이번 글에서는 어떻게 MVC Framework를 통해 이러한 기능을 제공할 수 있는지에 관해 알아보고자 합니다.
1. RESTful Web Service 이해하기
Web service는 HTTP요청에 대해 순수 Data를 응답하는 것이며 Javascript와 같이 Client Application에서 사용될 수 있습니다. Web Service를 구축하는데 특별한 규칙이 있는 것은 아니지만 가장 일반적으로는 REST(Representational State Transfer) Pattern을 따르는 것입니다. 물론 REST를 위한 공시적인 사양이 있는 것은 아니고 RESTful web service를 구성하기 위한 합의안이 존재하는 것도 아니지만 Web Service를 위해 널리 사용되는 몇 가지 공통적인 관례 정도가 존재합니다. 구체적인 사양의 부족은 REST가 무엇을 의미하는지와 어떻게 RESTful web service를 생성해야 하는지에 대한 끝없는 논쟁을 불러일으켰지만 어쨌건 만들어진 Web Service가 목적에 맞게 제대로 동작한다면 이러한 것들은 큰 문제가 되지는 않을 것입니다.
(1) 요청 URL과 Method
REST의 핵심전제(폭넓은 합의가 있는 유일한 측면)는 Web Service가 URL과 GET, POST와 같은 HTTP Method를 결합하여 API를 정의한다는 것입니다. Method는 동작의 유형을 특정하며 URL은 동작의 대상이 될 Data객체를 특정합니다. 예를 들어 여기 Product의 개체를 식별할 URL이 있습니다.
api/products/1 |
이 URL은 ProductId속성을 통해 1값을 가진 Product 객체를 식별할 것입니다. URL은 Product를 식별하지만 HTTP Method는 이것을 가지고 무엇을 수행할지를 지정합니다. 아래 표는 일반적으로 Web Service에 사용되는 HTTP Method와 이들이 관례적으로 대표하는 동작들을 나열한 것입니다.
GET | 이 Method는 하나 또는 그 이상의 Data 객체를 가져오는데 사용됩니다. |
POST | 이 Method는 새로운 Data객체를 생성하는데 사용됩니다. |
PUT | 이 Method는 기존에 존재하는 객체를 update하는데 사용됩니다. |
PATCH | 이 Method는 기존에 존재하는 객체의 일부를 update하는데 사용됩니다. |
DELETE | 이 Method는 기존에 존재하는 객체를 삭제하는데 사용됩니다. |
(2) JSON
대부분 RESTful web service는 JSON(JavaScript Object Notation)형식을 사용하여 Data를 응답합니다. JSON은 Client가 Javascript를 통해 간단하면서도 쉽게 Data를 처리할 수 있어서 많은 곳에서 사용되고 있습니다. 아래 link를 통해 JSON에 관한 상세한 내용을 확인해 볼 수 있지만
Web Service를 구축함에 있어서 ASP.NET Core는 필요한 대부분의 기능을 제공하고 있으므로 JSON의 모든 측면을 이해하고 있을 필요는 없습니다.
REST는 단지 Web Service를 설계하는 하나의 방법일 뿐이며 현재 이를 대체하는 여러 인기있는 대안이 존재합니다. 그중에서 GraphQL은 React JavaScript framework와 가장 근접한 것으로 다양하게 사용될 수 있는 Web Service구현 방식입니다. REST web service와는 달리 URL과 HTTP Method의 개별적인 조합을 통해 특정 Query를 제공하며 Application의 모든 Data는 물론 Client가 요청한 형식과 Data로의 접근을 제공할 수 있습니다. GraphQL은 다소 복잡한 설정과 Client의 많은 준비를 필요로 할 수 있지만 그 결과 Client의 개발자가 그 들이 필요한 Data를 제어할 수 있는 더욱 유연한 web service를 제공해 줄 수 있습니다. GraphQL은 직접적으로 ASP.NET Core에서 지원하지 않지만 아래 Link를 통해서 가능한 .NET 구현을 확인해 볼 수 있습니다.
GraphQL | A query language for your API
다른 대안으로 gRPC라는 것도 존재하는데 속도와 효휼성에 초점을 맞춘 것으로 완전한 원격 procedure호출 Framework입니다. 현재 시점에 gRPC는 Angular나 React framework와 같은 Web Browser에서 사용될 수 없습니다. 왜냐하면 Browser는 gRPC가 HTTP 요청을 공식화하기 위한 세부적 접근방법을 제공하지 않기 때문입니다.
2. 최소 API를 사용한 Web Service 생성
우선 Program.cs를 아래와 같이 수정하여 간단한 Web Service를 생성해 봅니다.
using MyWebApp.Models;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSqlServer<NorthwindContext>(builder.Configuration.GetConnectionString("DefaultConnection"));
var app = builder.Build();
const string APIURL = "api/products";
app.MapGet($"{APIURL}/{{id}}", async (HttpContext context, NorthwindContext data) =>
{
string? id = context.Request.RouteValues["id"] as string;
if (id != null)
{
Products? p = data.Products.Find(int.Parse(id));
if (p == null)
context.Response.StatusCode = StatusCodes.Status404NotFound;
else {
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize<Products>(p));
}
}
});
app.MapGet(APIURL, async (HttpContext context, NorthwindContext data) => {
context.Response.ContentType="application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize<IEnumerable<Products>>(data.Products));
});
app.MapPost(APIURL, async (HttpContext context, NorthwindContext data) => {
Products? p = await JsonSerializer.DeserializeAsync<Products>(context.Request.Body);
if (p != null)
{
await data.AddAsync(p);
await data.SaveChangesAsync();
context.Response.StatusCode = StatusCodes.Status200OK;
}
});
app.UseMiddleware<MyWebApp.TestMiddleware>();
app.MapGet("/", () => "Hello World!");
app.Run();
예제에서는 API를 endpoint에 등록하였으며 MapGet과 MapPost Method를 통해 3개의 route를 등록하였습니다. 이들은 모두 /api로 시작하는 URL과 일치하는데 이 것은 관례적으로 Web Service의 접두사로 사용되는 것입니다.
첫번째 route의 endpoint는 Database에서 단일 Product객체를 찾기 위해 사용한 segment 변수에서 값을 전달받고 두 번째 route의 endpoint는 Database에서 모든 Product객체를 가져오게 됩니다. 마지막 세 번째 endpoint는 POST 요청을 처리하는데 여기서는 새로운 Product객체를 위한 JSON표현식을 가져오기 위해 요청 body를 읽은 후 그 결과를 Database에 추가하도록 하고 있습니다.
Web Service를 생성하기 위한 ASP.NET Core의 더 나은 기능이 있고 곧 어떤 것인지 알아볼테지만 위의 예제는 URL과 HTTP Method가 어떻게 결합하여 동작하는지를 잘 보여주고 있으며 이것이 Web Service를 만드는 데 있어서 가장 핵심적인 부분이라 할 수 있습니다.
Project를 실행하여 /api/products/1 으로 URL을 요청합니다. URL은 예제상에서 첫 번째 route와 일치되어 다음과 같은 응답을 생성할 것입니다.
/api/products로의 요청은 두번째 route와 일치되어 모든 product객체를 나열하게 됩니다.
세 번째 route의 경우에는 Web Browser를 통해 URL만으로 HTTP POST 요청을 보내는 것은 불가능하므로 조금 다른 접근법을 사용해야 합니다. 우선 Windows용 UI도구로 Postman이라는 것을 사용하고자 합니다. 아래 Link를 통해 Postman을 내려받고 설치합니다.
Postman API Platform | Sign Up for Free
Postman을 실행시켜 아래와 같이 'POST'를 선택하고 주소에 'https://localhost:7014/api/products'처럼 Web Service가 동작하는 URL을 설정합니다. 그리고 Body에 raw를 선택하고 'JSON'으로 설정을 맞춘 후 다음과 같이 Product의 객체를 JSON형식으로 전달합니다.
결과로 200OK가 떨어지면 정상적으로 처리되었음을 의미하며 위에서 지정한 Product개체가 Database에 추가된 것입니다. 다시 /api/products로 URL을 요청하여 방금 추가한 Product개체가 존재하는지 확인합니다.
POST 요청에서는 요청의 Body가 JSON형식이며 Product 개체로 생성되고 Database에 추가되기 위해 Parsing이 이루어질 것입니다. 보내지는 JSON개체는 보시는 바와 같이 ProductName, UnitPrice, Price,
CategoryId, SupplierId 등의 이름과 값으로 이루어져 있기에 Product개체를 생성하기에 충분한 정보를 포함하고 있으며 ProductId 속성과 관련된 개체의 고유한 Key는 개체가 추가될 때 Database에 의해 자동으로 할당됩니다.
3. Controller를 사용한 Service 생성
위 예제처럼 Web Service생성을 위해 개별적으로 endpoint를 생성하는 것에서 각 endpoint는 응답을 생성하는 일련의 유사한 과정(Entity Framework Core Service를 가져와 database에 질의하고 응답을 위해 Content-Type header를 설정하고 개체를 JSON으로 serialize 하는 등등)이 중복적으로 존재하고 있습니다. 결과적으로 endpoint를 통해 Web Service를 생성하는 것은 이해하기 어렵고 관리하기에 난해질 수 있으며 무엇보다 Program.cs file의 Code자체가 복잡해져서 관리하기가 여러 워 질 수 있습니다.
이와 달리 더 우아하고 강력한 접근 방법은 Controller를 사용하는 것이며 이것은 Web Service를 단일 class안에서 정의될 수 있도록 합니다. Controller는 MVC Framework의 일부로서 ASP.NET Core Platform에 구축되어 endpoint가 URL을 처리하는 것과 같은 방법으로 Data처리합니다.
MVC Framework는 Model-View-Controller pattern을 구현한 것으로 Application의 구조화를 위한 방법 중 하나로 설명할 수 있습니다. 예제에서는 pattern의 3개 핵심 중 2개를 사용하고 있는데 data model (MVC에서 M)과 Controller (MVC에서 C)가 그것입니다. 추후에는 나머지 하나인 View에 관해서 Razor를 사용해 어떻게 HTML응답을 생성할 수 있는지에 대해 알아볼 것입니다.
MVC Pattern은 ASP.NET이 발전한 과정 중에서 중요한 부분에 해당하며 Web Form Model과는 완전히 다른 것으로 취급될 수 있습니다. Web Form Application은 쉽게 시작할 수 있지만 관리하기가 어렵고 개발자들로부터 HTTP 요청과 응답에 대한 상세를 숨긴 채 동작하도록 설계되었습니다. 반면 MVC Pattern은 MVC Pattern으로 쓰인 Application에 대해 강력함과 확장 가능한 구조를 제공하면서 동시에 개발자들로부터 어떠한 것도 숨기지 않습니다. MVC Framework는 ASP.NET을 활성화시키는데 크게 기여하였으며 Web Forms에 대한 지원을 떨어뜨리고 오로지 MVC Pattern을 사용하는 것에만 집중한 ASP.NET Core가 되기까지의 기반을 제공하였습니다.
또한 ASP.NET Core가 진화하면서 다른 Web Application에 대한 style도 포용하게 되었고 이런 과정에서 MVC Framework는 이제 Application을 생성하는 방법 중 하나가 되었습니다. MVC Pattern에 대한 유용성을 훼손시킬 필요는 없지만 더 이상 ASP.NET Core개발에 사용되는 중심적인 역활을 가지지 않으며 MVC Framework에서 고유하게 사용될 수 있는 기능은 Razor Page나 Blazor같은 다른 접근 방법을 통해서도 접근될 수 있습니다.
이러한 진화의 결과에서 MVC pattern은 이제 ASP.NET Core개발에서 있어서 필수가 아님을 이해하는 것이 중요합니다. MVC pattern에 관해서는 아래 Link를 통해 자세한 내용을 확인할 수 있습니다.
https://en.wikipedia.org/wiki/Model–view–controller
(1) MVC Framework 사용하기
Controller를 사용해 Web Service를 만들기 위해 우선 MVC Framework를 사용하도록 설정해야 합니다. 따라서 아래와 같이 관련된 Serivce와 Endpoint를 추가하도록 합니다. (이전에 생성한 Web Service endpoint는 더이상 유효하지 않으므로 제거하였습니다.)
builder.Services.AddSqlServer<NorthwindContext>(builder.Configuration.GetConnectionString("DefaultConnection"));
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.UseMiddleware<MyWebApp.TestMiddleware>();
AddControllers method는 MVC Framework에서 필요한 Service를 정의하며 MapControllers method는 Controller가 요청을 처리할 수 있도록 하는 route를 정의합니다. MVC framework를 위한 다른 기능의 Method들도 존재하지만 일단 예제에서는 Web Service를 위한 MVC Framework를 설정하는 Method만이 사용되었습니다.
(2) Controller 생성
Controller는 Class로서 Action으로 알려진 Method를 통해 HTTP 요청을 처리할 수 있습니다. Controller는 Application이 시작될 때 자동으로 인식되는데 Controller로 끝나는 모든 이름의 class가 Controller가 되며 Controller에서 정의한 모든 Method가 Action이 됩니다. Controller가 얼마나 간단히 만들어질 수 있는지를 알아보기 위해 Project에 Controllers라는 folder를 만들고 ProductsController.cs라는 이름의 file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Models;
namespace MyWebApp.Controller
{
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
public IEnumerable<Products> GetProducts()
{
return new Products[] {
new Products() { ProductName = "A Product" },
new Products() { ProductName = "B Product" }
};
}
[HttpGet("{id}")]
public Products GetProduct(int id)
{
return new Products() { ProductName = "C Product" };
}
}
}
ProductsController class는 MVC Framework가 Controller에서 찾는 조건을 충족합니다. Controller에서는 GetProducts()와 GetProduct()라는 이름의 Public Method를 정의하고 있는데 이 Method는 Action으로 취급됩니다.
● Base class
Controller는 Base class로부터 상속받고 있는데 이 class는 MVC Framework에 의해 제공되는 기능의 접근을 제공하고 있으며 ASP.NET Core Platform의 근간이기도 합니다. 아래 표는 ControllerBase class에서 제공되는 기본적인 속성을 나열한 것입니다.
비록 Controller가 일반적으로 ControllerBase나 Controller class로 부터 파생되기는 하지만 이것은 단순한 규칙일 뿐이며 MVC Framework는 이름이 Controller로 끝나는 class에서 파생되거나 Controller attribute로 decorate 되면서 Controller로 끝나 모든 class를 허용합니다. 물론 class에 NonController attribute를 적용할 수는 있으나 이렇게 되면 해당 class는 HTTP 요청을 받을 수 없게 됩니다.
HttpContext | 이 속성은 현재 요청의 HttpContext 개체를 반환합니다. |
ModelState | 이 속성은 Data의 Validation처리에 대한 상세를 반환합니다. |
Request | 이 속성은 현재 요청에 대한 HttpRequest 개체를 반환합니다. |
Response | 이 속성은 현재 응답에 대한 HttpResponse 개체를 반환합니다. |
RouteData | 이 속성은 routing middleware에 의해 요청 URL로 부터 추출된 Data를 반환합니다. |
User | 이 속성은 현재 요청과 관련된 사용자를 서술하는 개체를 반환합니다. |
Controller의 새로운 instance는 Action이 요청을 처리하는 매 순간마다 생성되므로 상기 속성은 현재 요청에만 관련된 것입니다.
● Controller Attribute
action method에서 지원되는 HTTP method와 URL은 Controller에 적용되는 attribute에 의해 결정됩니다. Controller의 URL은 Route attribute에 의해 특정되며 아래와 같이 class에 적용됩니다.
[Route("api/[controller]")]
public class ProductsController : ControllerBase
attribute 인수의 [controller] 부분은 Controller class의 이름에서 URL을 파생하는 데 사용됩니다. 이때 class이름에서 Controller부분은 제외한 attribute이름만이 Controller URL로 설정되므로 /api/products URL이 사용되는 것입니다.
각 action은 지원할 HTTP Method를 특정하는 attribute로 다음과 같이 decorate 됩니다.
[HttpGet]
public IEnumerable<Products> GetProducts()
Action Method에 주어진 이름은 Web Service를 위해 사용되는 Controller에게는 중요하지 않습니다. Controller의 다른 용도에서는 이름이 중요하겠지만 Web Service에서는 HTTP Method Attribute와 Route Pattern이 더 중요하게 작용합니다.
예제의 HttGet attribute는 MVC Framework에게 GetProducts() Method는 HTTP Get 요청을 처리할 것이라고 말해주고 있습니다. 아래 표는 Action에 HTTP Method를 적용할 수 있는 Attribute를 나열하고 있습니다.
HttpGet | 이 attribute는 GET을 사용하는 HTTP요청에 의해 Action이 호출될 수 있음을 특정합니다. |
HttpPost | 이 attribute는 POST을 사용하는 HTTP요청에 의해 Action이 호출될 수 있음을 특정합니다. |
HttpDelete | 이 attribute는 DELETE을 사용하는 HTTP요청에 의해 Action이 호출될 수 있음을 특정합니다. |
HttpPut | 이 attribute는 PUT을 사용하는 HTTP요청에 의해 Action이 호출될 수 있음을 특정합니다. |
HttpPatch | 이 attribute는 PATCH를 사용하는 HTTP요청에 의해 Action이 호출될 수 있음을 특정합니다. |
HttpHead | 이 attribute는 HEAD를 사용하는 HTTP요청에 의해 Action이 호출될 수 있음을 특정합니다. |
AcceptVerbs | 이 attribute는 여러 HTTP 동사를 지정하는데 사용됩니다. |
특정한 HTTP Method를 통해 Action에 attribute를 적용하는 것은 Controller의 기본 URL을 Build 하는 것에도 사용될 수 있습니다.
[HttpGet("{id}")]
public Products GetProduct(int id)
위 attribute는 MVC Framework로 하여금 GetProduct() Method가 /api/products/{id}와 같은 URL pattern에 해당하는 GET 요청을 처리할 수 있도록 합니다. Controller와 Action의 검색 처리가 이루어지는 동안 Controller에 적용된 attribute는 Controller가 처리할 URL Pattern설정을 구축하는 데 사용됩니다.
Controller를 작성할 때는 Controller가 지원하는 URL Pattern과 HTTP Method의 조합이 단 하나의 Action Method에만 Mapping 되도록 하는 것이 중요합니다. 동일한 요청이 여러 Action Method에서 처리될 수 있는 상황이면 MVC Framework는 어떤 것을 사용해야 할지 모호하게 되므로 예외가 발생할 것입니다.
GET | api/products | GetProducts |
GET | api/products/{id} | GetProduct |
예제를 통해 attribute의 조합과 이전에 Web Service를 생성하기 위해 endpoint를 사용했을 때처럼 같은 URL Pattern에서 사용한 MapGet Method가 얼마나 동일한지를 알 수 있습니다.
눈대중만으로도 POST 요청이 Application의 상태를 변경하는 동작에 사용되고 GET 요청은 단순히 특정 Data를 읽기 위해 사용된다는 것을 알 수 있습니다. 기본적인 용어에 따라 GET 요청은 안전한 상호작용(Data를 가져오는 것 이외 다른 부작용은 없으므로)이며, POST 요청은 비안전 상호작용(무엇인가를 바꾸게 되므로)이라 할 수 있습니다. 이러한 규칙은 World Wide Web Consortium(W3C)에 의해 결정됩니다.HTTP/1.1: Method Definitions (w3.org)
GET 요청의 경우 모든 정보가 URL에 포함되므로 주소 지정이 가능하고 따라서 이들 주소를 bookmark 하거나 별도로 Link를 공유할 수 있습니다. 하지만 상태를 변경하는 것으로 GET을 사용해서는 안됩니다. 유감스럽게도 많은 개발자가 HTTP규칙을 무시해오고 있지만 여러분의 Application에서는 반드시 HTTP규칙을 준수할 수 있도록 제작할 것을 권장합니다.
● Action Method Result
Controller에서 제공하는 주요 이점 중 하나는 MVC Framework가 응답 Header의 설정을 담당하고 client로 보내는 Data객체를 serializing 한다는 것입니다. 이것은 아래와 같이 Action Method에 정의된 Result로 확인할 수 있습니다.
[HttpGet("{id}")]
public Products GetProduct(int id)
endpoint를 사용했을 때는 JSON Serializer를 통해 직접적으로 응답을 작성할 수 있는 문자열을 생성하고 client에게 응답에 JSON data를 포함하고 있음을 알라기 위해 Content-Type header를 설정하였습니다. 그러나 예제에서의 Action method는 Product개체를 반환하고 있고 이것은 자동으로 처리됩니다.
● Controller에서의 의존성 주입 사용
Controller의 새로운 Instance는 Action이 요청을 처리할 때마다 생성됩니다. Application의 Service는 Controller의 생성자를 통해 선언된 모든 의존성과 Action Method에서 정의한 모든 의존성을 resove 하는 데 사용됩니다. 이것은 모든 Action에서 필요로 하는 Service를 생성자를 통해 처리할 수 있도록 하며 또한 각각의 Action은 자신만의 의존성을 선언할 수 있습니다.
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Models;
namespace MyWebApp.Controller
{
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly NorthwindContext _context;
public ProductsController(NorthwindContext context)
{
_context= context;
}
[HttpGet]
public IEnumerable<Products> GetProducts()
{
return _context.Products;
}
[HttpGet("{id}")]
public Products? GetProduct([FromServices] ILogger<ProductsController> logger, int id)
{
logger.LogInformation("Action Invoked");
return _context.Products.Where(x => x.ProductId== id).SingleOrDefault();
}
}
}
생성자는 NorthwindContext Server의 의존성을 선언하고 있으며 이를 통해 Application의 Data로의 접근을 제공하고 있습니다. Service는 요청 scope를 사용해 resove 되는데 이것은 Controller가 생명주기를 따로 고려하지 않고 모든 Service를 요청할 수 있음을 의미합니다.
Entity Framework Core Context의 새로운 개체는 각 Controller마다 생성됩니다. 어떤 개발자의 경우 성능 향상을 이유로 Context 개체의 재사용을 시도하는데 이것은 하나의 질의로부터 가져온 Data가 다음 질의에 영향을 줄 수 있기 때문에 문제를 유발할 수 있습니다. 우리가 모르게 Entity Framework Core는 Database로의 연결을 효휼적으로 관리하고 있으므로 굳이 Controller외부에서 context 개체를 재사용하거나 따로 저장하려고 시도할 필요가 없습니다.
GetProducts action method는 NorthwindContext를 database에서 모든 Product개체를 요청하는 데 사용하고 있습니다. GetProduct action method도 역시 NorthwindContext Service를 사용하고 있지만 logging service인 ILogger<T>의 의존성을 같이 선언하고 있습니다. action method에서 선언된 의존성은 반드시 [FromServices]로 decorate 되어야 합니다.
기본적으로 MVC Framework는 요청 URL로부터 Action method의 매개변수에 필요한 값을 가져오려고 시도하는데 [From Serivce] Attribute는 이러한 동작을 override 하는 것입니다. Controller에서 Serivce가 사용되는 것을 확인해 보기 위해 Project를 실행하고 /api/products/1로 URL을 요청합니다. 그러면 다음과 같은 응답을 보일 것입니다.
또한 Application의 실행 Console을 통해서는 다음과 같은 Log도 같이 확인해 볼 수 있습니다.
Controller 생명주기의 한 가지 결과는 특정 순서로 호출되는 Method에 의한 부작용에 의존할 수 없습니다. 예를 들어 예제에서 GetProduct Method에 의해 전달된 ILogger<T>개체를 요청 이후 GetProducts method에서 읽어 들일 수 있도록 내부 속성에 할당할 수 없습니다. 각 Controller개체는 요청을 처리하는 데 사용되며 오로지 하나의 Action method만이 각 개체에 대해 MVC Framework에서 호출될 수 있습니다.
● Model Binding을 사용한 Route Data 접근
위에서는 잠깐 'MVC Framework가 요청 URL로부터 Action method의 매개변수에 필요한 값을 가져오려고 시도한다.'라는 말을 언급한 적이 있습니다. 이것은 Model Binding으로 알려진 것인데 추후에 자세히 살펴볼 테지만 우선은 예제를 통해 간단히 구현해 보았습니다.
[HttpGet("{id}")]
public Products? GetProduct([FromServices] ILogger<ProductsController> logger, int id)
{
logger.LogInformation("Action Invoked");
return _context.Products.Where(x => x.ProductId== id).SingleOrDefault();
}
예제에서는 int형의 id를 GetProduct Method에서 사용하고 있는데 이를 통해 Action method가 호출될 때 MVC Framework는 routing data로부터 자동으로 int형값으로 변환해 같은 이름을 기준으로 값을 주입하게 되고 해당 값은 LINQ method를 사용해 Database로 질의를 수행하여 결과를 가져오는 데 사용됩니다.
● Request Body로부터 Model Binding
Model binding은 또한 요청 Body의 Data를 통해서도 사용될 수 있는데 이 것은 client가 보낸 Data를 Action method에서 쉽게 받을 수 있도록 합니다. 아래 예제는 POST 요청에 대응하는 새로운 Action method를 추가한 것입니다. 이 Method를 통해 client는 Product개체를 요청 body를 통해 JSON표현식으로 제공할 수 있습니다.
[HttpPost]
public void SaveProduct([FromBody] Products product)
{
_context.Products.Add(product);
_context.SaveChanges();
}
새로운 Action method는 2개의 attribute에 의존하고 있습니다. HttpPost attribute는 Action method에 적용되며 MVC Framework로부터 Action method가 POST 요청을 처리할 것임을 알려주고 있습니다. FromBody Attribute는 Action method의 매개변수에 적용되는 것으로 해당 매개변수가 요청 body로부터 parsing 하여 가져올 값을 특정하며 Action method가 호출될 때 MVC Framework는 새로운 product 개체를 생성하고 요청 body의 값을 통해 해당 개체의 속성을 채우게 됩니다. Model binding처리는 복잡하고 Data 유효성 검증과도 조합되지만 예제를 실행함으로써 간단히 결과를 확인해 볼 수 있습니다. 일단 project를 실행한 뒤 postman을 통해 아래와 같이 URL와 body를 설정하고 해당 요청을 보냅니다.
해당 요청의 결과로 200OK가 나온다면 성공한 것입니다. /api/products로 URL을 요청해 결과를 확인합니다.
● Action 추가하기
계속해서 예제를 다듬어 HTTP PUT과 DELETE Method를 통해 Product 개체를 변경하고 삭제하기 위한 Action을 추가해 볼 것입니다.
[HttpPut]
public void UpdateProduct([FromBody] Products product)
{
_context.Products.Update(product);
_context.SaveChanges();
}
[HttpDelete("{id}")]
public void DeleteProduct(int id)
{
_context.Products.Remove(new Products() { ProductId = id });
_context.SaveChanges();
}
UpdateProduct Action은 SaveProduct Action과 비슷하게 Model Binding을 사용해 요청 Body로부터 Product 개체를 전달받고 있습니다. DeleteProduct Action은 URL에서 Primary key 값을 전달받고 해당 key만을 가진 Product 개체를 생성하고 있습니다. 이는 Entity Framework Core가 작동하기 위해 개체를 필요로 하기 때문이며 이를 통해 client가 key값만을 전달하는 것으로 Product개체의 삭제를 구현할 수 있게 됩니다.
Project를 실행하여 postman을 통해 변경할 product를 아래와 같이 설정합니다.
200OK결과가 나오면 postman에서 새로운 Tab을 추가하여 /api/products/79와 같이 방금 변경한 Product의 ID를 지정하고 해당 Product 개체를 확인합니다.
Action method는 Model Binding 기능을 통해 개체를 전달받아 Database를 Update 하게 됩니다. 이번에는 DELETE Method를 지정하여 마지막에 추가한 Product개체를 삭제해 봅니다.
결과를 확인합니다.
4. Web Service 향상
위의 예제는 기능적으로는 Product 개체를 다루기에 충분하지만 좀 더 개선될 수 있는 여지도 존재하고 있습니다. 이러한 개선점을 하나씩 짚어보도록 하겠습니다.
만약 위 Web Service를 제삼자 javascript client에 지원해야 한다면 해당 Web Serivce에 CORS(cross-origin requests)를 적용해야 할 수도 있습니다. 왜냐하면 Browser는 기본적으로 같은 출처(origin)에서 만들어진 HTTP 요청만을 수용함으로써 client를 보호하고 있기 때문입니다. 여기서 같은 origin이라 함은 같은 Scheme, host, port로 이루어진 URL을 의미합니다. 느슨한 CORS는 초기 HTTP 요청으로 Server가 특정 URL로부터 발생한 요청을 허용하는지를 확인하고 악성 code를 차단함으로써 동의 없이 타인의 Service가 사용되는 것을 제한합니다.
하지만 경우에 따라 CORS 제한을 풀어야 하는 경우도 있는데 이를 위해 ASP.NET Core는 CORS를 처리하는 내장된 Service를 제공하고 있습니다.
builder.Services.AddCors();
위의 option pattern은 Microsoft.AspNetCore.Cors.Infrastructure에 정의된 CorsOptions class를 통해 CORS를 설정하는 데 사용됩니다. 자세한 사항은 아래 link를 참고하시면 됩니다.
Enable Cross-Origin Requests (CORS) in ASP.NET Core | Microsoft Learn
(1) 비동기 Action
ASP.NET Core Platform은 pool로부터 thread를 할당해 각 요청을 처리합니다. 따라서 동시에 처리되는 요청의 수는 pool의 size에 따라 제한될 수 있으며 thread는 결과가 나오기를 기다리는 동안 다른 요청을 처리하기 위해 사용될 수 없습니다.
외부 resource에 의존적인 작업은 요청 thread가 연장된 기간 동안 대기하도록 할 수 있습니다. 예컨대 Database는 자체적인 동시성 제한이 있을 수 있고 query를 실행할 수 있을 때까지 대기열에 넣을 수 있을 것입니다. ASP.NET Core 요청 thread는 Database가 HTTP Client에게 보낼 수 있는 응답을 만들어 내보낼 때 까지는 다른 요청은 처리할 수 없습니다.
이러한 문제는 ASP.NET Core thread가 차단될 경우 다른 요청을 처리할 수 있도록 하기 위해 비동기 Action을 정의하고 Application에서 동시에 처리할 수 있는 HTTP 요청의 수를 증가시킴으로써 해결될 수 있습니다. 아래 예제는 상기 예제의 Controller를 개선하여 비동기 Action을 사용한 것입니다.
비동기 자체가 응답을 빠르게 생성하는 것이 아닙니다. 비동기의 주된 목적은 동시에 처리 가능한 요청의 수를 증가시키는 것입니다.
[HttpGet]
public IAsyncEnumerable<Products> GetProducts()
{
return _context.Products.AsAsyncEnumerable();
}
[HttpGet("{id}")]
public async Task<Products?> GetProduct([FromServices] ILogger<ProductsController> logger, int id)
{
logger.LogInformation("Action Invoked");
return await _context.Products.Where(x => x.ProductId == id).SingleOrDefaultAsync();
}
[HttpPost]
public async Task SaveProduct([FromBody] Products product)
{
await _context.Products.AddAsync(product);
await _context.SaveChangesAsync();
}
[HttpPut]
public async Task UpdateProduct([FromBody] Products product)
{
_context.Products.Update(product);
await _context.SaveChangesAsync();
}
[HttpDelete("{id}")]
public async Task DeleteProduct(int id)
{
_context.Products.Remove(new Products() { ProductId = id });
await _context.SaveChangesAsync();
}
Entity Framework Core는 몇몇 Method에서 AddAsync, SaveChangesAsync 등의 비동기 Version을 제공하고 있으며 await keyword와 함께 사용되었습니다. 물론 모든 동작에서 비동기를 수행할 수 있는 것은 아닙니다. 예제에서 Update와 Remove가 그것인데 UpdateProduct와 DeleteProduct Action안에서는 변경되지 않기 때문입니다.
Database로의 LINQ 질의를 포함하여 몇몇 동작에서는 IAsyncEnumerable<T> interface가 사용될 수 있으며 이것은 비동기적으로 열거되어야 하는 개체의 연속성을 표시하고 ASP.NET Core 요청 thread가 Database에 의해 생성되는 각 개체를 위해 대기하게 되는 것을 방지합니다.
Controller에 의해 생성되는 응답에는 변화가 없지만 ASP.NET Core가 각 요청을 처리하기 위해 할당한 thread가 Action Method에 의해 차단되는 경우는 피할 수 있게 되었습니다.
(2) Over-Binding 방지하기
예제의 일부 Method에서는 Model Binding기능을 통해 요청 Body로부터 Data를 가져와 Database로의 동작을 수행하고 있습니다. 그런데 SaveProduct action method에는 문제가 하나 있습니다. 예를 들어 다음과 같이 Product 개체를 추가할 때 ProductId값을 고의적으로 전달하게 되면
IDENTITY_INSERT Column에는 값을 insert 할 수 없다는 오류를 보게 됩니다. 기본적으로 Entity Framework Core는 새로운 개체를 저장할 때 Database에 primary key값을 할당하도록 구성합니다. 다시 말해 Application은 이미 할당된 Key의 값을 계속 추적할 필요가 없으며 키의 할당을 조정할 필요 없이 같은 Database를 여러 Application에서 공유할 수 있습니다. 이때 Product data model class는 ProductId 속성을 가지고 있고 Model binding은 속성의 의미를 따로 고려하지 않고 처리하므로 client가 제공한 어떤 값이든 개체를 생성할 때 해당 값으로의 추가를 시도하게 됩니다. 그러나 이 과정에서 결국 SaveProduct Method에서 예외를 발생시키게 되는 것입니다.
이것은 흔히 over-binding으로 알려져 있으며 client가 제공한 값이 예상치를 벗어나게 되는 경우 심각한 문제를 유발할 수 있습니다. 문제는 자칫 Application이 예상치 못한 동작을 수행함으로써 Application 보안을 손상시키거나 사용자에게 본래 가질 수 있는 권한보다 더 많은 접근을 허용하는 경우가 발생할 수 있습니다.
Over-binding을 막기 위한 안전한 방법은 분리된 Data model class를 생성하는 것이며 이 class를 Model binding처리를 통해 Data를 수신하는 목적으로만 사용하는 것입니다. 예제를 위해 아래와 같이 ProductsBinding.cs라는 이름의 file을 Project의 Models folder에 추가합니다.
namespace MyWebApp.Models
{
public class ProductsBinding
{
public string ProductName { get; set; } = "";
public decimal UnitPrice { get; set; }
public Products ToProduct() => new Products()
{
ProductName = this.ProductName,
UnitPrice = this.UnitPrice
};
}
}
예제는 오로지 Application이 새로운 개체를 생성하기 위해 client로부터 전달받기를 원하는 속성만 정의하고 있습니다. ToProduct() Method는 Application에서 필요로 하는 최소한의 정보만 가지고 Product개체를 생성하고 있으며 동시에 client가 ProductName, UnitPrice속성만을 제공할 것임을 보증하게 됩니다. 이어서 SaveProduct Action Method를 수정해 ProductsBinding class를 사용하도록 함으로써 Over-Binding을 방지하도록 합니다.
[HttpPost]
public async Task SaveProduct([FromBody] ProductsBinding product)
{
await _context.Products.AddAsync(product.ToProduct());
await _context.SaveChangesAsync();
}
Project를 실행하고 postman에서 이전에 사용했던 것과 동일한 값으로 SaveProduct의 POST를 시도합니다.
요청 값은 여전히 ProductId를 포함하고 있지만 Model Binding처리에 의해 해당 값은 무시되고 Product개체의 생성은 정상적으로 이루어져 Database에 반영될 것입니다.
(3) Action Result 사용
ASP.NET Core는 응답에 대한 상태 Code를 자동으로 설정하지만 항상 원하는 결과를 얻을 수는 없습니다. 이 부분에서는 RESTful web service를 위한 확실한 규칙은 없으며 Microsoft가 만든 예상 결과는 기대한 것과 일치하지 않을 수 있습니다. 예를 들어 postman에서 /api/products/999 이라는 URL로 GET 요청을 했을 때 상태 코드는 아래와 같이 나올 것입니다.
상기 URL요청은 GetProduct Action method에 의해 처리되며 Database로부터 ProductId값이 999인 개체를 가져오기 위한 Query를 수행하고 상태 code 204와 함께 그 결과를 표시하게 됩니다. 그러나 조건과 일치하는 개체는 존재하지 않으며 GetProduct Method는 NULL을 반환하게 됩니다. MVC Framework는 Action Method로부터 Null을 전달받게 되면 204 상태 code를 반환하는데 이는 요청은 성공적으로 수행했으나 반환할 Data가 존재하지 않는다는 것을 의미합니다. 그러나 모든 Web Service를 이렇게 동작하는 것은 아니며 이에 대한 다른 대안으로 '찾을 수 없음'을 나타내는 404를 반환하기도 합니다.
비슷하게 SaveProducts method의 경우에도 개체를 저장하게 되면 상태 code 200을 반환하겠지만 Data가 저장되는 동안에는 Primary Key가 생성되지 않으므로 client는 할당된 key값을 알지 못합니다.
Web Serivce를 세부적으로 구현할 때 무엇을 어떻게 해야 한다는 정확한 명확한 기준은 존재하지 않습니다. 그저 Project상황에 맞게 가장 좋은 접근법을 선택해야 하는 것입니다. 제시된 예제는 기본 동작을 바꾸는 방법을 안내하기 위한 것이지 Web Serivce구현 시 특정 style을 따라야 함을 지시하는 것이 아닙니다.
Action method는 MVC Framework에게 action result로 알려진 IActionResult interface를 구현한 개체를 반환함으로써 특정한 응답을 보내도록 지시할 수 있는데 이것으로 Action method는 HttpResponse 개체를 사용해 직접 만들지 않고도 필요한 응답의 유형을 특정할 수 있도록 합니다.
이를 위해 ControllerBase class는 아래와 같은 일련의 Method들을 제공하여 Action method로부터 반환될 수 있는 Action 결과 개체를 만들 수 있도록 지원하고 있습니다.
Ok | 이 Method에 의해 반환된 IActionResult는 200 OK 상태 code를 만들고 응답 body에 선택적 Data개체를 보냅니다. |
NoContent | 이 Method에 의해 반환된 IActionResult는 204 No Content 상태 code를 생성합니다. |
BadRequest | 이 Method에 의해 반환된 IActionResult는 400 bad request 상태 code를 생성합니다. 또한 client에게 문제점을 서술하는 선택적 Model State수용할 수 있습니다. |
File | 이 Method에 의해 반환된 IActionResult는 200 OK 상태 code를 생성하며 Content-Type header에 특정된 type을 설정하여 지정된 File을 client에게 전송합니다. |
NotFound | 이 Method에 의해 반환된 IActionResult는 404 Not Found 상태 code를 생성합니다. |
Redirect / RedirectPermanent | 이 Method에 의해 반환된 IActionResult는 Client를 특정 URL로 Redirect합니다. |
RedirectToRoute / RedirectToRoutePermanent | 이 Method에 의해 반환된 IActionResult는 Client를 특정 URL로 Redirect합니다. 다만 URL은 convention routing을 사용한 Routing System을 통해 생성됩니다. |
LocalRedirect / LocalRedirectPermanent | 이 Method에 의해 반환된 IActionResult는 Client를 Application의 Locald인 특정 URL로 Redirect합니다. |
RedirectToAction / RedirectToActionPermanent | 이 Method에 의해 반환된 IActionResult는 Client를 Action Method로 Redirect합니다. 이때 Redirection을 위한 URL은 URL Routing System을 통해 생성됩니다. |
RedirectToPage / RedirectToPagePermanent | 이 Method에 의해 반환된 IActionResult는 Client를 Razor Page로 Redirect합니다. |
StatusCode | 이 Method에 의해 반환된 IActionResult는 특정한 상태 code를 통해 응답을 생성합니다. |
Action method가 개체를 반환할 때는 개체를 Ok Method에 전달하고 결과를 반환하는 것과 동일하게 처리되며 Action이 Null을 반환할때는 NoContent Method를 통해 결과를 반환하는 것과 동일하게 처리됩니다. 아래 예제는 GetProduct와 SaveProduct Action을 개선하여 상기 Method를 사용하도록 함으로써 Web Service Controller의 기본 동작이 override 되도록 하였습니다.
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
Products? p = await _context.Products.FindAsync(id);
if (p == null)
return NotFound();
return Ok(p);
}
[HttpPost]
public async Task<IActionResult> SaveProduct([FromBody] ProductsBinding product)
{
Products p = product.ToProduct();
await _context.Products.AddAsync(p);
await _context.SaveChangesAsync();
return Ok(p);
}
Project를 실행하여 postman을 통해 이전과 동일하게 GetProduct Method를 실행하게 되면 수정된 Code에 따라 404 Not Found 상태 코드가 반환될 것입니다.
SaveProdcut() Method도 마찬가지인데 저장하려는 Product개체를 JSON Format으로 전달하면 해당 개체를 Database에 저장한 뒤 생성된 개체를 다시 반환하고 있음을 볼 수 있습니다.
● Redirection
Redirection과 관련된 대부분의 Action Result Method는 Client를 다른 URL로 Redirect 합니다. 하지만 이 외에 Redirection을 수행하는 가장 기본적인 방법은 아래와 같이 Redirect Method를 호출하는 것입니다.
LocalRedirect와 LocalRedirect Method는 Controller가 Local이 아닌 다른 URL로 Redirect를 수행을 시도하면 예외를 발생시키게 됩니다. 이러한 동작은 사용자에 의해 제공된 URL로 Redirect 할 때 유용하게 사용될 수 있는데, 다른 사용자를 신뢰할 수 없는 site로 Reidrect를 시도하는 open redirection 공격을 방지할 수 있기 때문입니다.
[HttpGet("redirect")]
public IActionResult Redirect()
{
return Redirect("/api/products/1");
}
redirection URL은 string형식의 문자열로서 Redirect Method의 인수로 전달됩니다. Project를 실행하여 /api/products/redirect로 URL을 요청하면 다음과 같은 결과를 볼 수 있습니다.
● Action Method로의 Redirect
RedirectToAction 또는 RedirectToActionPermanent Method를 사용하면 다른 Action method로 Redirect를 수행할 수 있습니다. 아래 수정된 예제는 client를 Controller에 정의된 다른 Action method로 Redirect 하도록 합니다.
[HttpGet("redirect")]
public IActionResult Redirect()
{
return RedirectToAction(nameof(GetProduct), new { Id = 1 });
}
Action method는 nameof 표현식을 통해 Redirect 할 Action method를 지정하고 있는데 이것으로 오타 없이 특정 문자열을 지정할 수 있습니다. 또한 route를 생성하는데 필요한 다른 추가적인 값은 익명 개체를 사용해 적용하였습니다. 해당 Action method의 실행결과는 이전 결과와 같습니다.
만약 단순히 특정 Action method를 특정하게 된다면 redirection은 현재 controller를 대상으로 하게 됩니다. 그러나 RedirectToAction Action method를 overload 하게 된다면 Controller 이름까지도 명시할 수 있습니다.
RedirectToRoute와 RedirectToRoutePermanent method는 client를 routing system에 segment 변숫값을 제공하고 사용할 route를 선택할 수 있도록 하여 생성된 URL로 redirect 하게 됩니다. 이 것은 복잡한 routing system이 설정된 Application에 유용하게 사용될 수 있으나 잘못된 URL로 쉽게 redirect 될 수 있으므로 주의가 필요합니다.
[HttpGet("redirect")]
public IActionResult Redirect()
{
return RedirectToRoute(new
{
controller = "Products",
action = "GetProduct",
Id = 1
});
}
이 Redirection의 일련의 값들은 Controller와 Action method를 선택하기 위해 규약 routing에 의존하고 있습니다. 규약 routing은 일반적으로 HTML 응답을 생성하는 Controller와 함께 사용됩니다.
(4) Data Validation
client로부터 Data가 들어올 때는 일부 혹은 많은 Data가 잘못될 수 있다고 가정하고 Application이 사용할 수 없는 Data를 걸러내기 위한 준비를 해야 합니다. MVC Framework Controller에 제공하는 Data Validation기능은 추후에 자세히 설명할 것이며 우선은 client가 Database에 저장하는데 필요한 속성에 값을 제공하도록 보장해야 한다는 문제에 초첨을 맞춰보고자 합니다. 이를 위해 Model binding의 Data Model Class속성에 아래와 같이 attribute를 적용합니다.
using System.ComponentModel.DataAnnotations;
namespace MyWebApp.Models
{
public class ProductsBinding
{
[Required]
public string ProductName { get; set; } = "";
[Range(0, 100)]
public decimal UnitPrice { get; set; }
public Products ToProduct() => new Products()
{
ProductName = this.ProductName,
UnitPrice = this.UnitPrice
};
}
}
Required Attribute는 속성에 Client가 반드시 값을 제공해야 한다는 것을 나타내며 요청에 값이 없을 경우 Null이 할당된 속성에 적용될 수 있습니다. Range Attribute는 값이 설정한 최소와 최대 치안에 들어와야 한다는 것을 의미하며 요청에 값이 없는 경우 기본값 0이 될 수 있는 원시 Type에 사용할 수 있습니다.
아래 예제는 SaveProduct action를 변경하여 Model Binding Process에 의해 생성된 개체를 저장하기 전에 validation이 수행되도록 하였습니다. 이때 validation attribute를 적용한 속성에는 무조건 값을 포함하게 됩니다.
[HttpPost]
public async Task<IActionResult> SaveProduct([FromBody] ProductsBinding product)
{
if (ModelState.IsValid)
{
Products p = product.ToProduct();
await _context.Products.AddAsync(p);
await _context.SaveChangesAsync();
return Ok(p);
}
return BadRequest(ModelState);
}
ModelState 속성은 ControllerBase class로부터 상속되며 IsValid 속성은 model binding process가 Validation조건을 만족하는 Data를 생성하게 되면 true값을 반환하게 됩니다. client로부터 수신한 Data가 정확한 경우 Ok method로부터 action result가 반환될 테지만 Data가 Validation에 확인에 실패하게 된다면 IsValid 속성은 false를 반환하게 될 것이며 BadRequest method로부터의 action result가 대신 사용될 것입니다. BadRequest method는 ModelState 속성에 의해 반환된 개체를 받아 client에게 Validation의 Error상황을 설명하는 데 사용됩니다.(Validation Error를 서술하기 위한 표준적인 방법이라는 건 없으며 client는 문제가 있음을 400 상태 code를 통해 확인할 수 있습니다.)
Validation 확인을 위해 Project를 실행하고 postman에서 아래와 같이 POST를 전송합니다.
실행결과 Web Service는 400 Bad Request를 반환하였으며 어떤 것이 문제가 되는지에 대한 상세를 표시하고 있습니다.
(5) API Controller Attribute
ApiController attribute는 Web Service Controller class에 적용할 수 있으며 Model bindin과 Validation 기능에 대한 동작을 바꿀 수 있습니다. 요청 Body로부터 Data를 가져오고 ModelState.IsValid속성을 명시적으로 확인할 때 ApiController attribute가 Decorate 된 Controller에서는 굳이 FromBody Attribute를 사용할 필요가 없습니다. body로부터 Data를 가져와 Data를 Validating 하는 것은 일반적인 Web Service에서 매우 필요한 것이므로 Attribute가 사용될 때 자동으로 적용됩니다.
[HttpPost]
public async Task<IActionResult> SaveProduct(ProductsBinding target)
{
Products p = target.ToProduct();
await _context.Products.AddAsync(p);
await _context.SaveChangesAsync();
return Ok(p);
}
[HttpPut]
public async Task UpdateProduct(Products product)
{
_context.Products.Update(product);
await _context.SaveChangesAsync();
}
ApiController Attribute는 선택적이지만 web service controller를 간소화하는데 도움이 될 수 있습니다.
(6) Null 속성 제외하기
필요하다면 Web Service로부터 Data가 반환될때 필요한 속성만을 노출함으로서 Null을 가진 속성은 제외할 수 있습니다. 예를 들어 postman에서 다음과 같이 요청을 수행한 경우
GET 요청이 보내지고 Web Service로 부터 위와 같은 응답이 표시될 것입니다. GetProduct action method에 의해 처리된 요청은 응답에서 category와 supplier가 null이 되었습니다. 이들 속성은 navigation속성으로서 복잡한 질의가 수행되는 경우 Entity Framework Core가 Data를 관련시키기 위해 사용됩니다. 하지만 예제와 같은 간단한 질의의 경우에는 어떠한 값도 이들 navation 속성에 할당되지 않는데 이로 인해 client는 사용할 수 없는 값을 가진 속성을 같이 받게 되는 것입니다.
● 특정 속성만 선택하기
위와 같은 문제를 피할 수 있는 첫 번째 방법은 Client가 필요한 속성만을 반환하는 것입니다. 이러한 방법은 각 응답을 완전히 제어하는 것이지만 관리하기에 어렵고 client개발자들에게는 각 Action에서 서로 다른 일련의 값을 반환함으로써 혼란을 가져올 수 있습니다.
아래 예제에서는 Database로부터 가져온 Product 개체에서 특정한 속성만을 선택함으로써 필요 없는 다른 속성은 생략하도록 하였습니다.
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
Products? p = await _context.Products.FindAsync(id);
if (p == null)
return NotFound();
return Ok(new { ProductName = p.ProductName, UnitPrice = p.UnitPrice });
}
속성이 선택되고 OK method로 전달되는 개체에 추가되고 결과적으로 navigation속성과 null값이 생략된 응답을 받을 수 있게 되었습니다.
● JSON Serializer 구성하기
개체를 serialize 할 때 특정 속성을 생략하기 위한 목적으로 JsonIgnore attribute를 설정할 수 있습니다. 아래 예제는 실제 속성에 JsonIgnore를 설정하는 방법을 나타내고 있습니다.
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MyWebApp.Models
{
public partial class Products
{
public Products()
{
OrderDetails = new HashSet<OrderDetails>();
}
public int ProductId { get; set; }
public string ProductName { get; set; }
public int? SupplierId { get; set; }
public int? CategoryId { get; set; }
public string QuantityPerUnit { get; set; }
public decimal? UnitPrice { get; set; }
public short? UnitsInStock { get; set; }
public short? UnitsOnOrder { get; set; }
public short? ReorderLevel { get; set; }
public bool Discontinued { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public virtual Categories Category { get; set; }
public virtual Suppliers Supplier { get; set; }
public virtual ICollection<OrderDetails> OrderDetails { get; set; }
}
}
예제에서 사용된 Condition 속성에는 JsonIgnoreCondition값이 할당되었으며 기타 아래 표의 값이 사용될 수 있습니다.
Always | 개체가 serialize될때 항상 해당 속성은 무시하도록 합니다. |
Never | 개체가 serialize될때 해당 속성이 포함되도록 합니다. JsonIgnore attribute를 설정하지 않은것과 같습니다. |
WhenWritingDefault | 개체가 serialize될때 해당 속성의 값이 null이거나 해당 속성의 type에 대한 기본값인 경우에만 무시하도록 합니다. |
WhenWritingNull | 개체가 serialize될때 해당 속성의 값이 null인 경우에만 무시하도록 합니다. |
예제에서 JsonIgnore attribute는 WhenWritingNull값을 통해 적용되었으므로 Categories속성은 해당 속성의 값이 null인 경우에 serialize대상에서 제외될 것입니다. 결과를 보기 위해 위에서 변경했던 GetProduct method를 다시 아래와 같이 되돌립니다.
[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
Products? p = await _context.Products.FindAsync(id);
if (p == null)
return NotFound();
return Ok(p);
}
Project를 실행하여 postman을 통해 아래와 같이 요청하게 되면
예상한 대로 Categories속성은 응답에 포함되어 있지 않음을 알 수 있습니다. attribute는 model class에 적용될 수 있으며 제외되어야 할 속성이 비교적 작은 경우에 유용하게 사용될 수 있으나 대상이 될 model class가 많고 그에 따른 많은 속성을 모두 고려애햐 한다면 attribute를 사용하기에 부담이 될 수 있습니다. 이런 경우 option pattern을 통해 serialization을 위한 일반적인 정책을 정의할 수 있습니다.
builder.Services.AddControllers();
builder.Services.Configure<JsonOptions>(opts => {
opts.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
var app = builder.Build();
JSON serializer는 JsonOptions class의 JsonSerializerOptions속성을 사용해 설정되었으며 null값은 DefaultIgnoreCondition속성을 통해 관리되는데 이때 설정값은 JsonIgnoreCondition의 값 중 하나가 될 수 있습니다. (options pattern에서 Always값은 아무런 의미가 없으며 ASP.NET Core가 시작될 때 예외를 유발할 수 있습니다.)
예제에서의 설정은 모든 JSON 응답에 영향을 주게 될 것이며 주의 깊게 사용되어야 합니다. 특히, Data Model Class가 null값을 사용하는데 이러한 속성 자체가 client에게 중요한 정보로 취급될 수 있는 경우에는 더 그렇습니다. 해당 설정이 어떻게 적용되는지를 알아보기 위해 이번에는 postman을 통해 /api/products URL로 GET 요청을 수행합니다.
결과를 보면 어떠한 개체도 null값을 가진 속성을 포함하지 않음을 알 수 있습니다.
JsonIgnore attribute는 위에서 설정한 정책을 override 할 수 있습니다. 따라서 Application전체적으로 정책을 적용함과 동시에 특정 속성이 null이가 기본값을 가지고 있는 경우 예외적으로 응답에 포함시킬 수 있습니다.