재우니의 블로그

 

이 기사에서는 CQRS가 무엇인지, 이 디자인 패턴을 사용할 때의 장단점에 대해 간략하게 논의한 다음 데모 프로젝트에서 CQRS를 MediatR과 함께 실제로 구현해 보겠습니다.

<번역이 깕끔하지 않을 수 있으니 참고해 주시기 바랍니다.>

 

CQRS 가 무엇인가?

 

CQRS는 Command Query Responsibility Segregation의 약어입니다. CQRS는 응용 프로그램 개발자가 Command 와 Query logic 를 쉽게 분리할 수 있도록 하는 디자인 패턴입니다. 다음은 Command 와 Query logic 이 의미하는 바에 대한 간략한 설명입니다.

 

 

MediatR 이 무엇인가?

 

응용 프로그램에 MediatR 을 추가함으로써 개체 간의 종속성(dependencies)을 줄여서 응용 프로그램의 결합이 줄어들고(less coupled) 유지 관리가 쉬워집니다.

 

MediatR은 in-process 메시징을 허용합니다.(개체 간 직접 communication 을 허용하지 않음). 모든 Request 에는 처리기(handler) 가 포함되어 있습니다. handler 은 MediatR 을 호출하는 서비스에서 보낸 Request 을 처리하는 역할을 합니다.

 

MediatR에 전송된 각 Request 에는 자체 handler 가 할당됩니다. 아래는 MediatR 사용의 개념을 설명하기 위해 만든 매우 간단한 다이어그램입니다.

 

 

 

Query 와 Command logic

 

실제 구현을 심층 분석하기 전에 애플리케이션에서 queries 와 commands 에 관해 이야기 할 때 우리 모두가 같은 생각을 하고 있는지 확인해 보고 싶습니다.

 

 

Queries

 

데이터베이스에 대해 query 를 수행할 때 작업할 수 있는 데이터 집합을 검색하려고 합니다. 표준 선택 쿼리는 다음과 같습니다.

 

SELECT Name, Age FROM Users

 

Commands

 

CQRS 패턴을 다루는 경우, 일반적인 commands 는 Insert, Update, Delete 입니다. 이러한 종류의 Logic 은 애플리케이션에서 다른 Layer 과 분리되어 있는 Business Level 에 배치됩니다.

이는 business operations 이므로 응용 프로그램에서 데이터를 반환하지 않습니다. 다음은 일반 SQL에서 위에서 언급한 commands 의 세 가지 예입니다.

 

 

Insert

 

항상 데이터베이스에 신규 데이터를 추가합니다. 이는 다음 명령으로 수행할 수 있습니다.

 

INSERT INTO Users (Id, Name, Age)
VALUES(3, 'Christian', 26)

 

Update

 

키 식별자를 사용하여 해당 키와 관련된 데이터를 업데이트합니다. 데이터베이스에서 업데이트하려는 경우 업데이트 명령은 다음과 같습니다.

 

UPDATE Users
SET Name = 'Kenya'
WHERE Id = 1

 

Delete

 

일부 데이터를 제거하려면 Delete 을 수행합니다. 데이터베이스에서 ID가 4인 사용자를 제거하고 싶다고 가정해 보겠습니다.

 

DELETE FROM Users
WHERE Id = 4

 

CQRS 및 MediatR 사용의 장단점

 

 

CQRS의 장점

 

첫번째, 데이터베이스 트랜잭션이 많은 애플리케이션을 실행하는 경우, 이러한 트랜잭션이 가능한 빨리 실행되는지 확인하려고 합니다. 애플리케이션에서 CQRS를 구현할 때, 각각의 queries 와 commands 은 그들 자신의 Handler 에 할당됩니다. 각각의 Query (get) 와 Command (insert, update, delete) 을 분리하므로, CQRS 는 각각 최적화(optimize) 하기 쉽기 때문에 편리합니다.

 

두번째, CQRS는 복잡한 연결과 Business operation 을 유발하는 Handler 를 분리할 수 있기 때문에 코드를 쉽게 작성할 수 있도록 도와 줍니다.

 

세번째, 새로운 개발자들을 애플리케이션에 도입한다면, commands 와 queries 가 더 작은 비트로 분리되어 있기 때문에 쉽게 참여할 수 있습니다.

 

