본문으로 건너뛰기

유스케이스 CQRS 사양

이 사양서는 Functorium의 CQRS 요청 인터페이스, FinResponse<A> 판별 공용체, FinT LINQ 확장, 캐싱 및 영속성 계약의 공개 API를 정의합니다.

타입네임스페이스설명
ICommandRequest<TSuccess>Functorium.Applications.UsecasesCommand 요청 마커 인터페이스
ICommandUsecase<TCommand, TSuccess>Functorium.Applications.UsecasesCommand Handler 인터페이스
IQueryRequest<TSuccess>Functorium.Applications.UsecasesQuery 요청 마커 인터페이스
IQueryUsecase<TQuery, TSuccess>Functorium.Applications.UsecasesQuery Handler 인터페이스
FinResponse<A>Functorium.Applications.UsecasesSucc/Fail 판별 공용체 (Match, Map, Bind, LINQ 지원)
FinResponseFunctorium.Applications.Usecases정적 팩토리 클래스 (Succ, Fail)
IFinResponseFunctorium.Applications.Usecases비제네릭 기본 인터페이스 (IsSucc/IsFail)
IFinResponse<out A>Functorium.Applications.Usecases공변성 지원 제네릭 인터페이스
IFinResponseFactory<TSelf>Functorium.Applications.UsecasesCRTP 기반 Fail 생성 인터페이스
IFinResponseWithErrorFunctorium.Applications.UsecasesError 접근 인터페이스 (Pipeline용)
FinToFinResponseFunctorium.Applications.UsecasesFin<A>FinResponse<A> 변환 확장 메서드
FinTLinqExtensionsFunctorium.Applications.LinqFinT 모나드 트랜스포머 LINQ 확장 메서드
ICacheableFunctorium.Applications.Usecases캐싱 계약 인터페이스
IUnitOfWorkFunctorium.Applications.Persistence영속성 트랜잭션 계약
IUnitOfWorkTransactionFunctorium.Applications.Persistence명시적 트랜잭션 스코프
CtxIgnoreAttributeFunctorium.Applications.UsecasesCtxEnricher 자동 생성 제외 속성
  • FinResponse<A> IsSucc/IsFail을 사용합니다 (IsSuccess가 아님)
  • 성공 값 접근은 ThrowIfFail() 또는 Match()를 통해서만 가능합니다
  • 정적 팩토리는 FinResponse.Succ(value)FinResponse.Fail<T>(error)입니다 (FinResponse<T>.Succ()가 아님)
  • Command/Query 모두 FinResponse<TSuccess>를 반환 타입으로 사용합니다

요약에서 주요 타입과 핵심 규칙을 확인했습니다. 다음으로 각 타입의 상세 API를 살펴봅니다.


Functorium의 CQRS 인터페이스는 Mediator 라이브러리의 ICommand<T>/IQuery<T>를 기반으로, 반환 타입을 FinResponse<TSuccess>로 고정하여 Result 패턴을 강제합니다.

// 요청 인터페이스 — record로 구현
public interface ICommandRequest<TSuccess> : ICommand<FinResponse<TSuccess>>
{
}
// Handler 인터페이스 — Usecase 클래스로 구현
public interface ICommandUsecase<in TCommand, TSuccess>
: ICommandHandler<TCommand, FinResponse<TSuccess>>
where TCommand : ICommandRequest<TSuccess>
{
}
타입 파라미터제약설명
TSuccess없음성공 시 반환할 데이터 타입
TCommandICommandRequest<TSuccess>Command 요청 타입 (반공변)
// 요청 인터페이스 — record로 구현
public interface IQueryRequest<TSuccess> : IQuery<FinResponse<TSuccess>>
{
}
// Handler 인터페이스 — Usecase 클래스로 구현
public interface IQueryUsecase<in TQuery, TSuccess>
: IQueryHandler<TQuery, FinResponse<TSuccess>>
where TQuery : IQueryRequest<TSuccess>
{
}
타입 파라미터제약설명
TSuccess없음성공 시 반환할 데이터 타입
TQueryIQueryRequest<TSuccess>Query 요청 타입 (반공변)
Mediator.ICommand<FinResponse<TSuccess>>
└─ ICommandRequest<TSuccess>
Mediator.ICommandHandler<TCommand, FinResponse<TSuccess>>
└─ ICommandUsecase<TCommand, TSuccess>
Mediator.IQuery<FinResponse<TSuccess>>
└─ IQueryRequest<TSuccess>
Mediator.IQueryHandler<TQuery, FinResponse<TSuccess>>
└─ IQueryUsecase<TQuery, TSuccess>

