MediatR 추상 인터페이스
MediatR의 IRequest와 IRequestHandler를 래핑한 도메인 의도를 명확히 하는 인터페이스입니다.
📚 제공 인터페이스
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 |
🎓 참고 자료