본문으로 건너뛰기

CQRS 리포지토리 패턴

C# Functorium으로 Repository와 Query 어댑터를 구현하는 실전 가이드


주문 목록 API에 새 필터가 추가될 때마다 GetByCustomer, GetRecent, SearchByKeyword… Repository 메서드가 끝없이 늘어나고 있나요? 읽기용 프로퍼티가 도메인 모델에 스며들어 쓰기 로직을 오염시키고, 하나를 고치면 다른 쪽이 깨지는 악순환이 반복됩니다.

이 튜토리얼은 그 문제를 Command와 Query의 책임 분리(CQRS)로 해결합니다. 도메인 엔티티 기초에서 시작하여 Repository 패턴, Query 어댑터, Usecase 통합까지, 22개의 실습 프로젝트를 통해 CQRS 패턴의 모든 측면을 단계별로 학습합니다.

“조회 조건이 하나 추가될 때마다 Repository 메서드를 하나 추가하고 있다면, 그것은 설계가 아니라 관성입니다.”

수준대상권장 학습 범위
초급CRUD와 Entity 기본 경험이 있는 개발자Part 0~1
중급Repository 패턴을 이해하고 심화 학습을 원하는 개발자Part 2~3
고급CQRS 아키텍처 설계와 Usecase 통합에 관심 있는 개발자Part 4~5 + 부록

이 튜토리얼을 완료하면 다음을 할 수 있습니다:

  1. CQRS 패턴으로 Command(IRepository)와 Query(IQueryPort)를 분리 설계할 수 있습니다
  2. FinT 모나드 합성과 Specification 기반 동적 검색으로 함수형 CQRS 파이프라인을 구성할 수 있습니다
  3. 트랜잭션 파이프라인과 도메인 이벤트 흐름으로 완성도 높은 CQRS 아키텍처를 구축할 수 있습니다

CQRS가 왜 필요한지, 어떤 문제를 해결하는지부터 시작합니다. 환경 설정을 마치고 CQRS 아키텍처의 전체 그림을 파악합니다.

같은 이름의 상품 두 개는 같은 상품일까요? Entity의 정체성(Identity)부터 시작하여 Aggregate Root, 도메인 이벤트, 엔티티 인터페이스까지 CQRS의 기반이 되는 도메인 모델링을 구축합니다.

주제핵심 학습 내용
1Entity와 IdentityEntity<TId>, IEntityId, Ulid 기반 ID
2Aggregate RootAggregateRoot<TId>, 도메인 불변식
3도메인 이벤트IDomainEvent, AddDomainEvent(), ClearDomainEvents
4엔티티 인터페이스IAuditable, ISoftDeletable

도메인 모델의 불변식을 보장하면서 영속화하려면 어떤 인터페이스가 필요할까요? Aggregate Root 단위의 쓰기 작업을 위한 IRepository 설계부터 InMemory, EF Core 구현, Unit of Work까지 진행합니다.

주제핵심 학습 내용
1Repository 인터페이스IRepository<TAggregate, TId>, 8개 CRUD, FinT<IO, T>
2InMemory RepositoryInMemoryRepositoryBase, ConcurrentDictionary
3EF Core RepositoryEfCoreRepositoryBase, ToDomain/ToModel
4Unit of WorkIUnitOfWork, SaveChanges, IUnitOfWorkTransaction

Part 3: Query 측 — 읽기 전용 패턴

섹션 제목: “Part 3: Query 측 — 읽기 전용 패턴”

조회 조건이 늘어날 때마다 메서드를 추가하는 대신, Specification 하나로 동적 검색을 처리합니다. DTO 프로젝션과 3가지 페이지네이션을 통해 읽기 전용 경로를 최적화합니다.

주제핵심 학습 내용
1IQueryPort 인터페이스IQueryPort<TEntity, TDto>, Search/SearchByCursor/Stream
2DTO 분리Command DTO vs Query DTO, 프로젝션
3페이지네이션과 정렬PageRequest, CursorPageRequest, SortExpression
4InMemory Query 어댑터InMemoryQueryBase, GetProjectedItems
5Dapper Query 어댑터DapperQueryBase, SQL 생성

Repository와 Query 어댑터가 준비되었으니, 이제 이들을 Usecase로 통합합니다. Mediator 패턴으로 Command/Query를 디스패치하고, FinT에서 FinResponse로의 변환, 도메인 이벤트 흐름, 트랜잭션 파이프라인까지 CQRS 아키텍처를 완성합니다.

주제핵심 학습 내용
1Command UsecaseICommandRequest, ICommandUsecase, FinResponse
2Query UsecaseIQueryRequest, IQueryUsecase, IQueryPort 연동
3FinT -> FinResponseToFinResponse(), LINQ 모나딕 합성
4도메인 이벤트 흐름IDomainEventCollector, Track, 발행
5트랜잭션 파이프라인트랜잭션 파이프라인, Command 자동커밋