FinResponse<A>는 성공(Succ)과 실패(Fail)를 표현하는 판별 공용체(discriminated union)입니다. Match, Map, Bind, LINQ 쿼리 표현식을 지원합니다.

public abstract record FinResponse<A> : IFinResponse<A>, IFinResponseFactory<FinResponse<A>>
{
public sealed record Succ(A Value) : FinResponse<A>;
public sealed record Fail(Error Error) : FinResponse<A>, IFinResponseWithError;
}
타입프로퍼티IsSuccIsFail설명
FinResponse<A>.SuccA Valuetruefalse성공 케이스
FinResponse<A>.FailError Errorfalsetrue실패 케이스
프로퍼티타입설명
IsSuccbool성공 상태 여부
IsFailbool실패 상태 여부
메서드시그니처설명
Match<B>B Match<B>(Func<A, B> Succ, Func<Error, B> Fail)상태에 따라 함수 호출 (값 반환)
Matchvoid Match(Action<A> Succ, Action<Error> Fail)상태에 따라 액션 호출
Map<B>FinResponse<B> Map<B>(Func<A, B> f)성공 값 변환
MapFailFinResponse<A> MapFail(Func<Error, Error> f)실패 값 변환
BiMap<B>FinResponse<B> BiMap<B>(Func<A, B> Succ, Func<Error, Error> Fail)성공/실패 동시 변환
Bind<B>FinResponse<B> Bind<B>(Func<A, FinResponse<B>> f)모나딕 바인드
BiBind<B>FinResponse<B> BiBind<B>(Func<A, FinResponse<B>> Succ, Func<Error, FinResponse<B>> Fail)성공/실패 동시 바인드
BindFailFinResponse<A> BindFail(Func<Error, FinResponse<A>> Fail)실패 상태 바인드
IfFailA IfFail(Func<Error, A> Fail)실패 시 대체 값 함수
IfFailA IfFail(A alternative)실패 시 대체 값
IfFailvoid IfFail(Action<Error> Fail)실패 시 액션 실행
IfSuccvoid IfSucc(Action<A> Succ)성공 시 액션 실행
ThrowIfFailA ThrowIfFail()실패 시 예외 발생, 성공 시 값 반환
Select<B>FinResponse<B> Select<B>(Func<A, B> f)LINQ select 지원 (Map과 동일)
SelectMany<B,C>FinResponse<C> SelectMany<B, C>(Func<A, FinResponse<B>> bind, Func<A, B, C> project)LINQ from ... from ... select 지원

정적 팩토리 메서드 (FinResponse 클래스)

섹션 제목: “정적 팩토리 메서드 (FinResponse 클래스)”
public static class FinResponse
{
public static FinResponse<A> Succ<A>(A value);
public static FinResponse<A> Succ<A>() where A : new();
public static FinResponse<A> Fail<A>(Error error);
}
메서드설명
Succ<A>(A value)성공 값으로 FinResponse 생성
Succ<A>()new A()로 기본 성공 FinResponse 생성 (A : new() 제약)
Fail<A>(Error error)실패 FinResponse 생성
// FinResponse<A>에 구현된 정적 팩토리 (Pipeline 내부 사용)
public static FinResponse<A> CreateFail(Error error);

Pipeline에서 TResponse.CreateFail(error)를 호출할 때 사용됩니다. static abstract 메서드이므로 구체 타입에서만 호출 가능합니다.

연산자설명
implicit operator FinResponse<A>(A value)값 → Succ 자동 변환
implicit operator FinResponse<A>(Error error)ErrorFail 자동 변환
operator trueIsSucc이면 true
operator falseIsFail이면 true
operator |Choice 연산자: 좌항이 Succ이면 좌항, 아니면 우항 반환

Pipeline과 관측성에서 FinResponse<A>를 타입 안전하게 다루기 위한 인터페이스 계층입니다.

IFinResponse 비제네릭 (IsSucc/IsFail 접근)
└─ IFinResponse<out A> 공변성 지원 제네릭
IFinResponseFactory<TSelf> CRTP 기반 Fail 생성 (Pipeline용)
IFinResponseWithError Error 접근 (Logger/Trace Pipeline용)
FinResponse<A>
├─ implements IFinResponse<A>
├─ implements IFinResponseFactory<FinResponse<A>>
└─ Fail : implements IFinResponseWithError
public interface IFinResponse
{
bool IsSucc { get; }
bool IsFail { get; }
}

Pipeline에서 제네릭 타입 없이 IsSucc/IsFail 속성에 접근하기 위한 비제네릭 인터페이스입니다.

