KIM-JINO5 b47af4b08a first commit 2 miesięcy temu
..
Email b47af4b08a first commit 2 miesięcy temu
ICommand.cs b47af4b08a first commit 2 miesięcy temu
ICommandHandler.cs b47af4b08a first commit 2 miesięcy temu
ICommandHandlerWithResponse.cs b47af4b08a first commit 2 miesięcy temu
ICommandWithResponse.cs b47af4b08a first commit 2 miesięcy temu
IQuery.cs b47af4b08a first commit 2 miesięcy temu
IQueryHandler.cs b47af4b08a first commit 2 miesięcy temu
README.md b47af4b08a first commit 2 miesięcy temu

README.md

MediatR 추상 인터페이스

MediatR의 IRequestIRequestHandler를 래핑한 도메인 의도를 명확히 하는 인터페이스입니다.

📚 제공 인터페이스

Command (쓰기 작업)

ICommand                                    // 반환값 없음
ICommand<TResponse>                         // 반환값 있음
ICommandHandler<TCommand>                   // 반환값 없는 Handler
ICommandHandler<TCommand, TResponse>        // 반환값 있는 Handler

Query (읽기 작업)

IQuery<TResponse>                           // 항상 반환값 있음
IQueryHandler<TQuery, TResponse>            // Query Handler

🎯 사용 방법

1. 반환값이 없는 Command

// Command
public sealed record DeleteMemberCommand(int MemberID) : ICommand;

// Handler
public sealed class Handler(IAppDbContext db)
    : ICommandHandler<DeleteMemberCommand>
{
    public async Task Handle(DeleteMemberCommand request, CancellationToken ct)
    {
        await db.Member
            .Where(x => x.ID == request.MemberID)
            .ExecuteDeleteAsync(ct);
    }
}

2. 반환값이 있는 Command

// Command
public sealed record CreateMemberCommand(
    string Email,
    string Name
) : ICommand<int>; // 생성된 ID 반환

// Handler
public sealed class Handler(IAppDbContext db)
    : ICommandHandler<CreateMemberCommand, int>
{
    public async Task<int> Handle(CreateMemberCommand request, CancellationToken ct)
    {
        var member = Member.Create(request.Email);
        await db.Member.AddAsync(member, ct);
        await db.SaveChangesAsync(ct);

        return member.ID;
    }
}

3. Query

// Query
public sealed record GetMemberQuery(int MemberID) : IQuery<MemberResponse>;

// Handler
public sealed class Handler(IAppDbContext db)
    : IQueryHandler<GetMemberQuery, MemberResponse>
{
    public async Task<MemberResponse> Handle(GetMemberQuery request, CancellationToken ct)
    {
        var member = await db.Member
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.ID == request.MemberID, ct);

        if (member is null)
            throw new KeyNotFoundException();

        return new MemberResponse { /* ... */ };
    }
}

✅ 장점

1. 의도 명확화

  • ICommand = 데이터 변경 작업
  • IQuery = 데이터 조회 작업
  • 코드를 읽는 사람이 즉시 이해 가능

2. CQRS 원칙 강제

  • Command와 Query가 명확히 구분됨
  • Query는 반드시 반환값 필요

3. Pipeline Behavior 구분 가능

// Query에만 캐싱 적용
public class QueryCachingBehavior<TQuery, TResponse>
    : IPipelineBehavior<TQuery, TResponse>
    where TQuery : IQuery<TResponse> // 👈 타입 제약
{
    // ...
}

// Command에만 트랜잭션 적용
if (request is ICommand or ICommand<TResponse>)
{
    // 트랜잭션 시작
}

4. 타입 안정성

  • 컴파일 타임에 타입 체크
  • where TCommand : ICommand 제약으로 안전성 보장

🔄 마이그레이션 가이드

기존 코드

// ❌ Before
public sealed record Command(...) : IRequest;
public sealed class Handler : IRequestHandler<Command> { }

새 코드

// ✅ After
public sealed record Command(...) : ICommand;
public sealed class Handler : ICommandHandler<Command> { }

호환성

  • 기존 IRequest 코드도 계속 동작함
  • 새로운 Feature부터 ICommand/IQuery 사용
  • 점진적 마이그레이션 가능

📋 적용 규칙

작업 유형 사용 인터페이스 예시
생성 (Create) ICommand<TResponse> CreateMemberCommand
수정 (Update) ICommand UpdateMemberCommand
삭제 (Delete) ICommand DeleteMemberCommand
조회 단건 (Get) IQuery<TResponse> GetMemberQuery
조회 목록 (Search) IQuery<TResponse> SearchMembersQuery

🎓 참고 자료