지금까지 배운 CQRS 패턴을 실제 도메인에 적용합니다. 주문, 고객, 재고, 카탈로그 각 도메인에서 Command/Query 분리가 어떤 이점을 가져오는지 직접 확인합니다.

주제핵심 학습 내용
1주문 관리주문 CQRS 완전 예제
2고객 관리고객 관리 + Specification 검색
3재고 관리재고 + Soft Delete + Cursor 페이징
4카탈로그 검색3가지 페이지네이션 비교

[Part 1] 도메인 엔티티 기초 1장: Entity와 Identity → 2장: Aggregate Root → 3장: 도메인 이벤트 → 4장: 엔티티 인터페이스

[Part 2] Command 측 — Repository 패턴 1장: Repository 인터페이스 → 2장: InMemory Repository → 3장: EF Core Repository → 4장: Unit of Work

[Part 3] Query 측 — 읽기 전용 패턴 1장: IQueryPort 인터페이스 → 2장: DTO 분리 → 3장: 페이지네이션과 정렬 → 4장: InMemory Query 어댑터 → 5장: Dapper Query 어댑터

[Part 4] CQRS Usecase 통합 1장: Command Usecase → 2장: Query Usecase → 3장: FinT -> FinResponse → 4장: 도메인 이벤트 흐름 → 5장: 트랜잭션 파이프라인

[Part 5] 도메인별 실전 예제 1장: 주문 관리 → 2장: 고객 관리 → 3장: 재고 관리 → 4장: 카탈로그 검색


Command 측 (쓰기)
├── IRepository<TAggregate, TId>
│ ├── Create / GetById / Update / Delete
│ ├── CreateRange / GetByIds / UpdateRange / DeleteRange
│ └── 반환 타입: FinT<IO, T>
├── InMemoryRepositoryBase (ConcurrentDictionary 기반)
├── EfCoreRepositoryBase (EF Core 기반)
└── IUnitOfWork
├── SaveChanges() : FinT<IO, Unit>
└── BeginTransactionAsync() : IUnitOfWorkTransaction
Query 측 (읽기)
├── IQueryPort<TEntity, TDto>
│ ├── Search(spec, page, sort) : FinT<IO, PagedResult<TDto>>
│ ├── SearchByCursor(spec, cursor, sort) : FinT<IO, CursorPagedResult<TDto>>
│ └── Stream(spec, sort) : IAsyncEnumerable<TDto>
├── InMemoryQueryBase
└── DapperQueryBase
Usecase 통합
├── ICommandRequest<TSuccess> : ICommand<FinResponse<TSuccess>>
├── ICommandUsecase<TCommand, TSuccess> : ICommandHandler
├── IQueryRequest<TSuccess> : IQuery<FinResponse<TSuccess>>
├── IQueryUsecase<TQuery, TSuccess> : IQueryHandler
└── ToFinResponse() : Fin<A> -> FinResponse<A>
Specification (검색 조건)
├── Specification<T> (추상 클래스)
│ ├── IsSatisfiedBy(T) : bool
│ ├── And() / Or() / Not() 조합
│ ├── & / | / ! 연산자
│ └── All (항등원, 동적 필터 빌더 시드)
└── ExpressionSpecification<T> (EF Core/SQL 지원)
├── ToExpression() → Expression<Func<T, bool>>
└── sealed IsSatisfiedBy (컴파일 + 캐싱)

  • .NET 10.0 SDK 이상
  • VS Code + C# Dev Kit 확장
  • C# 기초 문법 지식
  • Entity와 CRUD 기본 개념

cqrs-repository/
├── Part0-Introduction/ # Part 0: 서론
├── Part1-Domain-Entity-Foundations/ # Part 1: 도메인 엔티티 기초 (4개)
│ ├── 01-Entity-And-Identity/
│ ├── 02-Aggregate-Root/
│ ├── 03-Domain-Events/
│ └── 04-Entity-Interfaces/
├── Part2-Command-Repository/ # Part 2: Command 측 Repository (4개)
│ ├── 01-Repository-Interface/
│ ├── 02-InMemory-Repository/
│ ├── 03-EfCore-Repository/
│ └── 04-Unit-Of-Work/
├── Part3-Query-Patterns/ # Part 3: Query 측 읽기 전용 (5개)
│ ├── 01-QueryPort-Interface/
│ ├── 02-DTO-Separation/
│ ├── 03-Pagination-And-Sorting/
│ ├── 04-InMemory-Query-Adapter/
│ └── 05-Dapper-Query-Adapter/
├── Part4-CQRS-Usecase-Integration/ # Part 4: Usecase 통합 (5개)
│ ├── 01-Command-Usecase/
│ ├── 02-Query-Usecase/
│ ├── 03-FinT-To-FinResponse/
│ ├── 04-Domain-Event-Flow/
│ └── 05-Transaction-Pipeline/
├── Part5-Domain-Examples/ # Part 5: 도메인별 실전 예제 (4개)
│ ├── 01-Ecommerce-Order-Management/
│ ├── 02-Customer-Management/
│ ├── 03-Inventory-Management/
│ └── 04-Catalog-Search/
├── Appendix/ # 부록
└── README.md # 이 문서

