MediatR vs Wolverine

2024. 2. 26. 00:00ASPNET

반응형

dotnet core 에는 여러가지 message library 들이 있다. 

자체 event 및 delegate 를 통한 처리 방식도 있고 command pattern 을 이용한 구현 방식도 있다. 

 

3rd party library 를 사용할 경우 MediatR 을 많이 사용했을 것이다. 

오늘은 이 MediatR 대신 사용가능한 Wolverine 에 대해 알아보겠다. 

 

우선 MediatR 로 만들어는 경우는 핵심 코드가 다음과 비슷 할 것이다. 

 

MediatR 을 사용할 경우

dotnet add package MediatR

Program.cs

builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));

 

 

Controller.cs

 [HttpGet]
 public async Task<IActionResult> Get([FromServices] IMediator mediator)
 {
     var response = await mediator.Send(new Weather.Get.Command());
    
     return Ok(response);
 }

Handler.cs

public class Get
{
    public class Command : IRequest<Response>
    {
    }

    public class Response : WeatherForecast
    {
        public List<WeatherForecast> Forecasts { get; set; }

    }

    public class WeatherForecast
    {
        public DateOnly Date { get; set; }

        public int TemperatureC { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

        public string? Summary { get; set; }
    }

    public class CommandHandler : IRequestHandler<Command, Response>
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };
        public Task<Response> Handle(Command request, CancellationToken cancellationToken)
        {
            var forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            }).ToList();

            return Task.FromResult(new Response { Forecasts = forecasts });
        }
    }
}

 

 

return type 이 있는 Command 를 선언 하고 (IRequest<Response> 를 Implementation)

public class Command : IRequest<Response>

 

Handler 를 선언한 후  (IRequestHandler<Command, Response> 를 Implementation)

public class CommandHandler : IRequestHandler<Command, Response>
...

 

Handle Method 를 Implementation 하자

public Task<Response> Handle(Command request, CancellationToken cancellationToken)

 

위와 같이 하면 MediatR 을 이용하여 Controller 에서 Handler 쪽으로 Message 가 전달 된다. 

 

 

Wolverine 을 사용 할 경우

dotnet add package WolverineFx

Program.cs

builder.Host.UseWolverine()

 

자이제 여기서 차이점이 있는데 wolverine 같은 경우는 command 를 특정한 interface 를 이용하여 구현하지 않아도 된다. 

 public class Command {} // 이게 다다.. 빈 class 하나 선언하면 된다. 
 public record Command {} // record 도 가능하다

 

return type 도 필요 없고 IRequest 같은 interface 도 필요 없다. (의존성 없음)

Handler 도 마찬가지다 아래를 확인 하자

Handler.cs 

 public static Task<Response> Handle(Command request)

 

그런데 주의 할것은 있다. Handle 의 이름을 다른 형태로 변경하면 오류가 날 것이다. 

그 이유는 source generator 같은 것으로 동적으로 처리 코드를 compile 시점에 생성하기 때문이다. 

그래서 아래와 같은 코드가 generated 된 code 에 존재하게 된다. 

// The actual message execution
var outgoing1 = await Wolverine.Features.Weather.GetWithWolverine.CommandHandler.Handle(command).ConfigureAwait(false);

 

이와 같이 사용하면 wolverine 을 통해 Message 가 전달될 것이다. 

 

그런데 이제 문제가 하나 생긴다. 

wolverine 으로 실행한 프로젝트인 경우 처음 실행 시에

2~3 초의 message (cold stat) 전달 시간이 생긴다. 

그 이유는  위에 handler 들을 찾아서 source 를 동적으로 생성하기 때문이다. 

물론 첫 실행 이후는 아주 빠르게 동작하지만 .. 우리가 원하는 동작은 아니다. 

이런경우를 예방하기 위에 다음과 같은 option 을 wolverine 에 주자

 

 

 

 

Program.cs

builder.Host.UseWolverine(opts=>
{
    opts.ApplicationAssembly = Assembly.GetExecutingAssembly();
    opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Auto;
});

 

자 이제 실행해 보자 . 그런데 실행해 보면 처음 실행이 느린게 똑같은걸 알 수 있다. 

화내지 말고 침작 하자.

solution 을 확인해보자 다음과 같은 folder 와 code 들이 추가되어 있을 것이다. 

Auto generated code

 

이제 project 를 다운 시켰다가 다시 실행해보자

처음 실행에도 무척 빠르게 실행되는것을 확인 할 수 있다. 

 

이상태에서 TypeLoadMode.Static 으로 변경해보자

이제 처음 실행에도 아주 빠르게 실행되는 것을 확인 할 수 있다. 

 

자 그런데 이런경우 문제가 또 있다. 

cold start 를 방지하기 위해 모든 handler 들을 다 실행해 봐야 한다. 

그리고 수정이라도 하게 되면 다시 그 과정을 거쳐댜 한다. 

 

너무 불편하다. 

그래서 사실 wolverine 은 미리 코드를 생성하도록 지원하고 있다. 

 

코드를 다음과 같이 수정하자

 

Program.cs 

//app.Run();
await app.RunOaktonCommands(args);

 

이제 project folder 로 이동해서 command line 을 이용해서 다음을 입력하자

dotnet run -- codegen write

이렇게 하게 되면 모든 handler 가 자동으로 생성되게 된다. 

Handler 가 자동으로 생성되는 것을 알수 있다.

 

여기에 FluenentValidtion 이라는 validation check library 와도 연계된다. 

dotnet add package WolverineFx.FluentValidation

다음과 같이 설정을 추가 하자

builder.Host.UseWolverine(opts =>
{
	...
    opts.UseFluentValidation();
    opts.Services.AddSingleton(typeof(IFailureAction<>), typeof(CustomFailureAction<>));
});
public class CustomFailureAction<T> : IFailureAction<T>
{
    public void Throw(T message, IReadOnlyList<ValidationFailure> failures)
    {
        throw new Exception("Validation failed!: " + failures.Select(x => x.ErrorMessage).Join(", "));
    }
}

 

Handler 에 다음 코드도 추가한다. 

public class Validator : AbstractValidator<Command>
{
    public Validator()
    {
        RuleFor(c => c.EmailAddress).NotNull().NotEmpty();
    }
}

이제 실행시 EmailAddress 가 null 이거나 empty 이면 error 가 발생한다. 

 

MediatR 을 오랫동안 사용해본 입장에서 조금더 편리해진 library 라는 생각이 든다. 

꽤 오래전에 만들어진 library 이기도 하여 MediatR 을 변경해봐야 하는 시점이 된것 같다. 

그동안 정말 편리하게 사용했던 library 이다. 

 

그동안 고마웠다. ㅠㅠ

fare well MediatR

 

 

 

관련영상

https://youtu.be/B0bTMmbRFAQ

 

 

 

 

반응형

'ASPNET' 카테고리의 다른 글