네번째, 큰 애플리케이션이 있고 비즈니스 모델이 복잡한 경우 개발자에게 business logic 의 부분을 구분하도록 지정할 수 있습니다. 다시 설명드리면, 어떤 개발자는 queries 에서만 작업하고 다른 개발자 집합은 INSERT, UPDATE 또는 DELETE 같은 commands 에서만 작업한다는 것을 의미합니다.

 

 

CQRS의 단점

 

간단한 작은 애플리케이션을 실행하면 애플리케이션의 복잡도가 지나치게 높아질 것이라고 생각하기 때문에 오버킬 (overkill) 일 수도 있습니다. 또한 비즈니스 로직이 복잡하지 않은 경우에는 CQRS를 건너뛸 수 있습니다.(내 의견으로).

 

 

MediatR의 장점

 

첫번째, 애플리케이션을 유지 관리하는 것이 더 쉽고, Onboarding 에 필요한 시간과 리소스가 더 적기 때문에 새로운 개발자가 Onboard 하기가 더 쉬워질 것입니다.

더보기

Onboarding · 조직내 새로 합류한 사람이 빠르게 조직의 문화를 익히고 적응하도록 돕는 과정

 

두번째, MediatR 은 dependency injection 적용하지 않고, 바로 MediatR 클래스를 참조하는 것만 가능하며, 컨트롤러와 이들이 호출하는 서비스 간의 결합을 줄일 수 있습니다. 

 

세번째, 필요한 경우 많은 시간이 소요될 수 있는 많은 코드를 변경하지 않고도 프로젝트에 새로운 구성이나 다른 구성을 쉽게 적용할 수 있습니다.

 

 

 

 

ASP.NET Core 웹 API에서 MediatR을 사용하여 CQRS 구현

 

Visual Studio에서 ASP.NET Core 웹 API 템플릿을 기반으로 새 프로젝트를 만들어 시작합니다. 이 데모에서는 movies 에 중점을 둘 것입니다. 이 기사의 끝에서 데이터베이스에서 신규 movies 를 create 하거나 데이터베이스에서 신규 users 를 select 할 수 있습니다.

 

 

#1 – ASP.NET Core 웹 API 만들기

 

지침을 따르고 원하는 방식으로 응용 프로그램의 이름을 지정하세요. HTTPS 지원을 체크했으며 Docker 는 제외 했습니다.

 

 

#2 – API 초기 정리

 

API 는 템플릿을 사용하여 만들어지기 때문에 일부 초기 정리 작업을 수행해야 합니다. 가장 먼저 해야 할 일은 프로젝트에서 Weather Forecast 클래스와 컨트롤러를 삭제하는 것입니다.

 

 

사용할 models 과 enums 을 만드는 단계로 넘어갑시다. 참고로 완성된 소스는 [여기] 를 참고하세요.

 

#3 – Core 및 Domain Model 생성

 

데모 목적으로 애플리케이션의 일부를 별도의 프로젝트로 분리하는 깨끗한 아키텍처를 사용하여 이 애플리케이션을 만들지는 않겠습니다. 모든 것이 API 프로젝트 내에 있습니다.

 

먼저 몇 개의 폴더를 만들어야 합니다.

 

 

Enums 폴더 안에 MovieGenre.cs 라는 새 파일을 만들고 Movie 안에 Movie.cs라는 파일을 만듭니다.

 

movie 의 model /entity는 데모를 위한 매우 기본적인 것입니다. movie model 에 다음 properties 을 추가했습니다.

 

using MediatR_Demo.Core.Enums;

namespace MediatR_Demo.Domain.Entities.Movie
{
    public class Movie
    {
        public long Id { get; set; }
        public string? Title { get; set; }
        public string? Description { get; set; }
        public MovieGenre? Genre { get; set; }
        public int? Rating { get; set; }
    }
}

 

movie genres 를 포함하는  enum 은 다음과 같습니다.

 

namespace MediatR_Demo.Core.Enums
{
    public enum MovieGenre
    {
        Action,
        Comedy,
        Drama,
        Fantasy,
        Horror,
        Mystery,
        Romance,
        Thriller,
        Western
    }
}

 

여기에서 튜토리얼의 이 부분에 대해 변경한 사항을 볼 수 있습니다. 소스 변경된 부분 참고 => [여기]

 

 