public interface IFinResponse<out A> : IFinResponse
{
}

공변성(out)을 지원하여 파이프라인에서 읽기 전용으로 사용됩니다.

public interface IFinResponseFactory<TSelf>
where TSelf : IFinResponseFactory<TSelf>
{
static abstract TSelf CreateFail(Error error);
}

CRTP(Curiously Recurring Template Pattern)를 사용하여 타입 안전한 Fail 생성을 지원합니다. Pipeline의 UsecaseValidationPipelineUsecaseExceptionPipeline에서 TResponse.CreateFail(error)를 호출합니다.

public interface IFinResponseWithError
{
Error Error { get; }
}

실패 시 Error 정보에 접근하기 위한 인터페이스입니다. Logger Pipeline과 Trace Pipeline에서 사용됩니다. FinResponse<A>.Fail만 이 인터페이스를 구현합니다.


FinToFinResponse 확장 메서드 클래스는 Repository(Fin<A>) → Usecase(FinResponse<A>) 계층 간 변환에 사용됩니다.

메서드시그니처설명
ToFinResponse<A>Fin<A> → FinResponse<A>동일 타입 변환
ToFinResponse<A,B> (mapper)Fin<A> → Func<A, B> → FinResponse<B>성공 값 매핑 변환
ToFinResponse<A,B> (factory)Fin<A> → Func<B> → FinResponse<B>성공 시 factory 호출 (원본 값 무시)
ToFinResponse<A,B> (onSucc/onFail)Fin<A> → Func<A, FinResponse<B>> → Func<Error, FinResponse<B>> → FinResponse<B>성공/실패 모두 커스텀 처리
// 기본 변환
FinResponse<Product> response = fin.ToFinResponse();
// 매핑 변환
FinResponse<ProductDto> response = fin.ToFinResponse(product => new ProductDto(product));
// Factory 변환 (Delete 등 원본 값이 필요 없는 경우)
Fin<Unit> result = await repository.DeleteAsync(id);
return result.ToFinResponse(() => new DeleteResponse(id));

FinTLinqExtensionsFin<A>, IO<A>, Validation<Error, A> 타입을 FinT<M, A> 모나드 트랜스포머로 통합하여 LINQ 쿼리 표현식을 지원하는 partial 클래스입니다.

파일Source 타입Selector 반환 타입결과 타입설명
.Fin.csFin<A>FinT<M, B>FinT<M, C>Fin → FinT 승격 후 체이닝
.Fin.csFinT<M, A>Fin<B>FinT<M, C>FinT 체인 중간에 Fin 사용
.IO.csIO<A>B (Map)FinT<IO, B>IO → FinT 단순 변환
.IO.csIO<A>FinT<IO, B>FinT<IO, C>IO → FinT 승격 후 체이닝
.IO.csFinT<IO, A>IO<B>FinT<IO, C>FinT 체인 중간에 IO 사용
.Validation.csValidation<Error, A>FinT<M, B>FinT<M, C>Validation → FinT (제네릭)
.Validation.csValidation<Error, A>B (Map)FinT<M, B>Validation → FinT 단순 변환
.Validation.csValidation<Error, A>FinT<IO, B>FinT<IO, C>Validation → FinT (IO 특화)
.Validation.csFinT<M, A>Validation<Error, B>FinT<M, C>FinT 체인 중간에 Validation 사용
.Validation.csFinT<IO, A>Validation<Error, B>FinT<IO, C>FinT 체인 중간에 Validation (IO)
파일대상 타입반환 타입설명
.Fin.csFin<A>Fin<A>조건 불만족 시 Fail 반환
.FinT.csFinT<M, A>FinT<M, A>조건 불만족 시 Fail 반환
// Fin Filter
Fin<int> result = FinTest(25).Filter(x => x > 20);
// FinT Filter
FinT<IO, int> result = FinT<IO, int>.Succ(42).Filter(x => x > 20);
public static FinT<M, Seq<B>> TraverseSerial<M, A, B>(
this Seq<A> seq,
Func<A, FinT<M, B>> f)
where M : Monad<M>;

Seq<A>를 순차적으로 순회하며 각 요소를 FinT<M, B>로 변환합니다. fold를 사용하여 각 작업이 완전히 끝난 후 다음 작업을 시작하도록 보장합니다.

파라미터타입설명
seqSeq<A>처리할 시퀀스
fFunc<A, FinT<M, B>>각 요소를 FinT로 변환하는 함수

사용 시나리오: DbContext 등 동시성을 지원하지 않는 리소스를 순차적으로 안전하게 사용해야 하는 경우에 적합합니다. 각 항목이 독립적이고 리소스 공유가 없다면 Traverse를 사용하십시오.

