팁 (조언)
이 콘텐츠는 .NET Docs 또는 오프라인으로 읽을 수 있는 무료 다운로드 가능한 PDF로 제공되는 eBook인 'ASP.NET Core와 Azure로 현대 웹 애플리케이션 설계하기'에서 발췌한 내용입니다.
"처음 제대로 하는 것은 중요하지 않습니다. 마지막에 제대로 하는 것이 매우 중요합니다." - 앤드류 헌트와 데이비드 토마스
ASP.NET Core는 최신 클라우드 최적화 웹 애플리케이션을 빌드하기 위한 플랫폼 간 오픈 소스 프레임워크입니다. ASP.NET Core 앱은 가볍고 모듈식이며 종속성 주입을 기본적으로 지원하므로 테스트 용이성과 유지 관리 효율성이 향상됩니다. 보기 기반 앱 외에도 최신 웹 API 빌드를 지원하는 MVC와 결합된 ASP.NET Core는 엔터프라이즈 웹 애플리케이션을 빌드하는 강력한 프레임워크입니다.
MVC 및 Razor 페이지
ASP.NET Core MVC는 웹 기반 API 및 앱을 빌드하는 데 유용한 많은 기능을 제공합니다. MVC라는 용어는 사용자 요청에 응답하는 책임을 여러 부분으로 나누는 UI 패턴인 "Model-View-Controller"를 의미합니다. 이 패턴을 따르는 것 외에도 ASP.NET Core 앱의 기능을 Razor Pages로 구현할 수도 있습니다.
Razor Pages는 ASP.NET Core MVC에 기본 제공되며 라우팅, 모델 바인딩, 필터, 권한 부여 등에 동일한 기능을 사용합니다. 그러나 컨트롤러, 모델, 뷰 등에 대해 별도의 폴더와 파일을 사용하고 특성 기반 라우팅을 사용하는 대신 Razor Pages는 단일 폴더("/Pages")에 배치되고, 이 폴더의 상대 위치에 따라 라우팅되고, 컨트롤러 작업 대신 처리기로 요청을 처리합니다. 따라서 Razor Pages로 작업할 때 필요한 모든 파일과 클래스는 일반적으로 웹 프로젝트 전체에 분산되지 않고 공동 배치됩니다.
eShopOnWeb 샘플 애플리케이션에서 MVC, Razor 페이지 및 관련 패턴을 적용하는 방법에 대해 자세히 알아봅니다.
새 ASP.NET Core 앱을 만들 때 빌드하려는 앱 종류에 대한 계획을 염두에 두어야 합니다. 새 프로젝트를 만들 때 IDE 또는 CLI 명령을 사용하여 dotnet new 여러 템플릿 중에서 선택합니다. 가장 일반적인 프로젝트 템플릿은 비어 있음, Web API, 웹앱 및 웹앱(모델View-Controller)입니다. 프로젝트를 처음 만들 때만 이 결정을 내릴 수 있지만 취소할 수 없는 결정은 아닙니다. Web API 프로젝트는 표준 모델View-Controller 컨트롤러를 사용하며, 기본적으로 뷰가 없습니다. 마찬가지로 기본 웹앱 템플릿은 Razor Pages를 사용하므로 Views 폴더도 부족합니다. 뷰 기반 동작을 지원하기 위해 나중에 이러한 프로젝트에 Views 폴더를 추가할 수 있습니다. Web API 및 Model-View-Controller 프로젝트에는 기본적으로 Pages 폴더가 포함되지 않지만 나중에 추가하여 Razor Pages 기반 동작을 지원할 수 있습니다. 이러한 세 가지 템플릿은 데이터(웹 API), 페이지 기반 및 보기 기반의 세 가지 기본 사용자 상호 작용을 지원하는 것으로 생각할 수 있습니다. 그러나 원하는 경우 단일 프로젝트 내에서 이러한 템플릿을 모두 혼합하고 일치시킬 수 있습니다.
Razor Pages를 사용하는 이유
Razor Pages는 Visual Studio의 새 웹 애플리케이션에 대한 기본 방법입니다. Razor Pages는 SPA가 아닌 양식과 같은 페이지 기반 애플리케이션 기능을 빌드하는 더 간단한 방법을 제공합니다. 컨트롤러와 뷰를 사용하면 애플리케이션에 다양한 종속성 및 뷰 모델을 사용하고 다양한 보기를 반환하는 매우 큰 컨트롤러가 있는 것이 일반적이었습니다. 이로 인해 복잡성이 더 늘어나고 단일 책임 원칙 또는 개방/폐쇄 원칙을 효과적으로 따르지 않는 컨트롤러가 발생하는 경우가 많습니다. Razor Pages는 Razor 태그를 사용하여 웹 애플리케이션에서 지정된 논리적 "페이지"에 대한 서버 쪽 논리를 캡슐화하여 이 문제를 해결합니다. 서버 쪽 논리가 없는 Razor 페이지는 Razor 파일(예: "Index.cshtml")으로만 구성될 수 있습니다. 그러나 대부분의 비평범한 Razor Pages에는 관련 페이지 모델 클래스가 포함되어 있으며, 관례적으로 Razor 파일과 같은 이름을 사용하고 ".cs" 확장명이 붙습니다(예: "Index.cshtml.cs").
Razor 페이지의 페이지 모델은 MVC 컨트롤러와 뷰모델의 역할을 함께 수행합니다. 컨트롤러 작업 메서드를 사용하여 요청을 처리하는 대신 "OnGet()"과 같은 페이지 모델 처리기가 실행되어 기본적으로 연결된 페이지를 렌더링합니다. Razor Pages는 ASP.NET Core 앱에서 개별 페이지를 빌드하는 프로세스를 간소화하는 동시에 ASP.NET Core MVC의 모든 아키텍처 기능을 제공합니다. 새 페이지 기반 기능에 적합한 기본 선택입니다.
MVC를 사용하는 경우
웹 API를 빌드하는 경우 MVC 패턴은 Razor Pages를 사용하는 것보다 더 합리적입니다. 프로젝트에서 웹 API 엔드포인트만 노출하는 경우 Web API 프로젝트 템플릿에서 시작하는 것이 좋습니다. 그렇지 않으면 컨트롤러 및 연결된 API 엔드포인트를 모든 ASP.NET Core 앱에 쉽게 추가할 수 있습니다. 기존 애플리케이션을 ASP.NET MVC 5 이하에서 ASP.NET Core MVC로 마이그레이션하고 최소한의 노력으로 마이그레이션하려는 경우 보기 기반 MVC 방법을 사용합니다. 초기 마이그레이션을 완료한 후에는 새 기능에 Razor Pages를 채택하는 것이 적합한지 아니면 도매 마이그레이션으로 채택하는 것이 적합한지 평가할 수 있습니다. .NET 4.x 앱을 .NET 8로 포팅하는 방법에 대한 자세한 내용은 기존 ASP.NET 앱을 ASP.NET Core eBook으로 포팅을 참조하세요.
Razor 페이지 또는 MVC 보기를 사용하여 웹앱을 빌드하도록 선택하든 앱의 성능은 비슷하며 종속성 주입, 필터, 모델 바인딩, 유효성 검사 등에 대한 지원이 포함됩니다.
응답에 요청 매핑
핵심 ASP.NET Core 앱은 들어오는 요청을 나가는 응답에 매핑합니다. 낮은 수준에서 이 매핑은 미들웨어로 수행되며 간단한 ASP.NET Core 앱 및 마이크로 서비스는 사용자 지정 미들웨어로만 구성될 수 있습니다. ASP.NET Core MVC를 사용하는 경우 경로, 컨트롤러 및 작업 측면에서 고려할 때 다소 높은 수준에서 작업할 수 있습니다. 들어오는 각 요청은 애플리케이션의 라우팅 테이블과 비교되며, 일치하는 경로가 발견되면 연결된 작업 메서드(컨트롤러에 속)가 호출되어 요청을 처리합니다. 일치하는 경로가 없으면 오류 처리기(이 경우 NotFound 결과 반환)가 호출됩니다.
ASP.NET Core MVC 앱은 기존 경로, 특성 경로 또는 둘 다를 사용할 수 있습니다. 기본 경로는 아래 예제와 같이 구문을 사용하여 라우팅 규칙을 지정하는 코드에 정의됩니다.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});
이 예제에서는 "default"라는 경로가 라우팅 테이블에 추가되었습니다.
controller, action, id에 대한 자리 표시자를 사용하여 경로 템플릿을 정의합니다.
controller 및 action 자리 표시자에는 각각 지정된 기본값이HomeIndex있으며 id 자리 표시자는 선택 사항입니다("?"가 적용됨). 여기에 정의된 규칙은 요청의 첫 번째 부분이 컨트롤러의 이름, 작업에 대한 두 번째 부분과 일치해야 하며, 필요한 경우 세 번째 부분은 ID 매개 변수를 나타냅니다. 기존 경로는 일반적으로 요청 미들웨어 파이프라인이 구성된 Program.cs 애플리케이션에 대해 한 곳에서 정의됩니다.
특성 경로는 전역적으로 지정되지 않고 컨트롤러 및 작업에 직접 적용됩니다. 이 방법은 특정 방법을 볼 때 훨씬 더 쉽게 검색할 수 있다는 장점이 있지만 라우팅 정보가 애플리케이션의 한 곳에 보관되지 않는다는 것을 의미합니다. 특성 경로를 사용하면 지정된 작업에 대한 여러 경로를 쉽게 지정할 수 있으며 컨트롤러와 작업 간에 경로를 결합할 수 있습니다. 다음은 그 예입니다.
[Route("Home")]
public class HomeController : Controller
{
[Route("")] // Combines to define the route template "Home"
[Route("Index")] // Combines to define route template "Home/Index"
[Route("/")] // Does not combine, defines the route template ""
public IActionResult Index() {}
}
경로는 [HttpGet] 및 유사한 특성에 지정할 수 있으므로 별도의 [Route] 특성을 추가할 필요가 없습니다. 특성 경로는 아래와 같이 토큰을 사용하여 컨트롤러 또는 작업 이름을 반복할 필요성을 줄일 수도 있습니다.
[Route("[controller]")]
public class ProductsController : Controller
{
[Route("")] // Matches 'Products'
[Route("Index")] // Matches 'Products/Index'
public IActionResult Index() {}
}
Razor Pages는 특성 라우팅을 사용하지 않습니다. Razor 페이지의 @page 지시문으로 추가 경로 템플릿 정보를 지정할 수 있습니다.
@page "{id:int}"
이전 예제에서 해당 페이지는 경로와 일치하는 id 정수 매개 변수를 가집니다. 예를 들어 루트 에 있는 /Pages 페이지는 다음과 같은 요청에 응답합니다.
/Products/123
지정된 요청이 경로와 일치하지만 작업 메서드가 호출되기 전에 ASP.NET Core MVC는 요청에 대해 모델 바인딩 및 모델 유효성 검사를 수행합니다. 모델 바인딩은 들어오는 HTTP 데이터를 호출할 작업 메서드의 매개 변수로 지정된 .NET 형식으로 변환하는 작업을 담당합니다. 예를 들어 작업 메서드에 매개 변수가 int id 필요한 경우 모델 바인딩은 요청의 일부로 제공된 값에서 이 매개 변수를 제공하려고 시도합니다. 이를 위해 모델 바인딩은 게시된 형식의 값, 경로 자체의 값 및 쿼리 문자열 값을 찾습니다. 값이 id 발견되면 작업 메서드로 전달되기 전에 정수로 변환됩니다.
모델을 바인딩한 후 작업 메서드를 호출하기 전에 모델 유효성 검사가 수행됩니다. 모델 유효성 검사는 모델 형식에서 선택적 특성을 사용하며 제공된 모델 개체가 특정 데이터 요구 사항을 준수하는지 확인하는 데 도움이 될 수 있습니다. 특정 값은 필요에 따라 지정되거나 특정 길이 또는 숫자 범위 등으로 제한될 수 있습니다. 유효성 검사 특성이 지정되었지만 모델이 요구 사항을 준수하지 않는 경우 ModelState.IsValid 속성은 false이며 실패한 유효성 검사 규칙 집합을 클라이언트에 보내 요청을 수행할 수 있습니다.
모델 유효성 검사를 사용하는 경우 상태 변경 명령을 수행하기 전에 항상 모델이 유효한지 확인하여 앱이 잘못된 데이터에 의해 손상되지 않도록 해야 합니다. 필터를 사용하여 모든 작업에서 이 유효성 검사에 대한 코드를 추가할 필요가 없도록 할 수 있습니다. ASP.NET Core MVC 필터는 요청 그룹을 가로채는 방법을 제공하므로 일반적인 정책 및 교차 절단 문제를 대상으로 적용할 수 있습니다. 필터는 개별 작업, 전체 컨트롤러 또는 전역적으로 애플리케이션에 적용할 수 있습니다.
웹 API의 경우 ASP.NET Core MVC는 콘텐츠 협상을 지원하므로 요청에서 응답의 형식을 지정하는 방법을 지정할 수 있습니다. 요청에 제공된 헤더에 따라 데이터를 반환하는 작업은 XML, JSON 또는 지원되는 다른 형식으로 응답의 형식을 지정합니다. 이 기능을 사용하면 데이터 형식 요구 사항이 다른 여러 클라이언트에서 동일한 API를 사용할 수 있습니다.
Web API 프로젝트는 개별 컨트롤러, 기본 컨트롤러 클래스 또는 전체 어셈블리에 적용할 수 있는 특성을 사용하는 [ApiController] 것이 좋습니다. 이 특성은 자동 모델 유효성 검사를 추가하고 잘못된 모델을 사용하는 모든 작업은 유효성 검사 오류의 세부 정보가 포함된 BadRequest를 반환합니다. 또한 특성에는 기존 경로를 사용하는 대신 모든 작업에 특성 경로가 있어야 하며 오류에 대한 응답으로 보다 자세한 ProblemDetails 정보를 반환합니다.
컨트롤러를 효과적으로 제어하기
페이지 기반 애플리케이션의 경우 Razor Pages는 컨트롤러가 너무 커지는 것을 방지합니다. 각 개별 페이지에는 해당 처리기 전용의 자체 파일 및 클래스가 제공됩니다. Razor Pages가 도입되기 전에 많은 뷰 중심 애플리케이션에는 다양한 작업 및 보기를 담당하는 대규모 컨트롤러 클래스가 있습니다. 이러한 클래스는 자연스럽게 많은 책임과 종속성을 가지게 되므로 유지 관리가 더 어려워집니다. 보기 기반 컨트롤러가 너무 커지는 경우 Razor Pages를 사용하도록 리팩터링하거나 중재자와 같은 패턴을 도입하는 것이 좋습니다.
중재자 디자인 패턴은 클래스 간의 결합을 줄이는 동시에 클래스 간의 통신을 허용하는 데 사용됩니다. ASP.NET Core MVC 애플리케이션에서 이 패턴은 처리기를 사용하여 작업 메서드 작업을 수행하여 컨트롤러를 더 작은 조각으로 분할하는 데 자주 사용됩니다. 인기 있는 MediatR NuGet 패키지 는 이 작업을 수행하는 데 자주 사용됩니다. 일반적으로 컨트롤러에는 각각 특정 종속성이 필요할 수 있는 다양한 작업 메서드가 포함됩니다. 모든 작업에 필요한 모든 종속성 집합을 컨트롤러의 생성자에 전달해야 합니다. MediatR을 사용하는 경우 컨트롤러가 일반적으로 가질 수 있는 유일한 종속성은 중재자의 인스턴스입니다. 그런 다음 각 작업은 중재자 인스턴스를 사용하여 처리기에서 처리되는 메시지를 보냅니다. 처리기는 단일 작업과 관련이 있으므로 해당 작업에 필요한 종속성만 필요합니다. MediatR을 사용하는 컨트롤러의 예는 다음과 같습니다.
public class OrderController : Controller
{
private readonly IMediator _mediator;
public OrderController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<IActionResult> MyOrders()
{
var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name));
return View(viewModel);
}
// other actions implemented similarly
}
MyOrders 작업에서 메시지 호출 SendGetMyOrders 은 다음 클래스에서 처리됩니다.
public class GetMyOrdersHandler : IRequestHandler<GetMyOrders, IEnumerable<OrderViewModel>>
{
private readonly IOrderRepository _orderRepository;
public GetMyOrdersHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<IEnumerable<OrderViewModel>> Handle(GetMyOrders request, CancellationToken cancellationToken)
{
var specification = new CustomerOrdersWithItemsSpecification(request.UserName);
var orders = await _orderRepository.ListAsync(specification);
return orders.Select(o => new OrderViewModel
{
OrderDate = o.OrderDate,
OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel()
{
PictureUrl = oi.ItemOrdered.PictureUri,
ProductId = oi.ItemOrdered.CatalogItemId,
ProductName = oi.ItemOrdered.ProductName,
UnitPrice = oi.UnitPrice,
Units = oi.Units
}).ToList(),
OrderNumber = o.Id,
ShippingAddress = o.ShipToAddress,
Total = o.Total()
});
}
}
이 방법의 최종 결과는 컨트롤러가 훨씬 더 작고 주로 라우팅 및 모델 바인딩에 초점을 맞추는 반면 개별 처리기는 지정된 엔드포인트에 필요한 특정 작업을 담당합니다. 이 방법은 ApiEndpoints NuGet 패키지를 사용하여 MediatR 없이도 수행할 수 있습니다. 이 패키지는 Razor Pages가 보기 기반 컨트롤러에 제공하는 것과 동일한 이점을 API 컨트롤러에 가져오려고 시도합니다.
참조 – 요청을 응답에 매핑하기
- 컨트롤러 작업으로 라우팅
https://learn.microsoft.com/aspnet/core/mvc/controllers/routing- 모델 바인딩
https://learn.microsoft.com/aspnet/core/mvc/models/model-binding- 모델 유효성 검사
https://learn.microsoft.com/aspnet/core/mvc/models/validation- 필터
https://learn.microsoft.com/aspnet/core/mvc/controllers/filters- ApiController 특성
https://learn.microsoft.com/aspnet/core/web-api/
종속성을 다루기
ASP.NET Core는 의존성 주입이라는 기술을 기본적으로 지원하고 내부적으로 사용합니다. 종속성 주입은 애플리케이션의 여러 부분 간에 느슨한 결합을 가능하게 하는 기술입니다. 느슨한 결합은 애플리케이션의 일부를 더 쉽게 격리하여 테스트 또는 교체를 허용하기 때문에 바람직합니다. 또한 애플리케이션의 한 부분을 변경하면 애플리케이션의 다른 위치에서 예기치 않은 영향을 미칠 가능성이 줄어듭니다. 종속성 주입은 종속성 반전 원칙을 기반으로 하며 개방/폐쇄 원칙을 달성하는 데 핵심적인 경우가 많습니다. 애플리케이션이 종속성으로 작동하는 방식을 평가할 때 정적 집착 코드 냄새를 조심하고 "새로운 것은 붙이기"라는 격언을 기억하세요.
정적 집착은 클래스가 정적 메서드를 호출하거나 인프라에 대한 부작용 또는 종속성이 있는 정적 속성에 액세스할 때 발생합니다. 예를 들어 정적 메서드를 호출하는 메서드가 있는 경우 데이터베이스에 쓰기를 통해 메서드가 데이터베이스에 긴밀하게 결합됩니다. 해당 데이터베이스 호출을 중단하는 모든 항목은 메서드를 중단합니다. 이러한 테스트는 정적 호출을 모의하기 위해 상업용 모의 라이브러리가 필요하거나 테스트 데이터베이스로만 테스트할 수 있으므로 이러한 메서드를 테스트하는 것은 매우 어렵습니다. 인프라에 대한 의존도가 없는 정적 호출, 특히 완전히 상태 비저장 호출은 호출하기에 적합하며 결합 또는 테스트 용이성에 영향을 주지 않습니다(정적 호출 자체에 코드를 결합하는 것 외).
많은 개발자는 정적 집착 및 전역 상태의 위험을 이해하지만 직접 인스턴스화를 통해 코드를 특정 구현에 긴밀하게 결합합니다. "New is glue"는 이 결합을 상기시키기 위한 것이며, 이는 키워드 new 사용에 대한 일반적인 비난이 아닙니다. 정적 메서드 호출과 마찬가지로 외부 종속성이 없는 형식의 새 인스턴스는 일반적으로 코드를 구현 세부 정보에 긴밀하게 결합하거나 테스트를 더 어렵게 만들지 않습니다. 그러나 클래스가 인스턴스화될 때마다 해당 특정 위치의 특정 인스턴스를 하드 코딩하는 것이 타당할지, 아니면 해당 인스턴스를 종속성으로 요청하는 것이 더 나은 디자인인지 잠시 생각해 보세요.
종속성 선언
ASP.NET Core는 메서드 및 클래스가 종속성을 선언하고 인수로 요청하는 것을 중심으로 빌드됩니다. ASP.NET 애플리케이션은 일반적으로 Program.cs 또는 클래스에서 Startup 설정됩니다.
비고
Program.cs 완전히 앱을 구성하는 것은 .NET 6 이상 및 Visual Studio 2022 이상 앱의 기본 방법입니다. 이 새로운 방법을 시작하는 데 도움이 되도록 프로젝트 템플릿이 업데이트되었습니다. ASP.NET Core 프로젝트는 원하는 경우 클래스를 Startup 계속 사용할 수 있습니다.
Program.cs 서비스 구성
매우 간단한 앱의 경우 을 사용하여 WebApplicationBuilder 파일에서 직접 종속성을 연결할 수 있습니다. 필요한 모든 서비스가 추가되면 작성기를 사용하여 앱을 만듭니다.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
var app = builder.Build();
Startup.cs 서비스 구성
Startup.cs 자체는 여러 지점에서 종속성 주입을 지원하도록 구성됩니다. 클래스를 Startup 사용하는 경우 생성자를 제공할 수 있으며 다음과 같이 클래스를 통해 종속성을 요청할 수 있습니다.
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
}
}
클래스에 Startup 대한 명시적 형식 요구 사항이 없다는 점은 흥미롭습니다. 특수 Startup 한 기본 클래스에서 상속되지 않으며 특정 인터페이스를 구현하지도 않습니다. 생성자를 지정할 수도 있고, 생성자에 원하는 만큼 매개 변수를 지정할 수도 있습니다. 애플리케이션에 대해 구성한 웹 호스트가 시작되면, 설정 대상인 경우 Startup 클래스를 호출하고, 종속성 주입을 사용하여 Startup 클래스에 필요한 모든 종속성을 채웁니다. 물론 ASP.NET Core에서 사용하는 서비스 컨테이너에 구성되지 않은 매개 변수를 요청하는 경우 예외가 발생하지만 컨테이너가 알고 있는 종속성을 고수하는 한 원하는 모든 항목을 요청할 수 있습니다.
종속성 주입은 시작 인스턴스를 만들 때 처음부터 ASP.NET Core 앱에 기본 제공됩니다. 시작 클래스는 거기서 멈추지 않습니다. 메서드에서 Configure 종속성을 요청할 수도 있습니다.
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
}
ConfigureServices 메서드는 이 동작에 대한 예외입니다. 형식 IServiceCollection의 매개 변수를 하나만 사용해야 합니다. 실제로 종속성 주입을 지원할 필요는 없습니다. 즉, 서비스 컨테이너에 개체를 추가하는 역할을 하고, 다른 한편으로는 매개 변수를 통해 IServiceCollection 현재 구성된 모든 서비스에 액세스할 수 있기 때문입니다. 따라서 필요한 서비스를 매개변수로 요청하거나 클래스의 Startup의 각 부분에서 IServiceCollection를 ConfigureServices와 함께 작업하여, ASP.NET Core 서비스 컬렉션에 정의된 종속성을 사용할 수 있습니다.
비고
클래스에 특정 서비스가 제공되도록 해야 하는 경우, Startup 호출 내에서 IWebHostBuilder의 ConfigureServices 메서드를 사용하여 이를 구성할 수 있습니다.
Startup 클래스는 컨트롤러에서 미들웨어, 필터, 고유한 서비스에 이르기까지 ASP.NET Core 애플리케이션의 다른 부분을 구성하는 방법에 대한 모델입니다. 각 경우에 명시적 종속성 원칙을 따르고, 종속성을 직접 만드는 대신 요청하고, 애플리케이션 전체에서 종속성 주입을 활용해야 합니다. 구현, 특히 인프라에서 작동하거나 부작용이 있는 서비스 및 개체를 직접 인스턴스화하는 위치와 방법에 주의해야 합니다. 애플리케이션 코어에 정의되고 특정 구현 형식에 대한 참조를 하드코딩하는 인수로 전달되는 추상화 작업을 선호합니다.
애플리케이션 구조화
모놀리식 애플리케이션에는 일반적으로 단일 진입점이 있습니다. ASP.NET Core 웹 애플리케이션의 경우 진입점은 ASP.NET Core 웹 프로젝트가 됩니다. 그러나 솔루션이 단일 프로젝트로 구성되어야 한다는 의미는 아닙니다. 문제를 분리하기 위해 애플리케이션을 다른 계층으로 분할하는 것이 유용합니다. 계층으로 나뉘면 폴더를 넘어 프로젝트를 분리하는 것이 도움이 되며, 이는 더 나은 캡슐화를 달성하는 데 도움이 될 수 있습니다. ASP.NET Core 애플리케이션을 사용하여 이러한 목표를 달성하는 가장 좋은 방법은 5장에서 설명한 클린 아키텍처의 변형입니다. 이 접근 방식에 따라 애플리케이션의 솔루션은 UI, 인프라 및 ApplicationCore에 대한 별도의 라이브러리로 구성됩니다.
이러한 프로젝트 외에도 별도의 테스트 프로젝트도 포함됩니다(테스트는 9장에서 설명).
애플리케이션의 개체 모델 및 인터페이스는 ApplicationCore 프로젝트에 배치해야 합니다. 이 프로젝트에는 가능한 한 적은 종속성(특정 인프라 문제에 대한 종속성이 없음)이 있으며 솔루션의 다른 프로젝트는 이를 참조합니다. 지속해야 하는 비즈니스 엔터티는 인프라에 직접 의존하지 않는 서비스와 마찬가지로 ApplicationCore 프로젝트에 정의됩니다.
지속성 수행 방법 또는 사용자에게 알림을 보내는 방법과 같은 구현 세부 정보는 인프라 프로젝트에 유지됩니다. 이 프로젝트는 Entity Framework Core와 같은 구현별 패키지를 참조하지만 프로젝트 외부에서 이러한 구현에 대한 세부 정보를 노출해서는 안 됩니다. 인프라 서비스 및 리포지토리는 ApplicationCore 프로젝트에 정의된 인터페이스를 구현해야 하며, 지속성 구현은 ApplicationCore에 정의된 엔터티를 검색하고 저장하는 작업을 담당합니다.
ASP.NET Core UI 프로젝트는 UI 수준 문제를 담당하지만 비즈니스 논리 또는 인프라 세부 정보를 포함해서는 안 됩니다. 실제로 두 프로젝트 간의 종속성이 실수로 도입되지 않도록 하는 데 도움이 되는 인프라 프로젝트에 대한 종속성조차 갖지 않아야 합니다. 이 작업은 Autofac과 같은 타사 DI 컨테이너를 사용하여 수행할 수 있으며, 이를 통해 각 프로젝트의 모듈 클래스에서 DI 규칙을 정의할 수 있습니다.
구현 세부 정보에서 애플리케이션을 분리하는 또 다른 방법은 애플리케이션 호출 마이크로 서비스를 개별 Docker 컨테이너에 배포하는 것입니다. 이렇게 하면 두 프로젝트 간에 DI를 활용하는 것보다 우려 사항과 분리를 훨씬 더 크게 분리할 수 있지만 복잡성이 더 큽니다.
기능 조직
기본적으로 ASP.NET Core 애플리케이션은 컨트롤러 및 뷰 및 자주 ViewModels를 포함하도록 폴더 구조를 구성합니다. 이러한 서버 쪽 구조를 지원하는 클라이언트 쪽 코드는 일반적으로 wwwroot 폴더에 별도로 저장됩니다. 그러나 지정된 기능을 사용하려면 이러한 폴더 간에 이동해야 하는 경우가 많으므로 대규모 애플리케이션에서 이 조직에 문제가 발생할 수 있습니다. 각 폴더의 파일 및 하위 폴더 수가 늘어나면서 솔루션 탐색기를 스크롤하는 것이 점점 더 어려워집니다. 이 문제에 대한 한 가지 해결 방법은 파일 형식이 아닌 기능 별로 애플리케이션 코드를 구성하는 것입니다. 이 조직 스타일을 일반적으로 기능 폴더 또는 기능 조각 이라고 합니다(세로 조각 참조).
ASP.NET Core MVC는 이 목적을 위해 영역을 지원합니다. 영역을 사용하면 각 영역 폴더에 별도의 컨트롤러 및 뷰 폴더 집합(및 연결된 모델)을 만들 수 있습니다. 그림 7-1은 영역을 사용하는 예제 폴더 구조를 보여줍니다.
그림 7-1. 샘플 영역 조직
영역을 사용하는 경우 특성을 사용하여 컨트롤러가 속한 영역의 이름으로 컨트롤러를 데코레이트해야 합니다.
[Area("Catalog")]
public class HomeController
{}
또한 경로에 지역 지원을 추가해야 합니다.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: "areaRoute", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});
영역에 대한 기본 제공 지원 외에도 고유한 폴더 구조와 특성 및 사용자 지정 경로 대신 규칙을 사용할 수도 있습니다. 이렇게 하면 뷰, 컨트롤러 등에 대한 별도의 폴더를 포함하지 않은 기능 폴더를 포함할 수 있으므로 계층 구조를 더 돋보이게 하고 각 기능에 대한 모든 관련 파일을 한 곳에서 더 쉽게 볼 수 있습니다. API의 경우 폴더를 사용하여 컨트롤러를 바꿀 수 있으며, 각 폴더에는 모든 API 엔드포인트와 연결된 DDO가 포함될 수 있습니다.
ASP.NET Core는 기본 제공 규칙 형식을 사용하여 동작을 제어합니다. 이러한 규칙을 수정하거나 바꿀 수 있습니다. 예를 들어 해당 네임스페이스를 기반으로 지정된 컨트롤러의 기능 이름을 자동으로 가져오는 규칙을 만들 수 있습니다(일반적으로 컨트롤러가 있는 폴더와 상관 관계).
public class FeatureConvention : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
controller.Properties.Add("feature",
GetFeatureName(controller.ControllerType));
}
private string GetFeatureName(TypeInfo controllerType)
{
string[] tokens = controllerType.FullName.Split('.');
if (!tokens.Any(t => t == "Features")) return "";
string featureName = tokens
.SkipWhile(t => !t.Equals("features", StringComparison.CurrentCultureIgnoreCase))
.Skip(1)
.Take(1)
.FirstOrDefault();
return featureName;
}
}
그런 다음, 애플리케이션 ConfigureServices (또는 Program.cs)에 MVC에 대한 지원을 추가할 때 이 규칙을 옵션으로 지정합니다.
// ConfigureServices
services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));
// Program.cs
builder.Services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));
ASP.NET Core MVC는 규칙을 사용하여 보기를 찾습니다. 보기가 기능 폴더에 배치되도록 사용자 지정 규칙으로 재정의할 수 있습니다(위의 FeatureConvention에서 제공하는 기능 이름 사용). 이 방법에 대해 자세히 알아보고 MSDN Magazine 문서인 ASP.NET Core MVC용 기능 조각에서 작업 샘플을 다운로드할 수 있습니다.
API 및 Blazor 애플리케이션
애플리케이션에 보안이 설정되어야 하는 웹 API 집합이 포함되어 있는 경우 이러한 API는 View 또는 Razor Pages 애플리케이션과는 별도의 프로젝트로 구성하는 것이 좋습니다. 서버 쪽 웹 애플리케이션에서 API, 특히 공용 API를 분리하면 여러 가지 이점이 있습니다. 이러한 애플리케이션에는 종종 고유한 배포 및 로드 특성이 있습니다. 또한 토큰 기반 인증을 사용할 가능성이 가장 큰 쿠키 기반 인증 및 API를 활용하는 표준 양식 기반 애플리케이션을 사용하여 보안을 위한 다양한 메커니즘을 채택할 가능성이 매우 높습니다.
또한, Blazor 애플리케이션은 Blazor 서버를 사용하든 BlazorWebAssembly을 사용하든 관계없이 별도의 프로젝트로 빌드해야 합니다. 애플리케이션에는 보안 모델뿐만 아니라 다른 런타임 특성이 있습니다. 공통 형식을 서버 쪽 웹 애플리케이션(또는 API 프로젝트)과 공유할 가능성이 높으며, 이러한 형식은 공통 공유 프로젝트에 정의되어야 합니다.
eShopOnWeb에 BlazorWebAssembly 관리 인터페이스를 추가하려면 몇 가지 새 프로젝트를 추가해야 했습니다.
Blazor
WebAssembly 프로젝트 자체. BlazorAdmin
BlazorAdmin 프로젝트에서 PublicApi에서 사용되며 토큰 기반 인증을 사용하도록 구성된 새로운 공용 API 엔드포인트 집합이 정의됩니다. 그리고 이러한 두 프로젝트에서 사용되는 특정 공유 형식은 새 BlazorShared 프로젝트에 유지됩니다.
사람들은 왜 BlazorShared와 ApplicationCore 모두가 필요한 형식을 공유할 수 있는 공통 PublicApi 프로젝트가 이미 있는데도 불구하고 별도의 BlazorAdmin 프로젝트를 추가해야 하는지 물을 수 있습니다. 이 프로젝트에는 모든 애플리케이션의 비즈니스 논리가 포함되어 있으므로 필요 이상으로 크며 서버에서 보안을 유지해야 할 가능성이 훨씬 더 높다는 것입니다.
BlazorAdmin에 참조된 모든 라이브러리는 사용자가 Blazor 애플리케이션을 로드할 때 브라우저로 다운로드됩니다.
백엔드-For-Frontends (BFF) 패턴을 사용 여부에 따라, BlazorWebAssembly 앱이 사용하는 API가 Blazor와 형식을 100% 공유하지 않을 수 있습니다. 특히 여러 클라이언트에서 사용하려는 공용 API는 클라이언트별 공유 프로젝트에서 공유하지 않고 자체 요청 및 결과 형식을 정의할 수 있습니다. eShopOnWeb 샘플에서는 PublicApi 프로젝트가 실제로 공용 API를 호스팅한다고 가정하고 있으므로, 모든 요청 및 응답 형식이 BlazorShared 프로젝트에서 오는 것만은 아닙니다.
교차 편집 문제
애플리케이션이 성장함에 따라 중복을 제거하고 일관성을 유지하기 위해 횡단적 관심사를 식별하여 관리하는 것이 점점 더 중요해지고 있습니다. ASP.NET Core 애플리케이션의 횡단 관심사의 몇 가지 예는 인증, 모델 유효성 검사 규칙, 출력 캐싱 및 오류 처리, 그리고 기타 많은 것들이 있습니다. ASP.NET Core MVC 필터를 사용하면 요청 처리 파이프라인의 특정 단계 전후에 코드를 실행할 수 있습니다. 예를 들어 필터는 모델 바인딩 전후, 작업 전후 또는 작업 결과 전후에 실행할 수 있습니다. 권한 부여 필터를 사용하여 파이프라인의 나머지 부분에 대한 액세스를 제어할 수도 있습니다. 그림 7-2는 구성된 경우 요청 실행이 필터를 통해 흐르는 방식을 보여 줍니다.
그림 7-2. 필터 및 요청 파이프라인을 통해 실행을 요청합니다.
필터는 일반적으로 특성으로 구현되므로 컨트롤러 또는 작업(또는 전역)에 적용할 수 있습니다. 이러한 방식으로 추가되면, 작업 수준에서 지정된 필터가 컨트롤러 수준에서 지정된 필터를 무시하거나 그 위에 추가됩니다. 컨트롤러 수준의 필터는 전역 필터를 무시하게 됩니다. 예를 들어 [Route] 특성을 사용하여 컨트롤러와 작업 간의 경로를 빌드할 수 있습니다. 마찬가지로 다음 샘플과 같이 컨트롤러 수준에서 권한 부여를 구성한 다음 개별 작업으로 재정의할 수 있습니다.
[Authorize]
public class AccountController : Controller
{
[AllowAnonymous] // overrides the Authorize attribute
public async Task<IActionResult> Login() {}
public async Task<IActionResult> ForgotPassword() {}
}
첫 번째 메서드인 로그인은 [AllowAnonymous] 필터(속성)를 사용하여 컨트롤러 수준에서 설정된 권한 부여 필터를 덮어씁니다.
ForgotPassword 작업(및 AllowAnonymous 특성이 없는 클래스의 다른 작업)에는 인증된 요청이 필요합니다.
필터를 사용하여 API에 대한 일반적인 오류 처리 정책의 형태로 중복을 제거할 수 있습니다. 예를 들어 일반적인 API 정책은 존재하지 않는 키를 참조하는 요청에 NotFound 응답을 반환하고 BadRequest 모델 유효성 검사에 실패하면 응답을 반환하는 것입니다. 다음 예제에서는 작동 중인 이러한 두 정책을 보여 줍니다.
[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
if ((await _authorRepository.ListAsync()).All(a => a.Id != id))
{
return NotFound(id);
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
author.Id = id;
await _authorRepository.UpdateAsync(author);
return Ok();
}
작업 메서드가 다음과 같은 조건부 코드로 복잡해지도록 허용하지 마세요. 대신 필요에 따라 적용할 수 있는 필터로 정책을 끌어오세요. 이 예제에서는 명령이 API로 전송될 때마다 발생해야 하는 모델 유효성 검사 검사를 다음 특성으로 바꿀 수 있습니다.
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
ValidateModelAttribute 패키지를 포함하여 프로젝트에 NuGet 종속성으로 추가할 수 있습니다. API의 ApiController 경우 특성을 사용하여 별도의 ValidateModel 필터 없이도 이 동작을 적용할 수 있습니다.
마찬가지로 필터를 사용하여 레코드가 있는지 확인하고 작업이 실행되기 전에 404를 반환하여 작업에서 이러한 검사를 수행할 필요가 없습니다. 일반적인 규칙을 꺼내고 UI에서 인프라 코드 및 비즈니스 논리를 분리하는 솔루션을 구성한 후에는 MVC 작업 메서드가 매우 얇아야 합니다.
[HttpPut("{id}")]
[ValidateAuthorExists]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
await _authorRepository.UpdateAsync(author);
return Ok();
}
필터 구현에 대해 자세히 알아보고 MSDN Magazine 문서Real-World ASP.NET Core MVC 필터에서 작업 샘플을 다운로드할 수 있습니다.
유효성 검사 오류(잘못된 요청), 리소스를 찾을 수 없음 및 서버 오류와 같은 일반적인 시나리오를 기반으로 API에서 많은 일반적인 응답이 있는 경우 결과 추상화를 사용하는 것이 좋습니다. 결과 추상화는 API 엔드포인트에서 사용하는 서비스에서 반환되며 컨트롤러 작업 또는 엔드포인트는 필터를 사용하여 이를 변환합니다 IActionResults.
참조 – 애플리케이션 구조화
- 지역
https://learn.microsoft.com/aspnet/core/mvc/controllers/areas- MSDN Magazine – ASP.NET Core MVC용 기능 조각
https://learn.microsoft.com/archive/msdn-magazine/2016/september/asp-net-core-feature-slices-for-asp-net-core-mvc- 필터
https://learn.microsoft.com/aspnet/core/mvc/controllers/filters- MSDN Magazine – Real World ASP.NET 핵심 MVC 필터
https://learn.microsoft.com/archive/msdn-magazine/2016/august/asp-net-core-real-world-asp-net-core-mvc-filters- eShopOnWeb 결과
https://github.com/dotnet-architecture/eShopOnWeb/wiki/Patterns#result
안전
웹 애플리케이션 보안은 많은 고려 사항을 포함하는 큰 주제입니다. 가장 기본적인 수준에서 보안에는 지정된 요청의 수신자를 확인한 다음 요청이 필요한 리소스에 대한 액세스 권한만 있는지 확인하는 작업이 포함됩니다. 인증은 요청과 함께 제공된 자격 증명을 신뢰할 수 있는 데이터 저장소의 자격 증명과 비교하여 요청이 알려진 엔터티에서 들어오는 것으로 처리되어야 하는지 확인하는 프로세스입니다. 권한 부여는 사용자 ID에 따라 특정 리소스에 대한 액세스를 제한하는 프로세스입니다. 세 번째 보안 문제는 적어도 애플리케이션에서 SSL을 사용하는지 확인해야 하는 타사의 도청으로부터 요청을 보호하는 것입니다.
아이덴티티
ASP.NET 핵심 ID는 애플리케이션에 대한 로그인 기능을 지원하는 데 사용할 수 있는 멤버 자격 시스템입니다. Microsoft 계정, Twitter, Facebook, Google 등과 같은 공급자의 외부 로그인 공급자 지원뿐만 아니라 로컬 사용자 계정을 지원합니다. 애플리케이션은 ASP.NET 핵심 ID 외에도 Windows 인증 또는 ID 서버와 같은 타사 ID 공급자를 사용할 수 있습니다.
ASP.NET 핵심 ID는 개별 사용자 계정 옵션이 선택된 경우 새 프로젝트 템플릿에 포함됩니다. 이 템플릿에는 등록, 로그인, 외부 로그인, 잊어버린 암호 및 추가 기능에 대한 지원이 포함됩니다.
그림 7-3. ID를 미리 구성하려면 개별 사용자 계정을 선택합니다.
ID 지원은 Program.cs 또는 Startup미들웨어뿐만 아니라 서비스 구성을 포함합니다.
Program.cs ID 구성
Program.cs 인스턴스에서 WebHostBuilder 서비스를 구성한 다음, 앱이 만들어지면 해당 미들웨어를 구성합니다. 주의해야 할 핵심 사항은 필수 서비스에 대한 AddDefaultIdentity 호출과 필수 미들웨어를 추가하는 UseAuthentication 및 UseAuthorization 호출입니다.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
앱 시작 시 ID 구성
// Add framework services.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddMvc();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
UseAuthentication와 UseAuthorization이 MapRazorPages 앞에 나타나는 것이 중요합니다. ID 서비스를 구성할 때 AddDefaultTokenProviders에 대한 호출을 확인할 수 있습니다. 이는 웹 통신을 보호하는 데 사용할 수 있는 토큰과는 아무런 관련이 없지만, 대신 SMS 또는 이메일을 통해 사용자에게 보낼 수 있는 프롬프트를 만드는 공급자를 참조하여 ID를 확인합니다.
2단계 인증을 구성하고 공식 ASP.NET Core 문서에서 외부 로그인 공급자를 사용하도록 설정하는 방법에 대해 자세히 알아볼 수 있습니다.
인증
인증은 시스템에 액세스하는 사용자를 결정하는 프로세스입니다. ASP.NET 핵심 ID 및 이전 섹션에 표시된 구성 방법을 사용하는 경우 애플리케이션에서 일부 인증 기본값을 자동으로 구성합니다. 그러나 이러한 기본값을 수동으로 구성하거나 AddIdentity에서 설정한 기본값을 재정의할 수도 있습니다. ID를 사용하는 경우 쿠키 기반 인증을 기본 구성표로 구성합니다.
웹 기반 인증에는 일반적으로 시스템의 클라이언트를 인증하는 과정에서 수행할 수 있는 최대 5개의 작업이 있습니다. 이는 다음과 같습니다.
- 인증. 클라이언트에서 제공하는 정보를 사용하여 애플리케이션 내에서 사용할 ID를 만듭니다.
- 도전. 이 작업은 클라이언트가 자신을 식별하도록 요구하는 데 사용됩니다.
- 금하다. 클라이언트에 작업 수행이 금지되어 있음을 알릴 수 있습니다.
- 로그인합니다. 어떤 방식으로든 기존 클라이언트를 유지합니다.
- 로그아웃합니다. 고정 저장소에서 클라이언트를 제거합니다.
웹 애플리케이션에서 인증을 수행하기 위한 여러 가지 일반적인 기술이 있습니다. 이를 스키마라고 합니다. 지정된 체계는 위의 옵션 중 일부 또는 전부에 대한 작업을 정의합니다. 일부 스키마는 작업의 하위 집합만 지원하며 지원하지 않는 구성표를 수행하려면 별도의 스키마가 필요할 수 있습니다. 예를 들어 OpenId-Connect(OIDC) 체계는 로그인 또는 로그아웃을 지원하지 않지만 일반적으로 이 지속성에 쿠키 인증을 사용하도록 구성됩니다.
ASP.NET Core 애플리케이션에서는 위에서 설명한 각 작업에 대해 일반적인 DefaultAuthenticateScheme 구성과 선택적 특정 구성표를 설정할 수 있습니다. 예를 들어 DefaultChallengeScheme 및 DefaultForbidScheme를 지정합니다. 호출 AddIdentity 은 애플리케이션의 여러 측면을 구성하고 많은 필수 서비스를 추가합니다. 또한 인증 체계를 구성하기 위한 이 호출도 포함됩니다.
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});
이러한 체계는 지속성에 쿠키를 사용하고 기본적으로 인증을 위해 로그인 페이지로 리디렉션합니다. 이러한 체계는 웹 브라우저를 통해 사용자와 상호 작용하는 웹 애플리케이션에 적합하지만 API에는 권장되지 않습니다. 대신 API는 일반적으로 JWT 전달자 토큰과 같은 다른 형태의 인증을 사용합니다.
웹 API는 .NET 애플리케이션 및 다른 프레임워크의 동등한 형식과 같은 HttpClient 코드에서 사용됩니다. 이러한 클라이언트는 API 호출에서 사용 가능한 응답 또는 문제가 발생한 경우를 나타내는 상태 코드를 예상합니다. 이러한 클라이언트는 브라우저를 통해 상호 작용하지 않으며 API가 반환할 수 있는 HTML을 렌더링하거나 조작하지 않습니다. 따라서 API 엔드포인트가 인증되지 않은 경우 클라이언트를 로그인 페이지로 리디렉션하는 것은 적절하지 않습니다. 다른 체계가 더 적합합니다.
API에 대한 인증을 구성하려면 eShopOnWeb 참조 애플리케이션의 PublicApi 프로젝트에서 사용하는 다음과 같은 인증을 설정할 수 있습니다.
builder.Services
.AddAuthentication(config =>
{
config.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(config =>
{
config.RequireHttpsMetadata = false;
config.SaveToken = true;
config.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
단일 프로젝트 내에서 여러 인증 체계를 구성할 수 있지만 단일 기본 구성표를 구성하는 것이 훨씬 간단합니다. 이러한 이유로 eShopOnWeb 참조 애플리케이션은 애플리케이션의 뷰와 Razor Pages를 포함하는 기본 PublicApi 프로젝트Web와는 별도로 해당 API를 자체 프로젝트로 구분합니다.
Blazor 앱의 인증
Blazor 서버 애플리케이션은 다른 ASP.NET Core 애플리케이션과 동일한 인증 기능을 활용할 수 있습니다. Blazor WebAssembly 그러나 애플리케이션은 브라우저에서 실행되므로 기본 제공 ID 및 인증 공급자를 사용할 수 없습니다. Blazor WebAssembly 애플리케이션은 사용자 인증 상태를 로컬로 저장할 수 있으며 클레임에 액세스하여 사용자가 수행할 수 있어야 하는 작업을 결정할 수 있습니다. 그러나 사용자가 앱을 쉽게 우회하고 API와 직접 상호 작용할 수 있으므로 앱 내에서 BlazorWebAssembly 구현된 논리에 관계없이 서버에서 모든 인증 및 권한 부여 검사를 수행해야 합니다.
참조 – 인증
- 인증 작업 및 기본값
https://stackoverflow.com/a/52493428- SPA에 대한 인증 및 권한 부여
https://learn.microsoft.com/aspnet/core/security/authentication/identity-api-authorization- ASP.NET 핵심 Blazor 인증 및 권한 부여
https://learn.microsoft.com/aspnet/core/blazor/security/- 보안: ASP.NET Web Forms의 인증 및 권한 부여 및 Blazor
https://learn.microsoft.com/dotnet/architecture/blazor-for-web-forms-developers/security-authentication-authorization
승인
가장 간단한 형태의 권한 부여에는 익명 사용자에 대한 액세스를 제한하는 작업이 포함됩니다. 이 기능은 특정 컨트롤러 또는 작업에 특성을 적용하여 [Authorize] 수행할 수 있습니다. 역할을 사용하는 경우 다음과 같이 특정 역할에 속한 사용자에 대한 액세스를 제한하기 위해 특성을 더 확장할 수 있습니다.
[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{
}
이 경우, HRManager 역할 또는 Finance 역할(또는 둘 다)에 속한 사용자는 SalaryController에 액세스할 수 있습니다. 사용자가 여러 역할(여러 역할 중 하나 아님)에 속하도록 요구하려면 매번 필요한 역할을 지정하여 특성을 여러 번 적용할 수 있습니다.
여러 컨트롤러 및 작업에서 특정 역할 집합을 문자열로 지정하면 바람직하지 않은 반복이 발생할 수 있습니다. 최소한 이러한 문자열 리터럴에 대한 상수를 정의하고 문자열을 지정하는 데 필요한 모든 위치에서 상수를 사용합니다. 권한 부여 규칙을 캡슐화하는 권한 부여 정책을 구성한 다음 특성을 적용 [Authorize] 할 때 개별 역할 대신 정책을 지정할 수도 있습니다.
[Authorize(Policy = "CanViewPrivateReport")]
public IActionResult ExecutiveSalaryReport()
{
return View();
}
이러한 방식으로 정책을 사용하면 제한되는 작업의 종류를 적용되는 특정 역할 또는 규칙과 구분할 수 있습니다. 나중에 특정 리소스에 액세스해야 하는 새 역할을 만드는 경우 모든 [Authorize] 특성의 모든 역할 목록을 업데이트하는 대신 정책을 업데이트할 수 있습니다.
클레임
클레임은 인증된 사용자의 속성을 나타내는 이름 값 쌍입니다. 예를 들어 사용자의 직원 번호를 클레임으로 저장할 수 있습니다. 그런 다음 클레임을 권한 부여 정책의 일부로 사용할 수 있습니다. "EmployeeOnly"라는 정책을 만들 수 있으며, 이 정책은 다음 예제와 같이 "EmployeeNumber"이라는 클레임이 존재해야 합니다.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});
}
그런 다음, 위에서 설명한 [Authorize] 대로 이 정책을 특성과 함께 사용하여 컨트롤러 및/또는 작업을 보호할 수 있습니다.
웹 API 보안
대부분의 웹 API는 토큰 기반 인증 시스템을 구현해야 합니다. 토큰 인증은 상태 비저장을 사용하며 확장성을 염두에 두고 설계되었습니다. 토큰 기반 인증 시스템에서 클라이언트는 먼저 인증 공급자를 사용하여 인증해야 합니다. 성공하면 클라이언트에 토큰이 발급됩니다. 이는 단순히 암호화적으로 의미 있는 문자 문자열입니다. 토큰의 가장 일반적인 형식은 JSON 웹 토큰 또는 JWT(종종 "jot"로 발음됨)입니다. 클라이언트가 API에 요청을 발급해야 하는 경우 이 토큰을 요청의 헤더로 추가합니다. 그런 다음, 서버는 요청을 완료하기 전에 요청 헤더에 있는 토큰의 유효성을 검사합니다. 그림 7-4에서는 이 프로세스를 보여 줍니다.
그림 7-4. 웹 API에 대한 토큰 기반 인증입니다.
고유한 인증 서비스를 만들거나, Azure AD 및 OAuth와 통합하거나, IdentityServer와 같은 오픈 소스 도구를 사용하여 서비스를 구현할 수 있습니다.
JWT 토큰은 클라이언트 또는 서버에서 읽을 수 있는 사용자에 대한 클레임을 포함할 수 있습니다. jwt.io 같은 도구를 사용하여 JWT 토큰의 내용을 볼 수 있습니다. 콘텐츠가 쉽게 읽을 수 있으므로 암호 또는 키와 같은 중요한 데이터를 JTW 토큰에 저장하지 마세요.
SPA 또는 BlazorWebAssembly 애플리케이션에서 JWT 토큰을 사용하는 경우 토큰을 클라이언트의 어딘가에 저장한 다음 모든 API 호출에 추가해야 합니다. 이 작업은 일반적으로 다음 코드와 같이 헤더로 수행됩니다.
// AuthService.cs in BlazorAdmin project of eShopOnWeb
private async Task SetAuthorizationHeader()
{
var token = await GetToken();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
위의 메서드를 호출한 후 요청으로 _httpClient 수행된 요청에는 요청 헤더에 토큰이 포함되므로 서버 쪽 API가 요청을 인증하고 권한을 부여할 수 있습니다.
사용자 지정 보안
주의
일반적으로 사용자 고유의 사용자 지정 보안 구현을 구현하지 마십시오.
특히 암호화, 사용자 멤버 자격 또는 토큰 생성 시스템을 직접 구현하는 데 주의해야 합니다. 사용 가능한 많은 상용 및 오픈 소스 대안이 있으며, 이는 사용자 지정 구현보다 거의 확실하게 더 나은 보안을 갖습니다.
참조 – 보안
- 보안 문서 개요
https://learn.microsoft.com/aspnet/core/security/- ASP.NET Core 앱에서 SSL 적용
https://learn.microsoft.com/aspnet/core/security/enforcing-ssl- ID 소개
https://learn.microsoft.com/aspnet/core/security/authentication/identity- 권한 부여 소개
https://learn.microsoft.com/aspnet/core/security/authorization/introduction- Azure App Service에서 API Apps에 대한 인증 및 권한 부여
https://learn.microsoft.com/azure/app-service-api/app-service-api-authentication- ID 서버
https://github.com/IdentityServer
클라이언트 통신
ASP.NET Core 앱은 페이지를 제공하고 웹 API를 통한 데이터 요청에 응답하는 것 외에도 연결된 클라이언트와 직접 통신할 수 있습니다. 이 아웃바운드 통신은 다양한 전송 기술을 사용할 수 있으며, 가장 일반적인 것은 WebSockets입니다. ASP.NET Core SignalR은 애플리케이션에 실시간 서버 간 통신 기능을 간단하게 추가할 수 있는 라이브러리입니다. SignalR은 WebSocket을 비롯한 다양한 전송 기술을 지원하며 개발자의 많은 구현 세부 정보를 추상화합니다.
WebSockets를 직접 사용하든 다른 기술을 사용하든 실시간 클라이언트 통신은 다양한 애플리케이션 시나리오에서 유용합니다. 몇 가지 예는 다음과 같습니다.
라이브 채팅방 애플리케이션
애플리케이션 모니터링
작업 진행률 업데이트
공지
대화형 양식 애플리케이션
애플리케이션에 클라이언트 통신을 빌드할 때 일반적으로 두 가지 구성 요소가 있습니다.
서버 쪽 연결 관리자(SignalR Hub, WebSocketManager WebSocketHandler)
클라이언트 쪽 라이브러리
클라이언트는 브라우저로 제한되지 않습니다. 모바일 앱, 콘솔 앱 및 기타 네이티브 앱은 SignalR/WebSocket을 사용하여 통신할 수도 있습니다. 다음 간단한 프로그램은 WebSocketManager 샘플 애플리케이션의 일부로, 채팅 애플리케이션으로 전송된 모든 콘텐츠를 콘솔에 출력합니다.
public class Program
{
private static Connection _connection;
public static void Main(string[] args)
{
StartConnectionAsync();
_connection.On("receiveMessage", (arguments) =>
{
Console.WriteLine($"{arguments[0]} said: {arguments[1]}");
});
Console.ReadLine();
StopConnectionAsync();
}
public static async Task StartConnectionAsync()
{
_connection = new Connection();
await _connection.StartConnectionAsync("ws://localhost:65110/chat");
}
public static async Task StopConnectionAsync()
{
await _connection.StopConnectionAsync();
}
}
애플리케이션이 클라이언트 애플리케이션과 직접 통신하는 방법을 고려하고 실시간 통신이 앱의 사용자 환경을 개선할 수 있는지 여부를 고려합니다.
참조 – 클라이언트 통신
- ASP.NET Core SignalR
https://github.com/dotnet/aspnetcore/tree/main/src/SignalR- WebSocket Manager
https://github.com/radu-matei/websocket-manager
도메인 기반 디자인 - 적용해야 하나요?
DDD(Domain-Driven Design)는 비즈니스 도메인에 중점을 두는 소프트웨어를 빌드하는 민첩한 접근 방식입니다. 실제 시스템의 작동 방식을 개발자와 관련할 수 있는 비즈니스 도메인 전문가와의 통신 및 상호 작용에 중점을 둡니다. 예를 들어 주식 거래를 처리하는 시스템을 빌드하는 경우 도메인 전문가는 숙련된 주식 브로커일 수 있습니다. DDD는 크고 복잡한 비즈니스 문제를 해결하도록 설계되었으며 도메인 이해 및 모델링에 대한 투자가 가치가 없기 때문에 더 작고 간단한 애플리케이션에는 적합하지 않은 경우가 많습니다.
DDD 접근 방식에 따라 소프트웨어를 빌드할 때 팀(비기술 관련자 및 기여자 포함)은 문제 공간에 대한 유비쿼터스 언어 를 개발해야 합니다. 즉, 모델링되는 실제 개념, 해당 소프트웨어 및 개념을 유지하기 위해 존재할 수 있는 구조(예: 데이터베이스 테이블)에 동일한 용어를 사용해야 합니다. 따라서 유비쿼터스 언어로 설명된 개념은 도메인 모델의 기초가 되어야 합니다.
도메인 모델은 시스템의 동작을 나타내기 위해 서로 상호 작용하는 개체로 구성됩니다. 이러한 개체는 다음 범주에 속할 수 있습니다.
ID 스레드가 있는 개체를 나타내는 엔터티입니다. 엔터티는 일반적으로 나중에 검색할 수 있는 키를 사용하여 영구 저장소에 저장됩니다.
단위로 유지해야 하는 개체 그룹을 나타내는 집계입니다.
값 개체는 해당 속성 값의 합계를 기준으로 비교할 수 있는 개념을 나타냅니다. 예를 들어 DateRange는 시작 및 종료 날짜로 구성됩니다.
시스템의 다른 부분에 관심이 있는 시스템 내에서 발생하는 작업을 나타내는 도메인 이벤트입니다.
DDD 도메인 모델은 모델 내에서 복잡한 동작을 캡슐화해야 합니다. 특히 엔터티는 단순히 속성의 모음이 되어서는 안 됩니다. 도메인 모델이 동작이 부족하고 단지 시스템의 상태를 나타내는 경우 DDD에서 바람직하지 않은 빈혈 모델이라고 합니다.
이러한 모델 유형 외에도 DDD는 일반적으로 다양한 패턴을 사용합니다.
지속성 세부 정보를 추상화하기 위한 리포지토리입니다.
팩터리- 복합 개체 만들기를 캡슐화합니다.
복잡한 동작 및/또는 인프라 구현 세부 정보를 캡슐화하는 서비스입니다.
명령 - 명령 발행과 실행을 분리하기 위해.
사양, 쿼리 세부 정보를 캡슐화하기 위한 것입니다.
또한 DDD는 이전에 설명한 클린 아키텍처를 사용하여 단위 테스트를 사용하여 쉽게 확인할 수 있는 느슨한 결합, 캡슐화 및 코드를 사용할 것을 권장합니다.
DDD를 적용해야 하는 경우
DDD는 비즈니스(기술적인 것뿐만 아니라) 복잡성이 큰 대규모 애플리케이션에 적합합니다. 애플리케이션에는 도메인 전문가에 대한 지식이 필요합니다. 도메인 모델 자체에는 단순히 데이터 저장소에서 다양한 레코드의 현재 상태를 저장하고 검색하는 것 이상의 비즈니스 규칙 및 상호 작용을 나타내는 중요한 동작이 있어야 합니다.
DDD를 적용하지 않아야 하는 경우
DDD에는 기본적으로 CRUD(만들기/읽기/업데이트/삭제)에 불과한 소규모 애플리케이션 또는 애플리케이션에 대해 보증되지 않을 수 있는 모델링, 아키텍처 및 통신에 대한 투자가 포함됩니다. DDD에 따라 애플리케이션에 접근하도록 선택했지만, 도메인에 동작이 없는 빈약한 모델이 있다면 접근 방식을 재고해야 할지도 모릅니다. 애플리케이션에 DDD가 필요하지 않거나 데이터베이스 또는 사용자 인터페이스가 아닌 도메인 모델에서 비즈니스 논리를 캡슐화하기 위해 애플리케이션을 리팩터링하는 데 도움이 필요할 수 있습니다.
하이브리드 접근 방식은 애플리케이션의 트랜잭션 또는 더 복잡한 영역에만 DDD를 사용하는 것이지만, 애플리케이션의 CRUD 또는 읽기 전용 부분은 사용하지 않습니다. 예를 들어 보고서를 표시하거나 대시보드의 데이터를 시각화하기 위해 데이터를 쿼리하는 경우 집계의 제약 조건이 필요하지 않습니다. 이러한 요구 사항에 대해 별도의 간단한 읽기 모델을 사용하는 것은 완벽하게 허용됩니다.
참조 – Domain-Driven 디자인
- 일반 영어의 DDD(StackOverflow Answer)
https://stackoverflow.com/questions/1222392/can-someone-explain-domain-driven-design-ddd-in-plain-english-please/1222488#1222488
배치
호스트되는 위치에 관계없이 ASP.NET Core 애플리케이션을 배포하는 프로세스에는 몇 가지 단계가 있습니다. 첫 번째 단계는 CLI 명령을 사용하여 수행할 수 있는 애플리케이션을 게시하는 dotnet publish 것입니다. 이 단계에서는 애플리케이션을 컴파일하고 애플리케이션을 실행하는 데 필요한 모든 파일을 지정된 폴더에 배치합니다. Visual Studio에서 배포하면 이 단계가 자동으로 수행됩니다. 게시 폴더에는 애플리케이션 및 해당 종속성에 대한 .exe 및 .dll 파일이 포함됩니다. 자체 포함 애플리케이션에는 .NET 런타임 버전도 포함됩니다. ASP.NET Core 애플리케이션에는 구성 파일, 정적 클라이언트 자산 및 MVC 뷰도 포함됩니다.
ASP.NET Core 애플리케이션은 애플리케이션(또는 서버)이 충돌하는 경우 서버가 부팅되고 다시 시작될 때 시작해야 하는 콘솔 애플리케이션입니다. 프로세스 관리자를 사용하여 이 프로세스를 자동화할 수 있습니다. ASP.NET Core의 가장 일반적인 프로세스 관리자는 Linux의 Nginx 및 Apache, Windows의 IIS 또는 Windows 서비스입니다.
프로세스 관리자 외에도 ASP.NET Core 애플리케이션은 역방향 프록시 서버를 사용할 수 있습니다. 역방향 프록시 서버는 인터넷에서 HTTP 요청을 수신하고 일부 예비 처리 후 Kestrel에 전달합니다. 역방향 프록시 서버는 애플리케이션에 대한 보안 계층을 제공합니다. 또한 Kestrel은 동일한 포트에서 여러 애플리케이션 호스팅을 지원하지 않으므로 호스트 헤더와 같은 기술을 사용하여 동일한 포트 및 IP 주소에서 여러 애플리케이션을 호스팅할 수 없습니다.
그림 7-5. 역방향 프록시 서버 뒤에서 Kestrel에서 호스트되는 ASP.NET
역방향 프록시가 유용할 수 있는 또 다른 시나리오는 SSL/HTTPS를 사용하여 여러 애플리케이션을 보호하는 것입니다. 이 경우 역방향 프록시만 SSL을 구성해야 합니다. 그림 7-6과 같이 역방향 프록시 서버와 Kestrel 간의 통신은 HTTP를 통해 수행됩니다.
그림 7-6. HTTPS 보안 역방향 프록시 서버 뒤에서 호스트되는 ASP.NET
점점 더 많이 사용되는 방법은 Docker 컨테이너에서 ASP.NET Core 애플리케이션을 호스트하는 것입니다. 그러면 로컬로 호스트되거나 클라우드 기반 호스팅을 위해 Azure에 배포될 수 있습니다. Docker 컨테이너는 Kestrel에서 실행되는 애플리케이션 코드를 포함할 수 있으며, 위와 같이 역방향 프록시 서버 뒤에 배포됩니다.
Azure에서 애플리케이션을 호스팅하는 경우 Microsoft Azure Application Gateway를 전용 가상 어플라이언스로 사용하여 여러 서비스를 제공할 수 있습니다. Application Gateway는 개별 애플리케이션에 대한 역방향 프록시 역할을 하는 것 외에도 다음과 같은 기능을 제공할 수 있습니다.
HTTP 부하 분산
SSL 오프로드(SSL은 인터넷 연결에만 적용됨)
종단 간 SSL
다중 사이트 라우팅(단일 Application Gateway에서 최대 20개의 사이트 통합)
웹 애플리케이션 방화벽
Websocket 지원
고급 진단
10장의 Azure 배포 옵션에 대해 자세히 알아봅니다.
참조 – 배포
- 호스팅 및 배포 개요
https://learn.microsoft.com/aspnet/core/publishing/- 역방향 프록시와 함께 Kestrel을 사용하는 경우
https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel#when-to-use-kestrel-with-a-reverse-proxy- Docker에서 ASP.NET Core 앱 호스트
https://learn.microsoft.com/aspnet/core/publishing/docker- Azure Application Gateway 소개
https://learn.microsoft.com/azure/application-gateway/application-gateway-introduction
.NET