#4 – EF Core를 사용하여 데이터베이스 생성 및 마이그레이션

 

model 이 준비되면 movies 에 대한 일부 데이터를 저장할 수 있는 부분으로 이동하겠습니다.

 

4.1 – 필수 패키지 설치

Entity Framework Core(Code First)를 사용하여 애플리케이션용 데이터베이스를 연결하고 생성하겠습니다. 데이터베이스는 내 로컬 개발 시스템에서 호스팅됩니다. NuGet 패키지 관리자를 사용하여 필요한 패키지를 설치해 보겠습니다.

 

SQL Server

 
https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer/
### Package Manager
Install-Package Microsoft.EntityFrameworkCore.SqlServer

### .NET CLI
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

### PackageReference
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="VERSION-HERE" />

 

Tools

 

https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Tools
### Package Manager
Install-Package Microsoft.EntityFrameworkCore.Tools

### .NET CLI
dotnet add package Microsoft.EntityFrameworkCore.Tools

### PackageReference
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="VERSION-HERE" />

 

EF Core

https://www.nuget.org/packages/Microsoft.EntityFrameworkCore
### Package Manager
Install-Package Microsoft.EntityFrameworkCore

### .NET CLI
dotnet add package Microsoft.EntityFrameworkCore

### PackageReference
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="VERSION-HERE" />

 

 

4.2 – 데이터베이스 연결 설정

 

패키지를 설치하고 사용할 준비가 되었으면 데이터베이스 연결 문자열을 구성하고 시작 시 새 데이터베이스 컨텍스트를 추가하도록 응용 프로그램에 지시해야 합니다. appsettings.json으로 이동하여 연결 문자열에 대한 새 섹션을 만듭니다. 

 

{
  "ConnectionStrings": {
    "Standard": "Server=localhost;Database=mediatr-demo;Trusted_Connection=True;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

 

이것을 startup class 에 등록하기 전에 응용 프로그램에 대한  database context class 를 만들어야 합니다.

 

4.3 – 애플리케이션 Db 컨텍스트 생성

 

Context 라는 하위 폴더가 있는 부분에서 Repository 라는 폴더를 만듭니다. Context 폴더 안에 ApplicationDbContex.cs라는 새 파일을 추가해야 합니다. ApplicationDbContext.cs는 데이터베이스에서 처리하기 위해 Movie Model 을 포함해야 합니다.

 

using MediatR_Demo.Domain.Entities.Movie;
using Microsoft.EntityFrameworkCore;

namespace MediatR_Demo.Repository.Context
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }

        public DbSet<Movie> Movies { get; set; }
    }
}

 

이제 다음과 같은 애플리케이션 구조가 있어야 합니다.

 

 

 

4.4 – Connection String 로 애플리케이션 Db Context 등록하기

 

이제 이전에 appsettings.json 파일에 추가한 connection string 을 사용하여 ApplicationDbContext 클래스를 등록해야 합니다. Program.cs로 이동하여 다음을 추가합니다.

 

