본문으로 건너뛰기

Transaction/Caching

앞 장에서 Read+Create 이중 제약을 적용했습니다. 이번에는 동일한 이중 제약을 사용하면서 where 제약 조건으로 컴파일 타임에 적용 대상을 필터링하는 Pipeline을 다룹니다. Transaction Pipeline은 where TRequest : ICommand<TResponse> 제약으로 Command에만, Caching Pipeline은 where TRequest : IQuery<TResponse> 제약으로 Query에만 적용됩니다. Mediator 소스 제너레이터가 where 제약을 확인하여 해당 요청 타입에만 Pipeline을 등록하므로, 런타임 타입 검사 없이 Command/Query가 분리됩니다.

Transaction Pipeline:
isCommand? ──No──→ Skip (Query는 트랜잭션 불필요)
Yes──→ Begin → handler() → IsSucc? → Commit / Rollback
Caching Pipeline:
isCacheable? ──No──→ handler() 직접 실행
Yes──→ cache hit? → 캐시 반환
No → handler() → IsSucc? → 캐시 저장

이 장을 완료하면 다음을 할 수 있습니다:

  1. Transaction Pipeline이 Command에만 적용되는 이유를 설명할 수 있습니다
  2. Caching Pipeline이 성공 응답만 캐싱하는 이유를 설명할 수 있습니다
  3. 두 Pipeline 모두 Read+Create 제약이 필요한 이유를 이해할 수 있습니다
  4. Command/Query 분기가 where 제약 조건으로 이루어지는 방식을 이해할 수 있습니다

Transaction Pipeline은 Command 요청에만 트랜잭션을 적용합니다:

public sealed class SimpleTransactionPipeline<TResponse>
where TResponse : IFinResponse, IFinResponseFactory<TResponse>
{
public TResponse Execute(bool isCommand, Func<TResponse> handler)
{
if (!isCommand)
{
// Query는 트랜잭션 불필요
return handler();
}
// Command: Begin → Execute → Commit/Rollback
var response = handler();
if (response.IsSucc) // Read: IFinResponse
Commit();
else
Rollback();
return response;
}
}

실제 Functorium의 UsecaseTransactionPipelinewhere TRequest : ICommand<TResponse> 제약 조건을 사용합니다. Mediator 소스 제너레이터가 이 제약을 확인하여 Command 요청에만 Pipeline을 적용하므로, 런타임 분기 없이 컴파일 타임에 필터링됩니다.

Caching Pipeline은 Query 요청 중 ICacheable을 구현한 요청에만 캐싱을 적용합니다:

public sealed class SimpleCachingPipeline<TResponse>
where TResponse : IFinResponse, IFinResponseFactory<TResponse>
{
public TResponse GetOrExecute(string cacheKey, bool isCacheable, Func<TResponse> handler)
{
if (!isCacheable)
return handler();
if (TryGetFromCache(cacheKey, out var cached))
return cached;
var response = handler();
if (response.IsSucc) // Read: 성공 응답만 캐싱
SetCache(cacheKey, response);
return response;
}
}

두 Pipeline이 Read와 Create 능력을 각각 어떻게 사용하는지 정리하면 다음과 같습니다.

PipelineRead (IsSucc/IsFail)Create (CreateFail)
TransactionCommit/Rollback 결정예외 시 실패 응답 생성
Caching성공 응답만 캐싱예외 시 실패 응답 생성

두 Pipeline 모두 응답 상태를 읽어야 하므로 IFinResponse가 필요하고, 예외 처리를 위해 IFinResponseFactory<TResponse>도 필요합니다.

실제 Functorium Pipeline에서는 where 제약 조건으로 적용 대상을 컴파일 타임에 결정합니다:

