Microsoft ASP.NET Core MVC는 모델, 뷰, 컨트롤러로 전체 프로젝트를 구성하는 것이며 이와 함게 관련설정및 인증, 권한, 라우팅, 요청과 파이프라인응답등에 대해서 살펴보고자 합니다.
1. 개요
ASP.NET Core Razor Page는 간단한 웹프로젝트에는 훌륭한 선택이지만 다소 복잡한 프로젝트의 경우에는 복잡합의 체계적인 관리을 위해 구조적인 변화가 필요하고 여기서 Model-View-Controller(MVC) 디자인 패턴이 훌륭한 대안이 될 수 있습니다.
ASP.NET Core MVC는 Razor Page와 비슷해 보이지만 아래 3가지 요소를 통해 기술적인 처리를 명확하게 분리할 수 있도록 합니다.
- Model : 프로젝트에서 데이터 엔티티나 뷰 모델을 나타내는 클래스의 모임입니다.
- View : 뷰 모델에서 데이터를 HTML형태로 표시하는 Razor Page나 기타 화면에 표시되는 영역을 의미합니다.
- Controller : HTTP요청에 대해 behind code를 실행하는 클래스영역입니다. 또한 모델에서 만들어진 뷰 모델을 View 페이지로 전달합니다.
2. MVC 프로젝트 생성
Visual Studio 2022를 실행하고 ASP.NET Core Web App (Model-View-Controller) 템플릿을 선택합니다.
프로젝트명은 원하는 이름으로 입력합니다. 예제에서는 MyWebProject로 하였습니다.
다음 화면에서는 기본설정 그대로 유지하되 인증과 권한부분을 같이 알아볼 것이므로 Authentication Type만 Individual Accounts로 설정합니다.
Individual은 비밀번호를 포함한 사용자계정을 SQLite(기본값)로 저장하는 Individual authentication방식을 의미합니다.
● 인증 database로 SQL Server LocalDB 사용하기
Individual 인증방식은 SQLite를 사용한다고 했는데 필요하다면 SQL Server LocalDB를 대신 사용할 수 있도록 수정할 수 있습니다.
이를 위해 command창을 열고 프로젝트 폴더로 이동한뒤 아래 명령을 내려줍니다.
dotnet ef database update |
그러면 인증정보 저장을 위한 데이터베이스가 만들어 지게 되며 프로젝트의 appsettings.json 파일을 통해 관련 설정이 이루어져 있는 것을 확인할 수 있습니다.
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-MyWebProject-16009E11-84BE-44DC-8B7C-A5A758B3AB36;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
기본적으로 생성되는 데이터베이스파일의 위치는 'C:\Users\[사용자명]\'과 같은 애매한 위치가 될 수 있습니다. 만약 이 위치를 프로젝트의 루트폴더에 /Data와 같은 폴더로 변경하고자 한다면 다음과 같이 설정을 수정합니다.
우선 프로젝트의 Program.cs에서 아래와 같이 연결문자열을 가져오는 변수에서 프로젝트의 루트폴더값으로 경로명을 가져올 수 있도록 변경한뒤
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
if (connectionString.Contains("%CONTENTROOTPATH%"))
{
connectionString = connectionString.Replace("%CONTENTROOTPATH%", builder.Environment.ContentRootPath);
}
appsettings.json파일에 있는 연결문자열에 위에서 지정한 '%CONTENTROOTPATH%'값을 통해 AttachDbFileName을 설정합니다.
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-MyWebProject-D51D6A93-3AA4-4C89-8E33-321EB3116A28;AttachDbFileName=%CONTENTROOTPATH%\\Data\\aspnet-MyWebProject-D51D6A93-3AA4-4C89-8E33-321EB3116A28.mdf;Trusted_Connection=True;MultipleActiveResultSets=true"
},
그리고 Visusl Studio를 종료한뒤 본래 생성된 데이터베이스파일을 찾아 삭제하고 Command창을 열어 프로젝트가 있는 폴더로 이동하고 아래 명령을 내려주면 위에서 지정한 경로에 새로운데이터베이스 파일을 생성해 연결합니다.
dotnet ef database update
|
3. ASP.NET Core MVC 프로젝트 둘러보기
프로젝트의 Properties폴더를 열어보면 launchSettings.json파일을 볼 수 있습니다. 이 파일은 다음과 같이 HTTP와 HTTPS접속을 위한 URL과 랜덤한 숫자의 포트번호를 가지고 있습니다.
"profiles": {
"MyWebProject": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7211;http://localhost:5211",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
특히 포트번호는 필요한 경우 임의로 변경할 수 있습니다.
● 프로젝트 구조
Solution Explorer에서는 전체적인 프로젝트의 구조를 볼 수 있으며 각각 표시되는 폴더의 내용은 아래와 같습니다.
Areas | 웹 사이트 프로젝트를 ASP Core Identity와 통합하는 데 필요한 파일과 폴더가 위치하는 폴더입니다. |
bin, obj | 컴파일된 프로젝트의 어셈블리 혹은 프로세스를 빌드하는데 필요한 임시파일이 있는 폴더입니다. |
Controllers | 모델을 가져와 뷰에 전달하는 Action메서드를 가진 클래스파일을 가진 폴더입니다. |
Data | 인증및 권한등 인증정보를 저장하기 위한 ASP.NET Core 인증을 포함해 기타 DB데이터를 다루기 위한 Entity Framework Core class가 위치한 폴더입니다. |
Models | Controller에서 생성한 데이터를 표현하고 이를 View로 전달하기 위한 C#클래스가 위치한 폴더입니다. |
Properties | launchSettings.json이름의 개발에 필요한 IIS 혹은 IIS Express관련 설정파일이 존재하는 폴더입니다. 해당 파일은 개발하는 동안에만 사용되며 실제 서버로 배포되지 않습니다. |
Views | .cshtml Razor 파일, _ViewStart와 같은 Layout관련 또는 view에서 공용으로 사용되는 네임스페이스를 담은 _ViewImports와 같은 파일을 포함합니다. 또한 경우에 따라 아래 폴더를 포함할 수 있습니다. - Home : 웹사이트의 주요 진입점에 해당하는 home폴더입니다. - Shared : Layout, partial등 공통으로 사용되는 페이지나 스크립트를 가진 폴더입니다. |
wwwroot | 스크립트나 HTML, CSS, Image등 정적 웹사이트에서 사용되는 정적 파일들이 위치한 폴더입니다. |
app.db | 웹사이트에서 사용하는 SQLite database입니다. 프로젝트에서 SQL Server LocalDB를 사용하기로 설정했다면 이 파일은 필요하지 않습니다. |
appsettings.json appsettings.Development.json |
데이터베이스 연결문자열과 같은 runtime에서 읽을 수 있는 설정파일입니다. |
.csproj | 웹사이트 프로젝트 파일입니다. |
Program.cs | Main과 같이 프로그램의 주요 진입점을 포함하는 숨겨진 프로그램 클래스를 정의하며 HTTP요청을 처리하고 웹사이트를 호스트하는 pipeline을 구성합니다. 또한 프로젝트에서 필요한 각종 services를 추가하고 설정하는등 프로젝트의 전반적인 초기구성을 적용합니다. |
● ASP.NET Core Identity 데이터베이스
프로젝트에서 appsettings.json파일을 보면 다음과 같은 설정을 확인할 수 있습니다.
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-MyWebProject-16009E11-84BE-44DC-8B7C-A5A758B3AB36;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
프로젝트에서 identity data 저장소로 SQL Server LocalDB를 사용하기로 한 경우라면 SQL Server Object Explorer를 통해 해당 DB에 연결할 수 있습니다.
● 프로젝트 설정
프로젝트의 Program.cs 파일을 열어보면 기본적으로 top-level program 형식이 사용된 코드를 확인할 수 있습니다.
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MyWebProject.Data;
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.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/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.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
이는 기본적인 구성으로 프로그램의 진입점에 해당하는 Main메서드를 포함한 Program class가 숨겨졌다는 것을 의미합니다. 하지만 현재 상태에서도 대략 4개부분의 주요영역을 나누어 볼 수 있습니다.
우선 첫번째로 필요한 Namespace를 import하는 부분입니다.
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MyWebProject.Data;
보기에는 3개의 Namespace만 추가된것 처럼 보이지만 사실은 obj\Debug\net6.0경로에서 [프로젝트명].GlobalUsings.g.cs파일을 보면 기본적으로 필요한 네임스페이스가 global Namespace로 선언되어 있음을 확인할 수 있습니다.
두번째는 web host builder를 설정하고 생성하는 부분입니다.
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.AddControllersWithViews();
우선 데이터의 저장을 위해 appsettings.json 파일로 부터 읽은 연결문자열과 함께 SQL 서버(혹은 SQLite)를 사용하는 application database context를 등록하며 인증을 위한 ASP.NET Core Identity와 application데이터베이스 사용을 위해 설정을 추가하고 MVC controller및 views사용을 위한 서비스를 추가하는 것을 볼 수 있습니다.
builder의 경우 일반적으로 사용되는 2개의 객체를 가지고 있는데 하나는 Services이며 다른 하나는 Configuration입니다.
Configuration의 경우 appsettings.json파일의 설정이나 환경변수, 명령해 인수등을 통해 설정된 모든 값을 가지며 Services는 등록된 모든 의존성 서비스를 관할하는 객체입니다. ASP.NET Core는 dependency injection(DI : 의존성 주입) 디자인 패턴을 구현함으로서 Controller와 같은 곳에서 생성자를 통해 필요한 서비스를 요청할 수 있습니다.
세번째는 HTTP요청 파이프라인을 설정하는 부분입니다.
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/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.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
웹사이트가 개발자모드로 동작하는 경우를 위한 상대적인 URL경로를 설정하거나 사용자에게 친숙한 에러 페이지및 HSTS를 설정합니다. 또한 HTTPS redirection과 static files, routing그리고 ASP.NET Identity를 활성화하고 MVC 기본 route및 Razor Page를 설정합니다.
MapControllerRoute()는 기본적인 MVC패턴에 다라 route를 찾게 합니다. 또한 MapRzorPages()는 RzorPage맵핑을 위해 사용되는 것이며 현재 프로젝트에서는 인증 및 허가를 위해 ASP Core Identity를 사용하고 방문자 등록 및 로그인 같은 사용자 인터페이스 구성요소에 Razor Class Library를 사용하기 위해 존재합니다.
마지막 네번째는 thread-blocking 메서드로 웹사이트를 동작시키고 HTTP요청 대기및 응답하도록 하는 부분입니다.
app.Run();
● MVC Route
Route는 요청을 통해 Controller의 이름을 찾아 해당 Controller의 Instance를 생성하고 HTTP응답을 생성하는 Method로 매개변수값(존재하는 경우)을 전달해 Action Method를 실행하는 역활을 수행합니다.
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
위 설정은 MVC Route의 기본설정이며 세그먼트라고 하는 중괄호를 통해 Route의 패턴이 지정되어 있는 것을 볼 수 있습니다.
세그먼트는 URL로 구성되며 대소문자를 구별하지 않는데 순서에 따라 Controller, Method, Parameter값과 일치하게 구성되어 있고 해당 구조에 맞게 URL이 구성되지 않는 경우 controller는 Home으로 action은 Index라는 기본값이 적용되도록 되어 있습니다. Parameter는 없으면 무시됩니다.
URL | Controller | Action | Parameter(id) |
/ | Home | Index | |
/User | User | Index | |
/User/Admin | User | Admin | |
/User/Admin/10 | User | Admin | 10 |
● Controller
MVC에서 C는 Controller를 의미하며 Route는 URL를 통해 해당 Controller Class를 찾게 됩니다. 이러한 이유로 Controller는 다른 일반적인 Class와 다르게 [Controller]라는 attribute를 가져야 합니다. 보통 ASP.NET MVC 프로젝트를 생성하게 되면 Controller는 기본적으로 Controller 클래스에서 상속받도록 되어 있고 Controller 클래스는 다시 ControllerBase클래스에서 상속받고 있는데 ControllerBase에서 이미 [Controller] attribute가 적용되어 있으므로 파생클래스에서 attribute재적용은 필요하지 않습니다.
Contoller에서는 다음의 유용한 속성을 제공하고 있으며
- ViewData : Controller에서 저장한 키와 값의 쌍으로 된 데이터이며 뷰에서 접근할 수 있습니다. 대부분 현재의 요청과 응답에 한정됩니다.
- ViewBag : ViewData를 wrap한것으로 손쉽게 값을 설정하고 가져올 수 있도록 한 동적객체입니다.
- TempData : ViewData와 개념은 동일하지만 동일한 세션하에서 다른 요청과 응답에서도 사용될 수 있습니다. 이때문에 redirect되는 경우 값을 저장하기에 유용하며 저장된 값은 다음 요청에서도 읽을 수 있습니다.
이 밖에 Controller는 기본적으로 뷰와 연결될 수 있도록 아래와 같은 메서드를 제공하고 있습니다.
- View : 뷰를 실행한 후 ViewResult를 반환합니다. 뷰는 필요한 경우 모델등을 사용한 동적컨텐츠나 기타 HTML을 HTTP응답에 대응되는 Web Page로 Rander합니다.
- PartialView : 뷰를 실행한 후 PartialViewResult를 반환하며 전체 응답중 일부의 영역을 생성합니다.
- ViewComponent : 동적 HTML을 생성하는 component를 실행한 후 ViewComponentResult를 반환합니다.
- Json : Json 객체에 대응하는 JsonResult를 반환합니다. 대부분 HTML대신 간소화된 응답을 필요로 하는 Web API를 구현하는데 사용됩니다.
Controller는 일반적으로 Controller가 유효한 상태에 있고 제대로 클래스 생성자에서 제대로 작동하기 위한 서비스를 식별하며 Action이름을 통해 실행할 메서드를 식별합니다. 또한 뷰모델을 구성하기 위해 필요한 데이터를 HTTP요청으로 부터 추출하고 처리된 결과를 HTML이나 특정 파일, Json과 같은 것은 뷰로 생성하여 HTTP상태코드와 함께 클라이언트에 반환합니다.
아래는 기본적으로 생성되는 Controller의 Code를 보여주고 있습니다.
using Microsoft.AspNetCore.Mvc;
using MyWebProject.Models;
using System.Diagnostics;
namespace MyWebProject.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}
private readonly로 선언된 _logger는 HomeController의 생성자에서 설정되는 logger의 참조를 저장하기 위한 필드이며 정의된 3개의 모든 메서드는 View() 메서드를 호출하여 IActionResult형식의 interface를 반환하고 있습니다.
마지막 Error() 액션 메서드에서는 View() 메서드에서 추적을 위해 사용되는 요청 id와 함게 뷰모델을 전달하고 있고 응답이 캐시되지 않도록 설정되었습니다.
위에서 이미 언급한 것처럼 사용자가 /나 /home 경로를 URL로 요청하게 되면 규칙에 따라 Home Controller의 Index() 액션메서드를 호출하여 실행합니다.
● ControllerBase
ControllerBase자체로는 뷰를 지원하지 않고 웹서비스 생성을 위해서만 사용되는데 아래와 같은 HTTP context를 위한 유용한 몇가지 속성을 가지고 있습니다.
- Request : HTTP 요청과 관련된 것으로 Header, Query String, 요청 Body Stream, content type, cookie등에 대한 정보를 가지고 있습니다.
- Response : HTTP 응답과 관련된 것으로 header, 응답 Body Stream, content type, HTTP 상태 코드, cookie등에 대한 정보를 가지고 있으며 OnStarting 및 OnCompleted와 같은 delegate를 통해 Method의 동작을 가로챌 수 있습니다.
- HttpContext : 요청과 응답을 포함한 HTTP context전체를 나타냅니다. 추가된 middleware를 통한 서버의 특서및 연결정보를 가지며, 사용자에 대한 인증및 권한정보에 관한 것을 포함합니다.
● Action과 View
Index와 Privacy메서드는 실행을 구분하기 위한 것으로 관례에 따라 각자의 Web Page를 반환합니다. 관례는 기본적으로 Action과 동일한 이름의 cshtml뷰를 검색하여 반환하는 것입니다. cshtml뷰페이지는 기본적으로 Views폴더의 Home폴더 안에서 검색되며 경우에 따라 Shared와 같은 폴더를 대상으로 검색하기도 합니다.
● logging
예제로 생성한 프로젝트의 HomeController.cs를 보면 생성자 주입을 통해 ILogger인터페이스로 Logger의 객체를 받고 있음을 볼 수 있습니다.
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
이때 객체가 대입되는 _logger를 사용하면 Console에서 필요한 메세지를 담아 다양한 레벨로 출력할 수 있습니다.
public IActionResult Index()
{
_logger.LogError("에러!!");
_logger.LogWarning("경고!!");
return View();
}
● filter
Controllers와 Action에 자신만의 기능을 추가하고자 하는 경우 filter를 사용할 수 있으며 attribute 클래스로 구현하여 적용이 필요한 Controller와 Action에 attribute로 적용합니다. filter를 Controller에 적용하면 Controller에 속한 모든 Action메서드에 적용되며 Action에 적용하는 경우 해당 Action메서드에만 filter의 기능이 적용됩니다. 만약 프로젝트의 모든 부분에 특정한 filter를 적용해야 하는 경우라면 아래와 같이 AddControllersWithViews() 메서드를 통해 MvcOptions인스턴스의 Filters collection에 적용하고자 하는 filter를 지정해 줄 수 있습니다.
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(typeof(MyFilter));
});
기존 filter중에서 attribute로 적용되는 대표적인 것으로 [Authorize]가 있으며 Controller나 Action메서드에 접근하기전 인증과 권한에 대한 filter를 적용할 수 있도록 합니다.
[Authorize]
public IActionResult Index()
{
return View();
}
[Authorize]는 인증된 사용자만이 접근할 수 있도록 제한하는 것입니다. 만약 인증된(로그인된) 사용자 중에서도 특정 Group에 속한 사용자만을 접근대상으로 하는 경우라면 Roles와 같은 속성을 사용할 수 있습니다.
[Authorize(Roles = "admin")]
위와 같이 Controller나 Action메서드에 필요한 filter를 적용하는 것을 'decorate'라고 합니다.
● Role관리및 생성
ASP.NET Core MVC 프로젝트에서 Role관리를 위해서는 우선 아래와 같이 AddRoles를 호출하는 구문을 추가합니다.
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true).AddRoles<IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();
그리고 프로젝트에 RolesController이름의 새로운 Controller를 추가하고 다음과 같이 RolesController.cs를 수정합니다.
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace MyWebProject.Controllers;
public class RolesController : Controller
{
private string AdminRole = "admin";
private string UserEmail = "admin@cliel.com";
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<IdentityUser> _userManager;
private readonly ILogger<HomeController> _logger;
public RolesController(RoleManager<IdentityRole> roleManager, UserManager<IdentityUser> userManager, ILogger<HomeController> logger)
{
_roleManager = roleManager;
_userManager = userManager;
_logger = logger;
}
public async Task<IActionResult> Index()
{
if (!(await _roleManager.RoleExistsAsync(AdminRole)))
{
await _roleManager.CreateAsync(new IdentityRole(AdminRole));
}
IdentityUser user = await _userManager.FindByEmailAsync(UserEmail);
if (user == null)
{
user = new();
user.UserName = UserEmail;
user.Email = UserEmail;
IdentityResult result = await _userManager.CreateAsync(user, "Aa!12345");
if (!result.Succeeded)
{
foreach (IdentityError error in result.Errors)
{
_logger.LogError(error.Description);
}
}
}
if (!user.EmailConfirmed)
{
string token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
IdentityResult result = await _userManager.ConfirmEmailAsync(user, token);
if (!result.Succeeded)
{
foreach (IdentityError error in result.Errors)
{
_logger.LogError(error.Description);
}
}
}
if (!(await _userManager.IsInRoleAsync(user, AdminRole)))
{
IdentityResult result = await _userManager.AddToRoleAsync(user, AdminRole);
if (result.Succeeded)
{
foreach (IdentityError error in result.Errors)
{
_logger.LogError(error.Description);
}
}
}
return Redirect("/");
}
}
우선 새로 추가한 RolesController.cs에서는 새로운 사용자를 추가하기 위해 사용자의 Role과 이메일을 가진 2개의 Field를 만들었고, 생성자를 통해 Role관리를 위한 RoleManager와 User관리를 위한 UserManager의 객체를 생성자 주입을 통해 전달받도록 하였습니다.
Index에서는 첫번째로 Admin이라는 Role이 존재하지 않으면 새롭게 생성하도록 하고 있으며 사용자 또한 이메일을 통해 기존 사용자를 검색 후 존재하지 않는 사용자라면 해당 사용자를 새롭게 생성하도록 하였습니다. 또한 Admin Role에도 사용자를 할당하도록 합니다.
프로젝트를 실행하고 '/roles' URL로 이동합니다. 아무런 문제없이 첫페이지가 다시 나오면 성공적으로 Role과 사용자가 추가된 것입니다. HomeController.cs의 Privacy에 Authorize attribute를 추가하고
[Authorize(Roles = "admin")]
public IActionResult Privacy()
{
return View();
}
다시 프로젝트를 실행시켜 '/privacy'로 이동합니다. 그러면 로그인페이지가 나오게 되고 해당 로그인페이지에서 조금전 등록했던 이메일과 비밀번호를 지정해 로그인 합니다.
아래와 같이 /Privacy 페이지가 정상적으로 표시되면 로그인과 권한인증이 성공한 것입니다.
● cache
HTTP응답성의 성능향상을 위해 종종 cache가 사용되며 특정 Action 메서드에 cache사용을 위해서는 [ResponseCache] 속성을 사용합니다.
[ResponseCache] 속성에서는 아래와 같이 cache가 응답되는 곳과 얼마 동안 cache가 응답할지를 매개변수를 통해 설정할 수 있습니다.
[ResponseCache(Duration = 20, Location = ResponseCacheLocation.Any)]
public IActionResult Index()
{
return View();
}
Duration은 max-age HTTP 응답헤더를 초단위로 설정함으로서 cache의 응답기간을 지정합니다. Location은 Any, Client, 또는 None으로 설정할 수 있으며 cache-control HTTP 응답헤더를 설정합니다.
또한 NoStore를 설정할 수 있는데 이 값이 true면 Duration과 Location설정을 무시하고 cache-control HTTP응답헤더를 no-store로 설정합니다.
Index에 [ResponseCache]를 설정하고 View에서는 다음과 같이 현재 시간을 표시하도록 한뒤
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
<span>@DateTime.Now.ToLongTimeString()</span>
</div>
프로젝트를 실행하여 처음 표시되는 시간에 주목해 주세요. Privacy와 같은 다른 페이지를 갔다가 오시 Index페이지로 오면 처음표시된 시간과 동일한 시간을 표시할 것입니다. 하지만 Duration을 통해 20초를 설정했으므로 다른 페이지로 이동했다가 20초 이상 기다린뒤 다시 Index페이지로 돌아오면 그때는 현재 시간이 바뀌게 됩니다.
● route 구성하기
특정 Action 메서드에는 필요에 따라 Route Attribute를 통해 필요한 route를 정의할 수 있습니다.
예를 들어 현재 프로젝트에서 Privacy페이지로 이동하기 위한 URL은 'https://localhost:8990/Home/Privacy'인데 이것을 'https://localhost:8990/Privacy'로 변경하고자 한다면 Privacy Action 메서드에 Route Attribute를 다음과 같이 설정해야 합니다.
[Route("Privacy")]
public IActionResult Privacy()
● entity와 view model
MVC에서 M은 model을 의미합니다. model은 요청이나 응답에 대한 데이터 자체를 표현하는 것으로 entity model과 view model이라는 2가지가 사용됩니다.
entity model은 SQL Server에서의 데이터와 같은 것을 표현하는 model이며 class와 같은 것으로 정의되고 실제 Database를 상대로 존재하는 데이터를 가져오거나 데이터를 추가하고 변경할 수 있습니다.
view model은 HTTP응답을 통해 사용자에게 특정 데이터를 보여주기 위한 것으로 대부분 Controller에서 View로 전달되는 것입니다. view model은 entity model과 달리 데이터의 추가나 변경을 목적으로 하지 않으므로 class보다는 record로 정의합니다.
간단한 view model의 구현을 위해
[.NET/ASP.NET Core] - [ASP.NET Core] Razor Page로 웹프로젝트 만들기
글에서 만들었던 방법으로 프로젝트의 Data폴더에 Northwind DB를 생성하고 생성자 주입을 통해 NorthwindContext의 인스턴스를 가져올 수 있도록 합니다.
private readonly ILogger<HomeController> _logger;
private readonly NorthwindContext _db;
public HomeController(ILogger<HomeController> logger, NorthwindContext db)
{
_logger = logger;
_db = db;
}
그리고 Models에서는 다음과 같은 형식의 Record를 추가합니다.
using MyWebProject.Data;
namespace MyWebProject.Models
{
public record ViewModel(IList<Products>? ProductList);
}
Index Action 메서드에서는 위에서 정의 ViewModel을 통해 실제 DB에서 Northwind의 Products테이블 데이터를 설정하고 View로 생성한 ViewModel을 넘겨주도록 합니다. 이때 entity model은 _db의 Products가 되고 view model은 ViewModel이 됩니다.
public IActionResult Index()
{
ViewModel model = new(_db.Products.ToList());
return View(model);
}
● View
MVC에서 V는 View를 의미하며 사용자에게 친숙한 HTML이나 기타 다른 형식으로 model을 가져와 표시하는 역활을 수행합니다. ASP.NET Core MVC에서 사용하는 View Engine으로 Razor가 있으며 Razor는 View에서 활용할 수 있는 다양한 Engine중 하나에 해당합니다. 그리고 Razor를 활용한 RazorPage에서는 @문자를 통해 Server-Side Code를 구현할 수 있습니다.
Views 폴더에 보면 _ViewStart.cshtml 이름의 파일을 볼 수 있는데 이 파일은 다음과 같은 구현을 가지고 있습니다.
@{
Layout = "_Layout";
}
_ViewStart.cshtml은 모든 View에서 적용되는 기본적인 설정을 포함하는 파일로 Layout은 모든 View페이지에서 사용하는 Layout페이지를 지정하는 것입니다. 실제 프로젝트의 Shared폴더에 보면 _Layout.cshtml파일이 있고 해당 파일이 Layout으로 지정되어 있으므로 Index.cshtml과 같은 파일에서도 별도의 지정없이 기본적으로 _Layout.cshtml파일의 Layout이 적용된 형태로 HTML이 렌더링되는 것입니다.
_ViewImports.cshtml는 모든 View Page에 적용될 기본적인 Import구문을 가진 파일입니다.
@using MyWebProject
@using MyWebProject.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
_Layout.cshtml파일은 앞서 설명된것처럼 모든 View Page에 적용될 Layout이 설정되어 있습니다. 우선
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/MyWebProject.styles.css" asp-append-version="true" />
처럼 필요한 각종 css파일이 지정된 것을 볼 수 있는데 여기서 '~/css/site.css'처럼 ~표시는 최상위 wwwroot를 의미하는 문자입니다. 또한 parital로 부분 페이지가 지정된 것을 볼 수 있는데
<partial name="_LoginPartial" />
해당 설정에서 _LoginPartial역시 Shared폴더에 존재하는 파일과 정확히 대응됩니다.
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
부분에서는 @RenderBody()를 통해서 해당 Layout을 사용하는 Index.cshtml과 같은 HTML요소가 표시될 수 있도록 하는 것입니다.
_Layout.cshtml페이지에서는 이 밖에 필요한 script가 지정되어 있는것도 확인할 수 있습니다.
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
여기서 사용된 asp-append-version속성은 img혹은 script등에 사용할 수 있는 것으로 이 속성이 true면 Image Tag Helper를 호출하여 자동적으로 v라는 Query String을 생성해 파일의 hash를 값으로 설정합니다.
<script src="/js/site.js?v=4q1jwFhaPaZgr8WAUSrux6hAuh0XDg9kPS3xIVq36I0"></script>
이것은 file의 cache를 위한 것으로 file에 별다른 수정이 없다면 cached된 file을 호스트하게 되지만 만약 파일이 수정되면 hash값이 바뀌게 되고 그러면 새로운 file을 사용자에게 제공할 수 있게 됩니다.
또한 RenderSectionAsync는 _Layout.cshtml을 사용하는 다른 페이지에서 추가적으로 필요한 Script를 추가할 수 있는 영역을 설정하는 것으로 required가 true가 되면 필수적으로 각 view 페이지에서 Scripts영역의 구현을 강제하게 됩니다.
4. MVC 프로젝트 수정하기
이전 단계에서 Northwind의 Proudcts테이블 데이터를 가져와 ViewModel을 생성했는데 이를 Index.cshtml에서 표시하려면 우선 페이지 상단에 사용할 model을 정의합니다.
@using MyWebProject.Models
@model ViewModel
이때 model의 선언은 소문자로, 실제 model을 사용하는 경우에는 Model처럼 대문자로 시작해야 함에 주의하시기 바랍니다.
이제 html을 통해 가져온 Product의 목록을 표시하도록 합니다.
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
@if (Model.ProductList is not null)
{
<table>
@foreach(var product in Model.ProductList)
{
<tr>
<td>@product.ProductId</td>
<td>@product.ProductName</td>
<td>@product.UnitPrice</td>
</tr>
}
</table>
}
</div>
● Product의 상세 표시
Product의 목록을 보여준 상태에서 사용자가 원하는 Product의 상세내역을 보여주기 위해 별도의 Action 메서드와 페이지를 추가해 보도록 하겠습니다.
우선 HomeController.cs에서 ProductDetail이라는 이름의 Action 메서드를 추가합니다.
public IActionResult ProductDetail(int? id)
{
if (!id.HasValue)
{
return BadRequest("대상 제품을 확인할 수 없습니다.");
}
Products? p = _db.Products.Where(x => x.ProductId == id).SingleOrDefault();
if (p == null)
return NoContent();
return View(p);
}
ProductDetail() Action 메서드는 id 매개변수값을 받아 해당 값에 맞는 Product를 찾아 view로 Product의 model을 전달합니다.
그리고 ProductDetail이름과 일치하는 새로운 View Page를 추가해야 하는데 HomeController이므로 Home폴더 안에 ProductDetail.cshtml이름의 View Page를 생성합니다.
그리고 전달된 model을 가져와 HTML로 표시할 수 있도록 다음과 같이 수정합니다.
@using MyWebProject.Data
@model Products
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
@if (Model is not null)
{
<dl>
<dt>Product Name</dt>
<dd>@Model.ProductName</dd>
<dt>Product UnitPrice</dt>
<dd>@Model.UnitPrice</dd>
<dt>Product UnitsInStock</dt>
<dd>@Model.UnitsInStock</dd>
<dt>Product UnitsOnOrder</dt>
<dd>@Model.UnitsOnOrder</dd>
<dt>Product ProductId</dt>
<dd>@Model.ProductId</dd>
</dl>
}
ProductDetail Action 메서드는 Product의 ID값을 받도록 되어 있으므로 Index.html에서는 아래와 같이 a link 태그를 추가해 줍니다.
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
@if (Model.ProductList is not null)
{
<table>
@foreach(var product in Model.ProductList)
{
<tr>
<td><a asp-controller="Home" asp-action="ProductDetail" asp-route-id="@product.ProductId">@product.ProductId</a></td>
<td>@product.ProductName</td>
<td>@product.UnitPrice</td>
</tr>
}
</table>
}
</div>
a 태그는 asp-controller와 asp-action으로 route를 지정하였으며 asp-route-id를 통해 id 매개변수가 사용됨을 나타내고 있습니다. 만약 id대신 name이나 기타 다른 이름의 변수를 사용하려 한다면 asp-route-name과 같이 지정해 주면 됩니다. 물론 Action Method의 매개변수명도 동일하게 바꿔줘야 합니다.
● model binder
라우트가 Controller class와 호출할 Action Method를 식별한 이 후에 Method가 매개변수를 가지고 있다면 이 매개변수에는 값이 설정되어 있어야 합니다.
- Route 매개변수 : 예제에서 사용했었던 id와 같은 것으로 URL을 통해 값을 전달합니다.
- Query string 매개변수 : '/Home/ProductDetail?id=1'처럼 ?뒤에 매개변수 이름과 값의 쌍으로 필요한 데이터를 전달합니다.
- Form 매개변수 : HTML Form태그를 통해 필요한 데이터를 전달합니다.
여기서 Model binder는 int나 bool과 같은 단일적인 형식에서 부터 class나 record, struct와 같은 복합형식이나 배열까지 거의 대부분 형식의 데이터를 매핑할 수 있습니다.
예제 프로젝트에서 model binder를 어떻게 활용할 수 있는지를 알아보기 위해 우선 Models폴더에 아래 내용의 Region.cs 클래스파일을 생성합니다.
namespace MyWebProject.Models
{
public class Region
{
public int RegionId { get; set; }
public string? RegionDescription { get; set; }
}
}
HomeController.cs에서는 아래 Region Action 메서드를 추가하고
public IActionResult Region()
{
return View();
}
해당 Action메서드로 진입할때 표시할 Razor Page도 추가합니다.
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<form method="post" action="/home/region?regionid=5">
<input name="RegionDescription" value="Etc" />
<input type="submit" />
</form>
여기서 '/home/region/5'와 같이 지정되었다면 이것은 route 매개변수가 될 수 있으나 '?regionid=5'형태로 값이 지정되었으므로 Query String 매개변수에 해당합니다.
RegionDescription은 form안에 존재하여 submit을 통해 값이 전달되므로 form 매개변수가 됩니다. regionid와 RegionDescription값은 action에 의해 다시 /home/region에 전달하도록 하고 있으므로 이를 받을 수 있는 Action Method를 HomeController.cs에 하나더 추가하도록 합니다.
[HttpPost]
public IActionResult Region(Models.Region r)
{
_db.Region.Add(new Data.Region { RegionId = r.RegionId, RegionDescription = r.RegionDescription });
_db.SaveChanges();
return RedirectToAction("Index");
}
Region Razor Page에서 regionid와 RegionDescription값을 전달하면 Model binder는 위에서 만든 Region 클래스의 필드에 값을 바인딩하게 되고 이렇게 전달된 값을 Northwind의 Region Table로 추가하도록 하고 있습니다.
위에서 추가한 Region에 [HttpPost] attribute가 decorate되었음에 주목해 주세요. 이전에 추가한 Region과 메서드가 동일하므로 일반적인 GET요청(Razor Page를 표시하기 위한)인지 실제 데이터가 전달되는 POST요청인지 따라 실행할 Method를 구분해 줘야 하므로 두번째 Region에 Post요청의 경우에만 처리될 수 있도록 한 것입니다.
만약 어떤 Action Method에 특정 요청에 대한 attribute가 설정되어 있지 않다면 해당 Method는 GET, POST, PUT, DELTE등 거의 모든 요청을 받을 수 있는 상태가 됩니다.
프로젝트를 실행하여 /home/region URL로 이동한뒤
Submit 버튼을 누르고 잠시 후 메인페이지가 정상적으로 출력된다면 성공적으로 Region이 등록되었을 것입니다.
참고로 같은 이름의 매개변수가 여러개 존재한다면 가장 우선은 form 매개변수이며 query string이 가장 낮은 우선권을 가집니다.
● model 유효성 검증
Model에는 값을 전달하는 경우 정말 해당 값이 유효한지를 검증할 수 있도록 validation 규칙을 decorate할 수 있습니다. 이때 바인딩되는 데이터와 유효성 검증 오류는 ControllerBase의 ModelState를 통해 확인할 수 있습니다.
이전에 만든 Region모델에 유효성을 추가하기 위해 Region모델을 아래와 같이 수정합니다.
public class Region
{
public int RegionId { get; set; }
[Required(AllowEmptyStrings = false, ErrorMessage = "설명을 반드시 입력해 주세요.")]
public string? RegionDescription { get; set; }
}
예제에서 RegionDescription은 Requred를 통해 반드시 값이 존재하도록 하였으며 추가적으로 AllowEmptyStrings속성을 false로 하여 공백을 허용하지 않도록 하였습니다. 또한 값이 정말 존재하지 않는경우 ErrorMessage설정을 통해 사용자에게 제공할 메세지를 지정하였습니다.
[HttpPost]
public IActionResult Region(Models.Region r)
{
IEnumerable<string> errorMessage;
if (!ModelState.IsValid)
{
errorMessage = ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage);
TempData["error"] = JsonConvert.SerializeObject(errorMessage);
return RedirectToAction("Index");
}
_db.Region.Add(new Data.Region { RegionId = r.RegionId, RegionDescription = r.RegionDescription });
_db.SaveChanges();
return RedirectToAction("Index");
}
Region Action 메서드에서는 모델의 유효성 여부를 IsValid속성으로 판단하고 이 값이 false면(즉, 유효하지 않으면) ModelState의 Values로 에러메세지를 추출해 IEnumerable형식으로 TempData에 저장합니다.
@using MyWebProject.Models
@using Newtonsoft.Json
@model ViewModel
@{
ViewData["Title"] = "Home Page";
IEnumerable<string>? errors = JsonConvert.DeserializeObject<IEnumerable<string>?>((string?)TempData["error"] ?? string.Empty);
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
@if (errors is not null)
{
foreach(var error in errors)
{
<span>@error</span>
}
}
@if (Model.ProductList is not null)
{
<table>
@foreach(var product in Model.ProductList)
{
<tr>
<td><a asp-controller="Home" asp-action="ProductDetail" asp-route-id="@product.ProductId">@product.ProductId</a></td>
<td>@product.ProductName</td>
<td>@product.UnitPrice</td>
</tr>
}
</table>
}
</div>
Index.cshtml에서는 Region Action에서 저장한 TempData의 error값을 가져오고 값이 존재하면 관련 에러메세지를 순회하면서 <span>태그에 내용을 표시하도록 합니다.
● view helper method
사용자에게 화면을 표시하기 위한 View페이지에서는 HTML을 생성하기 위한 HTML 객체와 그와 관련한 메서드를 제공하고 있으며 아래에서 자주 사용되는 메서드를 나열하였습니다.
메서드 | 기능 |
ActionLink | Controller와 Action을 지정하는 URL을 포함한 <a> 태그를 생성합니다. 예를 들어 Html.ActionLink(linkText: "메인", actionName: "Index", controllerName: "Home") 라고 하면 <a href="/Home/Index">메인</a>태그를 생성하는 식입니다.(/Home/Index는 /로 대체될 수 있음) 또는 anchor tag helper를 통해 <a asp-action="Index" asp-controller="Home">메인</a>처럼 태그를 직접 작성할 수 있습니다. |
AntiForgeryToken | <form>내부에 anti-forgery token이 담긴 <hidden>태그를 삽입합니다. 이 것은 form이 submit될때 데이터가 정말 지정된 <form>에서 post된 데이터인지를 검증합니다. |
Display 또는 DisplayFor | display template을 통해 현재 Model과 연관된 HTML을 생성합니다. .NET types을 위한 display templates이 내장되어 있으며 사용자 template은 DisplayTemplates폴더에 생성될 수 있습니다. 이때 folder이름은 linux와 같은 대소문자를 구별하는 filesystem에서는 정확히 대소문자를 구별해야 합니다. |
DisplayForModel | 단일 표현식대신 전체 Model을 통해 HTML을 생성합니다. |
Editor 또는 EditorFor | editor template을 통해 현재 Model과 연관된 HTML을 생성합니다. <label>과 <input>을 사용하는 .NET types을 위한 editor templates이 내장되어 있으며 사용자 template은 EditorTemplates 폴더에 생성될 수 있습니다. 이때 folder이름은 linux와 같은 대소문자를 구별하는 filesystem에서는 정확히 대소문자를 구별해야 합니다. |
EditorForModel | 단일 표현식대신 전체 Model을 통해 HTML을 생성합니다. |
Encode | 특정 객체나 string을 HTML로 안전하게 Encode하기 위한 것입니다. 예를 들어 웹사이트를 공격할 수 있는 <script>와 태그 문자열을 '<script>'처럼 Encode하여 표현하는 것입니다. 다면 Razor @문자는 기본적으로 string을 Encode하여 표현하므로 일반적으로 자주 사용되지는 않습니다. |
Raw | string을 Encode없이 HTML을 생성하도록 합니다. |
PartialAsync 또는 RenderPartialAsync | partial view의 HTML을 생성하도록 합니다. 이때 선택적으로 Model이나 View로 특정 데이터를 전달할 수 있습니다. |
위 메서드를 사용하면 이전 예제에서 만들었던 Region.cshtml은 다음과 같이 수정될 수 있습니다.
public class Region
{
public int RegionId { get; set; }
[DisplayName("지역")]
[Required(AllowEmptyStrings = false, ErrorMessage = "설명을 반드시 입력해 주세요.")]
public string? RegionDescription { get; set; }
}
@model Region
@{
}
<form method="post" action="/home/region?regionid=5">
<input name="RegionDescription" value="Etc" />
<input type="submit" />
</form>
<p>추가지역</p>
@if (Model is not null)
{
<p>@Html.LabelFor(Model => Model.RegionDescription)</p>
<span>@Html.DisplayFor(Model => Model.RegionDescription)</span>
}
[HttpPost]
public IActionResult Region(Models.Region r)
{
IEnumerable<string> errorMessage;
if (!ModelState.IsValid)
{
errorMessage = ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage);
TempData["error"] = JsonConvert.SerializeObject(errorMessage);
return RedirectToAction("Index");
}
_db.Region.Add(new Data.Region { RegionId = r.RegionId, RegionDescription = r.RegionDescription });
_db.SaveChanges();
return View(r);
}
5. 비동기
Desktop이나 Mobile용 앱에서는 프로그램의 반응성 향상을 위해 어떤 처리를 진행하는 중에도 사용자와의 상호작용이 계속 유지될 수 있도록 하는 비동기처리가 구현되기도 합니다.
이는 Web Application에서도 그대로 적용될 수 있습니다. 예를 들어 대량의 파일작업을 수행하거나 특정 데이터를 서버에 요청하는 경우 응답이 돌아올때까지 대기해야하는 경우가 발생할 수 있는데 이런 경우 비동기처리를 통해 기존에 처리되던 작업은 계속 유지하고 사용자에게는 즉각적인 응답을 수행함으로서 사용자와의 상호작용을 유지하도록 하는 것입니다.
HTTP요청이 웹서버에 도달하면 해당 pool의 스레드가 요청을 처리하기 위해 할당됩니다. 하지만 이 스레드가 리소스를 기다려야 하는 경우 이 후에 들어오는 모든 요청을 처리할 수 없게 됩니다. 이때 웹사이트가 pool에 있는 스레드보다 더 많은 수의 요청을 동시에 받게 된다면 이들 중 일부 요청은 503 Service Unavailable과 같은 에러를 표시할 수 있고 이렇게 잠겨진 스레드는 작업을 처리할 수 없게 됩니다.
이러한 문제를 해결하는 방안중 하나는 직업 비동기를 구현하는 것입니다. 비동기는 스레드가 응답에 필요한 리소스를 대기중인것과는 상관없이 스레드 pool을 반환하고 다른 요청을 처리할 수 있게 함으로서 동시에 처리할 수 있는 요청수를 증가시켜 웹사이트의 반응성을 향상시킬 수 있습니다.
차라리 거대한 스레드 pool을 가지면 되지 않을까 생각할 수 있지만 대부분의 운영체제는 1MB의 스택을 가지고 있으며 비동기 메서드는 더 적은 메모리를 사용하므로 이것은 pool에 새로운 스레드를 생성해야할 필요성을 제거합니다. 일반적으로 pool에 새로운 스레드가 추가되는 속도는 1개의 스레드당 대략 2초정도가 소요되는데 이는 비동기 스레드사이를 전환하는 것보다 훨씬 더 오래 걸리는 시간입니다.
● 비동기 구현
비동기 구현을 위해 필요한 것은 Action Method에 async와 응답에 await를 사용하는 것입니다.
public async Task<IActionResult> ProductDetail(int? id)
{
if (!id.HasValue)
{
return BadRequest("대상 제품을 확인할 수 없습니다.");
}
Products? p = await _db.Products.Where(x => x.ProductId == id).SingleOrDefaultAsync();
if (p == null)
return NoContent();
return View(p);
}
'.NET > ASP.NET' 카테고리의 다른 글
[ASP.NET Core] Blazor 웹 프로젝트 시작하기 (0) | 2022.04.01 |
---|---|
[ASP.NET Core] ASP.NET Core Web API (2) | 2022.03.25 |
[ASP.NET Core] HttpContext.User (0) | 2022.02.10 |
[ASP.NET Core] Razor Page로 웹프로젝트 만들기 (10) | 2022.01.24 |
[ASP.NET Core] Session (0) | 2021.11.14 |