using MediatR_Demo.Repository.Context;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// ### 이 부분 추가!!!
builder.Services.AddDbContext(
    options => options.UseSqlServer(builder.Configuration.GetConnectionString("Standard")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

 

 

4.5 – 데이터베이스 마이그레이션 및 업데이트하기
 

지금 해야 할 일은 새 마이그레이션을 만들고 데이터베이스를 업데이트하는 것뿐입니다. Package Manager Console 로 이동하여 다음 명령을 실행합니다.

 

-- 마이그래이션 작업
Add-Migration MovieAdded

-- 데이터베이스 업데이트
Update-Database

 

EF Core 가 아래와 같이 알아서 실행해서 처리 해 줍니다. 잠시 기다려 봅니다.
 

 

데이터베이스를 열고 테이블을 확장하면 이제 마이그레이션 기록이 있는 테이블과 Movie 를 보관하는 테이블을 볼 수 있습니다. 
 

 

 

#5 – MediatR 설치 및 구성

 

지금까지 우리는 movies 를 보관하는 데 필요한 model, movies  에 대한 다양한 장르의 enum movies  를 저장하기 위한 database context  를 만들었습니다. 계속해서 데이터베이스의 일부 movies 를 채우겠습니다.

 

데이터베이스에 새 movies  를 추가하려면 INSERT 명령이 될 것이기 때문에 Command 을 만들어야 합니다. 나중에 movies 를 다시 검색하기 위해 데이터베이스를 Query 하는 방법을 보여 드리겠습니다.

 

CQRS 디자인 패턴을 쉽게 구현할 수 있도록 MediatR 을 사용할 것입니다. 프로젝트에 MediatR 을 설치해 봅시다. 먼저 MediatR 을 설치한 다음 ASP.NET DI 와 MediatR 을 연결할 수 있는 패키지를 설치합니다.

 

MediatR

 

### Package Manager
Install-Package MediatR

### .NET CLI
dotnet add package MediatR

### PackageReference
<PackageReference Include="MediatR" Version="VERSION-HERE" />

 

DI Package

### Package Manager
Install-Package MediatR.Extensions.Microsoft.DependencyInjection

### .NET CLI
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

### PackageReference
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="VERSION-HERE" />

 

 

 

Program.cs 로 이동하여 startup routine 에 MediatR 을 추가합니다.
 
using MediatR;
using MediatR_Demo.Repository.Context;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext(
    options => options.UseSqlServer(builder.Configuration.GetConnectionString("Standard")));

// ## 추가된 부분 여기!!
builder.Services.AddMediatR(typeof(Program));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
 

MediatR 은 이제 우리를 위해 일할 준비가 되었습니다. 애플리케이션을 시작하기 전에 launchsettings.json 을 약간 변경해야 합니다. 데모를 위해 실행 URL을 다음으로 업데이트 하세요.

 

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:14989",
      "sslPort": 44321
    }
  },
  "profiles": {
    "MediatR_Demo": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "https://localhost:7001;http://localhost:7000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "swagger",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

 

위의 내용 중에 변경된 부분은 여기 입니다.

 

"applicationUrl": "https://localhost:7001;http://localhost:7000",

 

 

#6 – Request Response Abstraction(추상화) 를 위한 DTO 생성

 

Movie 모델은 Movie를 설명하기 위해 몇 개의 문자열과 int를 사용합니다. MovieMovie 테이블을 채우려면 이 데이터를 채워야 합니다. 이를 위해 데이터 전송 개체(DTO)를 생성합니다. Domain 폴더 안에 DTOs 라는 폴더를 만들었습니다.

 

데이터를 요청하고 보낼 것이므로 DTOs 폴더 안에 Movie 용 하위 폴더가 있는 두 개의 폴더(Requests, Responses)를 만듭니다. 각 Movie 폴더 안에 다음 이름을 가진 파일을 추가해야 합니다.

 

CreateMovieRequest.cs
CreateMovieDto.cs

 

 

6.1 – CreateMovieRequest.cs
using MediatR_Demo.Core.Enums;

namespace MediatR_Demo.Domain.DTOs.Requests.Movie
{
    public class CreateMovieRequest
    {
        public string? Title { get; set; }
        public string? Description { get; set; }
        public MovieGenre? Genre { get; set; }
        public int? Rating { get; set; }
    }
}

 

6.2 – CreateMovieDto.cs
namespace MediatR_Demo.Domain.DTOs.Responses.Movie
{
    public class CreateMovieDto
    {
        public CreateMovieDto(long id)
        {
            Id = id;
        }

        public long Id { get; set; }
    }
}

 

다음 섹션에서는 movies 를 데이터베이스에 insert 하는 command 을 생성합니다. 

 

 

 

 

#7 – Movie  Commands 만들기 + Movie  Controller

 

DTOs 를 실행하고 insert command, handler 와 extension 을 구현하여 CQRS 패턴이 작동하도록 하죠.

 

 

7.1 – Movie 을 만들기 위한 command 만들기

 

다음 구조로 새 폴더 집합을 만듭니다.
./Application/Movies/Commands/CreateMovie

 

CreateMovie 라는 폴더 안에, 다음 이름으로 세 개의 파일을 만들어야 합니다.

 

CreateMovieCommand.cs
CreateMovieCommandExtensions.cs
CreateMovieCommandHandler.cs

 

하위 섹션에서는 각 파일을 살펴보고 해당 파일에 추가해야 하는 코드를 보여줍니다.

 

 

7.2 – CreateMovieCommand

command  파일은 title, description, genre, rating 을 가져옵니다. 이 값은 생성된 Movie 의 ID를 보유하는 CreateMovieDto로 반환됩니다. 아래는 구현입니다.

 

using MediatR;
using MediatR_Demo.Core.Enums;
using MediatR_Demo.Domain.DTOs.Responses.Movie;

namespace MediatR_Demo.Application.Movies.Commands.CreateMovie
{
    public class CreateMovieCommand : IRequest<CreateMovieDto>
    {
        public CreateMovieCommand(
            string? title,
            string? description,
            MovieGenre? genre,
            int? rating)
        {
            Title = title;
            Description = description;
            Genre = genre;
            Rating = rating;
        }

        public string? Title { get; set; }
        public string? Description { get; set; }
        public MovieGenre? Genre { get; set; }
        public int? Rating { get; set; }
    }
}

 

 

7.3 – CreateMovieCommandExtensions

다음은 extensions(확장) 기능입니다. 현재는 movie 를 만드는 로직만 구현할 것입니다. CreateMovie라는 메서드 이름을 지정했습니다. 이 메서드는 CreateMovieCommand를 받아 Movie Model 을 반환합니다.

 

using MediatR_Demo.Domain.Entities.Movie;

namespace MediatR_Demo.Application.Movies.Commands.CreateMovie
{
    public static class CreateUserCommandExtension
    {
        public static Movie CreateMovie(this CreateMovieCommand command)
        {
            var movie = new Movie
                (
                    command.Title,
                    command.Description,
                    command.Genre,
                    command.Rating
                );

            return movie;
        }
    }
}

 

이제 Movie 에 생성자(constructor)가 없기 때문에  Movie 개체에서 오류가 발생합니다. 이 문제를 해결하려면 Movie Model 로 돌아가서 다음과 같이 수정해야 합니다.

 

using MediatR_Demo.Core.Enums;

namespace MediatR_Demo.Domain.Entities.Movie
{
    public class Movie
    {
        public Movie(string? title, string? description, MovieGenre? genre, int? rating)
        {
            Title = title;
            Description = description;
            Genre = genre;
            Rating = rating;
        }

        public long Id { get; set; }
        public string? Title { get; set; }
        public string? Description { get; set; }
        public MovieGenre? Genre { get; set; }
        public int? Rating { get; set; }
    }
}

 

7.4 – CreateMovieCommandHandler

 

이제 가장 중요한 것 중 하나인 Handler 가 데이터베이스에 데이터를 Insert 하는 작업을 수행합니다. 이 메서드의 이름은 Handle이며 CreateUserCommand(요청) 및 cancellation token 을 받습니다. 완료되면 생성된 사용자의 ID를 반환해야 합니다.

 

using MediatR;
using MediatR_Demo.Domain.DTOs.Responses.Movie;
using MediatR_Demo.Repository.Context;

namespace MediatR_Demo.Application.Movies.Commands.CreateMovie
{
    public class CreateUserCommandHandler : IRequestHandler<CreateMovieCommand, CreateMovieDto>
    {
        private readonly ApplicationDbContext _dbContext;

        public CreateUserCommandHandler(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<CreateMovieDto> Handle(CreateMovieCommand request, CancellationToken cancellationToken)
        {
            var movie = request.CreateMovie();
            await _dbContext.Movies.AddAsync(movie);
            await _dbContext.SaveChangesAsync();

            return new CreateMovieDto(movie.Id);
        }
    }
}

 

IRequestHandler 인터페이스에서 Handle  Method 를 상속하지 않았으며 Handler 가 호출되면 실행될 로직을 구현했습니다. IMediator 를 사용하여 request 을 보낼 controller 로 이동하겠습니다.

 

 

7.5 - Movie  컨트롤러에 CreateMovie endpoint  추가하기

 

가장 먼저 해야 할 일은 Controllers 폴더 안에 MovieController.cs 라는 컨트롤러를 만드는 것입니다.

 

dependency injection 및 POST endpoint 을 포함하는 컨트롤러는 다음과 같은 방식으로 구현됩니다.

 

using MediatR;
using MediatR_Demo.Application.Movies.Commands.CreateMovie;
using MediatR_Demo.Domain.DTOs.Requests.Movie;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace MediatR_Demo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class MovieController : ControllerBase
    {
        private readonly IMediator _mediator;

        public MovieController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpPost]
        public async Task<IActionResult> CreateMovie([FromBody] CreateMovieRequest request)
        {
            var movie = await _mediator.Send(new CreateMovieCommand(
                request.Title,
                request.Description,
                request.Genre,
                request.Rating));

            return Ok(movie);
        }
    }
}

 