// LINQ 쿼리 표현식에서 사용
FinT<IO, Response> response =
from infos in GetFtpInfos()
from results in infos.TraverseSerial(info => Process(info))
select new Response(results);
방향IO.csValidation.cs
Source → FinT (Map)IO 전용제네릭 M
Source → FinT<M, B>IO 전용제네릭 M + IO
FinT<M, A> → SourceIO 전용제네릭 M + IO

IO는 그 자체가 특정 모나드이므로 제네릭 M 버전이 불필요합니다. Validation은 모나드가 아닌 데이터 타입이므로 어떤 모나드 M과도 조합 가능하여 제네릭 M 버전과 IO 특화 버전 모두 제공합니다.

파일내용
FinTLinqExtensions.cs클래스 정의 및 문서
FinTLinqExtensions.Fin.csFin<A> 확장 (SelectMany, Filter)
FinTLinqExtensions.IO.csIO<A> 확장 (SelectMany)
FinTLinqExtensions.Validation.csValidation<Error, A> 확장 (SelectMany)
FinTLinqExtensions.FinT.csFinT<M, A> 확장 (Filter, TraverseSerial)

Query 요청에 캐싱을 적용하기 위한 인터페이스입니다. IQueryRequest<TSuccess>를 구현하는 record가 ICacheable도 함께 구현하면 Pipeline이 자동으로 캐싱을 처리합니다.

public interface ICacheable
{
string CacheKey { get; }
TimeSpan? Duration { get; }
}
프로퍼티타입설명
CacheKeystring캐시 항목의 고유 키
DurationTimeSpan?캐시 유효 기간 (null이면 기본 정책 적용)
// 사용 예: Query 요청에 캐싱 적용
public sealed record GetProductByIdQuery(ProductId Id)
: IQueryRequest<ProductDto>, ICacheable
{
public string CacheKey => $"product:{Id}";
public TimeSpan? Duration => TimeSpan.FromMinutes(5);
}

Command Usecase에서 변경 사항을 영속화하기 위한 계약입니다. UsecaseTransactionPipeline이 Handler 실행 후 자동으로 SaveChanges를 호출합니다.

public interface IUnitOfWork : IObservablePort
{
FinT<IO, Unit> SaveChanges(CancellationToken cancellationToken = default);
Task<IUnitOfWorkTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
}
메서드반환 타입설명
SaveChangesFinT<IO, Unit>변경 사항을 영속화합니다
BeginTransactionAsyncTask<IUnitOfWorkTransaction>명시적 트랜잭션을 시작합니다

IUnitOfWorkIObservablePort를 상속합니다. IObservablePortstring RequestCategory { get; } 프로퍼티를 정의하여, 관측성 Pipeline에서 레이어와 카테고리를 자동으로 식별합니다.

public interface IUnitOfWorkTransaction : IAsyncDisposable
{
Task CommitAsync(CancellationToken cancellationToken = default);
}
메서드반환 타입설명
CommitAsyncTask트랜잭션을 커밋합니다
DisposeAsyncValueTask미커밋 트랜잭션은 자동 롤백됩니다 (IAsyncDisposable)

명시적 트랜잭션은 ExecuteDeleteAsync/ExecuteUpdateAsync 등 즉시 실행 SQL과 SaveChanges를 동일 트랜잭션으로 묶어야 할 때 사용합니다.

// 명시적 트랜잭션 사용 예
await using var tx = await unitOfWork.BeginTransactionAsync(ct);
// ... ExecuteDeleteAsync, SaveChanges 등
await tx.CommitAsync(ct);
// Dispose 시 미커밋이면 자동 롤백

[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class CtxIgnoreAttribute : Attribute;

이 속성이 적용된 Request record, 프로퍼티, 또는 record 생성자 파라미터는 CtxEnricher 소스 생성기에서 자동 생성 대상에서 제외됩니다.

대상효과
Classrecord 전체를 CtxEnricher 생성에서 제외
Property해당 프로퍼티만 CtxEnricher에서 제외
Parameter해당 record 생성자 파라미터만 CtxEnricher에서 제외

문서설명
Use Case와 CQRS 가이드CQRS 패턴 설계 의도와 구현 가이드
검증 시스템 사양TypedValidation, FluentValidation 통합
에러 시스템 사양DomainErrorType, ApplicationErrorType
포트와 어댑터 사양IRepository, IQueryPort
파이프라인 사양Pipeline 동작, UsecaseTransactionPipeline
관측 가능성 사양Field/Tag 사양, Meter 정의