// Transaction Pipeline: where 제약으로 Command에만 적용
internal sealed class UsecaseTransactionPipeline<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : ICommand<TResponse> // ← Command만
where TResponse : IFinResponse, IFinResponseFactory<TResponse>
{ ... }
// Caching Pipeline: where 제약으로 Query에만 적용
internal sealed class UsecaseCachingPipeline<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IQuery<TResponse> // ← Query만
where TResponse : IFinResponse, IFinResponseFactory<TResponse>
{ ... }

Mediator 소스 제너레이터가 where 제약을 확인하여, ICommand<TResponse>를 구현하지 않는 요청에는 UsecaseTransactionPipeline을 등록하지 않고, IQuery<TResponse>를 구현하지 않는 요청에는 UsecaseCachingPipeline을 등록하지 않습니다. 런타임 request is ICommandRequest 같은 타입 검사가 필요 없습니다.

Q1: Transaction Pipeline이 Query를 건너뛰는 것은 어떻게 구현되나요?

섹션 제목: “Q1: Transaction Pipeline이 Query를 건너뛰는 것은 어떻게 구현되나요?”

A: UsecaseTransactionPipelinewhere TRequest : ICommand<TResponse> 제약 조건을 사용합니다. Mediator 소스 제너레이터가 이 제약을 확인하여 Command 요청에만 Pipeline을 등록하므로, Query 요청에는 Transaction Pipeline 자체가 실행되지 않습니다. 런타임 타입 검사 없이 컴파일 타임에 필터링됩니다.

Q2: Caching Pipeline이 실패 응답을 캐싱하지 않는 이유는 무엇인가요?

섹션 제목: “Q2: Caching Pipeline이 실패 응답을 캐싱하지 않는 이유는 무엇인가요?”

A: 실패 응답은 일시적 오류(네트워크 타임아웃, 일시적 DB 장애 등)인 경우가 많습니다. 실패를 캐싱하면 재시도 시에도 캐시된 실패가 반환되어 복구 불가능한 상태가 됩니다. 따라서 response.IsSucc으로 성공 응답만 캐싱합니다.

Q3: Transaction과 Caching이 같은 이중 제약을 사용하지만 적용 대상이 다른 이유는 무엇인가요?

섹션 제목: “Q3: Transaction과 Caching이 같은 이중 제약을 사용하지만 적용 대상이 다른 이유는 무엇인가요?”

A: 두 Pipeline 모두 응답의 성공/실패를 읽는 능력(Read)과 예외 시 실패 응답을 생성하는 능력(Create)이 필요하므로 제약 조건은 동일합니다. 하지만 Transaction은 데이터 변경이 있는 Command에만, Caching은 읽기 전용인 Query에만 적용되는 것이 비즈니스 요구사항입니다.

Q4: ICacheable 인터페이스를 구현하지 않은 Query는 어떻게 되나요?

섹션 제목: “Q4: ICacheable 인터페이스를 구현하지 않은 Query는 어떻게 되나요?”

A: Caching Pipeline은 request is ICacheable로 캐싱 가능 여부를 확인합니다. ICacheable을 구현하지 않은 Query는 캐싱을 건너뛰고 매번 Handler를 실행합니다. 모든 Query에 캐싱을 강제하지 않아 선택적 최적화가 가능합니다.

03-Transaction-Caching-Pipeline/
├── TransactionCachingPipeline/
│ ├── TransactionCachingPipeline.csproj
│ ├── SimpleTransactionPipeline.cs
│ └── Program.cs
├── TransactionCachingPipeline.Tests.Unit/
│ ├── TransactionCachingPipeline.Tests.Unit.csproj
│ ├── xunit.runner.json
│ └── TransactionCachingPipelineTests.cs
└── README.md
Terminal window
# 프로그램 실행
dotnet run --project TransactionCachingPipeline
# 테스트 실행
dotnet test --project TransactionCachingPipeline.Tests.Unit

Repository 계층의 Fin<T>와 Usecase 계층의 FinResponse<T>ToFinResponse() 확장 메서드로 연결하는 브릿지 패턴을 학습합니다.

4.4장: Fin → FinResponse 브릿지