#8 – Movie Query 만들기

 

자, 이제 데이터베이스에 영화를 저장할 수 있게 되었으니, 다른 방법으로도 영화를 검색하면 좋을 것 같습니다. 아래 코드에서는 영화 목록과 영화를 바탕으로 한 영화 한 편을 검색할 것입니다.

 

8.1 – Movie 검색을 위한 DTO 만들기
먼저 GetMovieDto.cs 라는 Response 폴더에 DTO 를 만들어야 합니다.

 

DTO 는 Movie 데이터를 표시하는 데 사용됩니다. Movie model 을 반영합니다.
 
using MediatR_Demo.Core.Enums;

namespace MediatR_Demo.Domain.DTOs.Responses.Movie
{
    public class GetMovieDto
    {
        public long Id { get; set; }
        public string? Title { get; set; }
        public string? Description { get; set; }
        public MovieGenre? Genre { get; set; }
        public int? Rating { get; set; }
    }
}

 

 

8.2 – Query Requests 만들기
 
MediatR 과 함께 CQRS 패턴을 활용하여 모든 movie 과 하나의 movie 을 가져오는 옵션을 알아볼까 합니다. 이제 GetMovies 및 GetMovie 라는 두 개의 하위 폴더가 있는 ./Application/Movies/Queries 에 폴더를 만들어야 합니다. 이 폴더 안에 다음 파일을 만들어야 합니다.
 