모든 Part의 예제 프로젝트에는 단위 테스트가 포함되어 있습니다. 테스트는 단위 테스트 가이드 규칙을 따릅니다.

Terminal window
# 튜토리얼 전체 테스트
dotnet test --solution Docs.Site/src/content/docs/tutorials/cqrs-repository/cqrs-repository.slnx
# 개별 프로젝트 테스트
dotnet test --project Docs.Site/src/content/docs/tutorials/cqrs-repository/Part1-Domain-Entity-Foundations/01-Entity-And-Identity/EntityAndIdentity.Tests.Unit

Part 1: 도메인 엔티티 기초 (4개)

테스트 프로젝트주요 테스트 내용
1EntityAndIdentity.Tests.UnitEntity<TId>, IEntityId 동작 검증
2AggregateRoot.Tests.UnitAggregateRoot 불변식 검증
3DomainEvents.Tests.Unit도메인 이벤트 추가/삭제 검증
4EntityInterfaces.Tests.UnitIAuditable, ISoftDeletable 검증

Part 2: Command 측 — Repository 패턴 (4개)

테스트 프로젝트주요 테스트 내용
1RepositoryInterface.Tests.UnitIRepository 8개 CRUD 검증
2InMemoryRepository.Tests.UnitInMemory 구현체 검증
3EfCoreRepository.Tests.UnitEF Core 구현체 검증
4UnitOfWork.Tests.UnitSaveChanges, 트랜잭션 검증

Part 3: Query 측 — 읽기 전용 패턴 (5개)

테스트 프로젝트주요 테스트 내용
1QueryPortInterface.Tests.UnitIQueryPort Search/Stream 검증
2DtoSeparation.Tests.UnitCommand/Query DTO 분리 검증
3PaginationAndSorting.Tests.Unit페이지네이션, 정렬 검증
4InMemoryQueryAdapter.Tests.UnitInMemory Query 어댑터 검증
5DapperQueryAdapter.Tests.UnitDapper SQL 생성 검증

Part 4: CQRS Usecase 통합 (5개)

테스트 프로젝트주요 테스트 내용
1CommandUsecase.Tests.UnitCommand 핸들러 검증
2QueryUsecase.Tests.UnitQuery 핸들러 검증
3FinTToFinResponse.Tests.UnitToFinResponse 변환 검증
4DomainEventFlow.Tests.Unit이벤트 수집/발행 검증
5TransactionPipeline.Tests.Unit트랜잭션 파이프라인 검증

Part 5: 도메인별 실전 예제 (4개)

테스트 프로젝트주요 테스트 내용
1EcommerceOrderManagement.Tests.Unit주문 CQRS 검증
2CustomerManagement.Tests.Unit고객 관리 검증
3InventoryManagement.Tests.Unit재고 관리 검증
4CatalogSearch.Tests.Unit페이지네이션 비교 검증

T1_T2_T3 명명 규칙을 따릅니다:

// Method_ExpectedResult_Scenario
[Fact]
public void Create_ReturnsAggregate_WhenValid()
{
// Arrange
var order = Order.Create(OrderId.New(), customerId);
// Act
var actual = await repository.Create(order).RunAsync();
// Assert
actual.IsSucc.ShouldBeTrue();
}

이 튜토리얼의 모든 예제 코드는 Functorium 프로젝트에서 확인할 수 있습니다:

  • Repository 인터페이스: Src/Functorium/Domains/Repositories/
  • Repository 구현체: Src/Functorium.Adapters/Repositories/
  • Query 어댑터: Src/Functorium/Applications/Queries/
  • Usecase 인터페이스: Src/Functorium/Applications/Usecases/
  • 트랜잭션 파이프라인: Src/Functorium/Adapters/Observabilities/Pipelines/
  • 튜토리얼 프로젝트: Docs.Site/src/content/docs/tutorials/cqrs-repository/

이 튜토리얼은 다음 튜토리얼과 함께 학습하면 더 효과적입니다:


이 튜토리얼은 Functorium 프로젝트의 실제 CQRS 프레임워크 개발 경험을 바탕으로 작성되었습니다.