8.3 – Movie 가져오기
 
먼저 GetMovieDto 목록을 검색하기 위한 쿼리를 만듭니다.
using MediatR;
using MediatR_Demo.Domain.DTOs.Responses.Movie;

namespace MediatR_Demo.Application.Movies.Queries.GetMovies
{
    public class GetMoviesQuery : IRequest<IList<GetMovieDto>>
    {
    }
}

 

그런 다음 extension 클래스를 사용하여 Movie DTO 를 User 개체에 매핑합니다.
using MediatR_Demo.Domain.DTOs.Responses.Movie;
using MediatR_Demo.Domain.Entities.Movie;

namespace MediatR_Demo.Application.Movies.Queries.GetMovies
{
    public static class GetMoviesQueryExtensions
    {
        public static GetMovieDto MapTo(this Movie movie)
        {
            return new GetMovieDto
            {
                Id = movie.Id,
                Title = movie.Title,
                Description = movie.Description,
                Genre = movie.Genre,
                Rating = movie.Rating
            };
        }
    }
}

 

마지막으로, 모든 Movie 를 반환할 수 있는 목록으로 가져오기 위해 데이터베이스에 대한 작업을 수행하는 Handler 를 만들어야 합니다.
 
using MediatR;
using MediatR_Demo.Domain.DTOs.Responses.Movie;
using MediatR_Demo.Repository.Context;
using Microsoft.EntityFrameworkCore;

namespace MediatR_Demo.Application.Movies.Queries.GetMovies
{
    public class GetMoviesQueryHandler : IRequestHandler<GetMoviesQuery, IList<GetMovieDto>>
    {
        private readonly ApplicationDbContext _dbContext;

        public GetMoviesQueryHandler(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<IList<GetMovieDto>> Handle(GetMoviesQuery request, CancellationToken cancellationToken)
        {
            var movies = await _dbContext.Movies.ToListAsync();
            var movieList = new List<GetMovieDto>();
            foreach (var movieItem in movies)
            {
                var movie = movieItem.MapTo();
                movieList.Add(movie);
            }

            return movieList;
        }
    }
}
 

핸들러는 데이터베이스에 대해 async query 를 작성하고 모든 Movies 항목을 목록에 저장합니다. 목록의 각 Movie 항목에 대해 Movie 항목을 Movie dto에 매핑하고 해당 Movie dto 를 목록에 추가한 다음 반환합니다. 컨트롤러에서 데이터베이스의 모든 영화를 요청하기 위한 endpoint  를 추가합니다.

 

using MediatR;
using MediatR_Demo.Application.Movies.Commands.CreateMovie;
using MediatR_Demo.Application.Movies.Queries.GetMovies;
using MediatR_Demo.Domain.DTOs.Requests.Movie;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace MediatR_Demo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class MovieController : ControllerBase
    {
        private readonly IMediator _mediator;

        public MovieController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpGet]
        public async Task<IActionResult> GetMovies()
        {
            var movies = await _mediator.Send(new GetMoviesQuery());
            return Ok(movies);
        }

        [HttpPost]
        public async Task<IActionResult> CreateMovie([FromBody] CreateMovieRequest request)
        {
            var movie = await _mediator.Send(new CreateMovieCommand(
                request.Title,
                request.Description,
                request.Genre,
                request.Rating));

            return Ok(movie);
        }
    }
}

 

8.4 – ID로 Movie  가져오기

 

이것은 우리가 목록을 요구하는 것이 아니라 특정 영화를 요구하기 때문에 약간 다릅니다. 일반적인 경우에는 단일 영화가 요청되었을 때 더 많은 데이터를 포함합니다. 이 예에서는 데이터가 많지 않으므로 사용 가능한 모든 데이터를 반환합니다. 이미 파일을 만들었으므로 몇 가지 logic 를 추가해 보겠습니다.  GetMovieQuery.cs 내부에 다음 코드를 추가해야 합니다. 이 query 는 Handler 에서 검색하려는 movie 의 ID를 요청합니다.

 

using MediatR;
using MediatR_Demo.Domain.DTOs.Responses.Movie;

namespace MediatR_Demo.Application.Movies.Queries.GetMovie
{
    public class GetMovieQuery : IRequest<GetMovieDto>
    {
        public GetMovieQuery(long? id)
        {
            Id = id;
        }

        public long? Id { get; set; }
    }
}

 

extension 기능 내에서 목록 쿼리에서 이전에 수행한 것과 동일한 mapping 을 수행하고 있습니다.

 

using MediatR_Demo.Domain.DTOs.Responses.Movie;
using MediatR_Demo.Domain.Entities.Movie;

namespace MediatR_Demo.Application.Movies.Queries.GetMovie
{
    public static class GetMovieQueryExtensions
    {
        public static GetMovieDto MapTo(this Movie movie)
        {
            return new GetMovieDto
            {
                Id = movie.Id,
                Title = movie.Title,
                Description = movie.Description,
                Genre = movie.Genre,
                Rating = movie.Rating
            };
        }
    }
}

 

프로젝트 내용에 따라 더 많거나 적은 세부 정보를 포함하도록 업데이트할 수 있습니다.

 

Handler 내에서 ID를 사용하여 Movie 을 검색하는 간단한 Query 를 작성합니다. Microsoft 에서 데이터 쿼리에 대한 자세한 내용을 읽을 수 있습니다.

 

querying data here at Microsoft.

 

Querying Data - EF Core

Overview of information on querying in Entity Framework Core

learn.microsoft.com

 

 

using MediatR;
using MediatR_Demo.Domain.DTOs.Responses.Movie;
using MediatR_Demo.Repository.Context;
using Microsoft.EntityFrameworkCore;

namespace MediatR_Demo.Application.Movies.Queries.GetMovie
{
    public class GetMovieQueryHandler : IRequestHandler<GetMovieQuery, GetMovieDto>
    {
        private readonly ApplicationDbContext _dbContext;

        public GetMovieQueryHandler(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<GetMovieDto> Handle(GetMovieQuery request, CancellationToken cancellationToken)
        {
            var movie = await _dbContext.Movies.Where(x => x.Id == request.Id).FirstOrDefaultAsync();

            if (movie != null)
            {
                var movieItem = movie.MapTo();
                return movieItem;
            }
            return null;
        }
    }
}

 

이 경우 요청 서비스에서 보낸 특정 ID를 가진 Movie 가 없으면 null을 반환하는 핸들러를 구현했습니다. 보시다시피 EF Core를 사용하여 id가 GetMovieQuery request-id 의 id와 동일한 Movie 을 가져옵니다. 컨트롤러 내부에 특정 라우팅이 있는 endpoint 을 추가합니다.

 

using MediatR;
using MediatR_Demo.Application.Movies.Commands.CreateMovie;
using MediatR_Demo.Application.Movies.Queries.GetMovie;
using MediatR_Demo.Application.Movies.Queries.GetMovies;
using MediatR_Demo.Domain.DTOs.Requests.Movie;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace MediatR_Demo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class MovieController : ControllerBase
    {
        private readonly IMediator _mediator;

        public MovieController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpGet]
        public async Task<IActionResult> GetMovies()
        {
            var movies = await _mediator.Send(new GetMoviesQuery());

            if (movies != null)
            {
                return Ok(movies);
            }

            return NotFound("No movies in database. Please add a movie first.");
        }

        [HttpGet("/getmovies/{id}")]
        public async Task<IActionResult> GetMovie(long id)
        {
            var movie = await _mediator.Send(new GetMovieQuery(id));

            if (movie != null)
            {
                return Ok(movie);
            }

            return NotFound($"No movie in database with ID: {id}.");

        }

        [HttpPost]
        public async Task<IActionResult> CreateMovie([FromBody] CreateMovieRequest request)
        {
            var movie = await _mediator.Send(new CreateMovieCommand(
                request.Title,
                request.Description,
                request.Genre,
                request.Rating));

            return Ok(movie);
        }
    }
}

 

Moives 가 발견되지 않으면 status message 를 반환하고 그렇지 않으면 Moives 를 dto 객체로 반환합니다. 모든 Moives 를 검색하는 endpoint 에 동일한 error check 를 추가했습니다.

 

That’s it – 이제 우리는 응용 프로그램을 시작하고 일부 Moives 를 추가할 준비가 되었습니다. 

 

 

 

#9 – 영화 API 테스트

 

우리 모두가 기다려온 순간. 이제 API를 실행하고 첫 번째 동영상을 추가해 보겠습니다. 톰크루즈 주연의 탑건2를 추가했습니다. 아래 예를 참조하십시오.

 

9.1 – 데이터베이스에 영화 추가

 
데이터베이스 내부에는 제대로 추가된것을 확인할 수 있습니다.
 

 

Movie 를 몇 개 더 추가해 보겠습니다. 다음은 일부 데이터를 확인하기 위해 API에 대해 POST하는 데 사용할 수 있는 몇 가지 샘플 동영상입니다.

 

{
  "title": "Doctor Strange in the Multiverse of Madness",
  "description": "Doctor Strange teams up with a mysterious teenage girl from his dreams who can travel across multiverses, to battle multiple threats, including other-universe versions of himself, which thre...",
  "genre": 3,
  "rating": 7
}

 

 

9.2 – 모든 Movie 가져오기
 
 
완벽하게 작동합니다. 실제 업무적으로 응용 프로그램에서는 페이징을 구현하지만 여기에서는 언급하지 않겠습니다.
 

 

9.3 – ID로 Movie  가져오기

 

우리가 볼 수 있듯이 아주 잘 진행되었습니다. 모든 방법이 작동하고 데이터가 예상대로 추가되고 검색됩니다.

 

 

요약하기

 

CQRS 디자인 패턴을 사용하면 애플리케이션에서 개체 간의 종속성 수를 줄일 수 있습니다. MediatR을 추가함으로써 직접 communication 을 피하는 message 처리를 도입할 수 있습니다.

 

이 기사가 복잡한 business models 또는 복잡성 측면에서 "가벼운" 것이 아닌, 응용 프로그램으로 작업할 때 CQRS 디자인 패턴이 좋은 이유를 이해하는 데 도움이 되었기를 바랍니다.

 

 

위의 언급된 내용을 github 에 오픈 공개한 사이트 입니다.

 

CQRS and MediatR demo.

 

GitHub - Christian-Schou/CQRS-and-MediatR-demo: A demo on how to implement CQRS in an ASP.NET Core Web API using MediatR

A demo on how to implement CQRS in an ASP.NET Core Web API using MediatR - GitHub - Christian-Schou/CQRS-and-MediatR-demo: A demo on how to implement CQRS in an ASP.NET Core Web API using MediatR

github.com

 

 

출처

 

https://blog.christian-schou.dk/how-to-implement-cqrs-with-mediatr-in-asp-net/

 

How to implement CQRS with MediatR in ASP.NET Core

Learn how to implement CQRS design pattern in an ASP.NET Core Web API easily using MediatR to separate command and query logic.

blog.christian-schou.dk