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 + 부록 |
학습 목표
섹션 제목: “학습 목표”이 튜토리얼을 완료하면 다음을 할 수 있습니다:
- CQRS 패턴으로 Command(IRepository)와 Query(IQueryPort)를 분리 설계할 수 있습니다
- FinT 모나드 합성과 Specification 기반 동적 검색으로 함수형 CQRS 파이프라인을 구성할 수 있습니다
- 트랜잭션 파이프라인과 도메인 이벤트 흐름으로 완성도 높은 CQRS 아키텍처를 구축할 수 있습니다
Part 0: 서론
섹션 제목: “Part 0: 서론”CQRS가 왜 필요한지, 어떤 문제를 해결하는지부터 시작합니다. 환경 설정을 마치고 CQRS 아키텍처의 전체 그림을 파악합니다.
Part 1: 도메인 엔티티 기초
섹션 제목: “Part 1: 도메인 엔티티 기초”같은 이름의 상품 두 개는 같은 상품일까요? Entity의 정체성(Identity)부터 시작하여 Aggregate Root, 도메인 이벤트, 엔티티 인터페이스까지 CQRS의 기반이 되는 도메인 모델링을 구축합니다.
| 장 | 주제 | 핵심 학습 내용 |
|---|---|---|
| 1 | Entity와 Identity | Entity<TId>, IEntityId, Ulid 기반 ID |
| 2 | Aggregate Root | AggregateRoot<TId>, 도메인 불변식 |
| 3 | 도메인 이벤트 | IDomainEvent, AddDomainEvent(), ClearDomainEvents |
| 4 | 엔티티 인터페이스 | IAuditable, ISoftDeletable |
Part 2: Command 측 — Repository 패턴
섹션 제목: “Part 2: Command 측 — Repository 패턴”도메인 모델의 불변식을 보장하면서 영속화하려면 어떤 인터페이스가 필요할까요? Aggregate Root 단위의 쓰기 작업을 위한 IRepository 설계부터 InMemory, EF Core 구현, Unit of Work까지 진행합니다.
| 장 | 주제 | 핵심 학습 내용 |
|---|---|---|
| 1 | Repository 인터페이스 | IRepository<TAggregate, TId>, 8개 CRUD, FinT<IO, T> |
| 2 | InMemory Repository | InMemoryRepositoryBase, ConcurrentDictionary |
| 3 | EF Core Repository | EfCoreRepositoryBase, ToDomain/ToModel |
| 4 | Unit of Work | IUnitOfWork, SaveChanges, IUnitOfWorkTransaction |
Part 3: Query 측 — 읽기 전용 패턴
섹션 제목: “Part 3: Query 측 — 읽기 전용 패턴”조회 조건이 늘어날 때마다 메서드를 추가하는 대신, Specification 하나로 동적 검색을 처리합니다. DTO 프로젝션과 3가지 페이지네이션을 통해 읽기 전용 경로를 최적화합니다.
| 장 | 주제 | 핵심 학습 내용 |
|---|---|---|
| 1 | IQueryPort 인터페이스 | IQueryPort<TEntity, TDto>, Search/SearchByCursor/Stream |
| 2 | DTO 분리 | Command DTO vs Query DTO, 프로젝션 |
| 3 | 페이지네이션과 정렬 | PageRequest, CursorPageRequest, SortExpression |
| 4 | InMemory Query 어댑터 | InMemoryQueryBase, GetProjectedItems |
| 5 | Dapper Query 어댑터 | DapperQueryBase, SQL 생성 |
Part 4: CQRS Usecase 통합
섹션 제목: “Part 4: CQRS Usecase 통합”Repository와 Query 어댑터가 준비되었으니, 이제 이들을 Usecase로 통합합니다. Mediator 패턴으로 Command/Query를 디스패치하고, FinT에서 FinResponse로의 변환, 도메인 이벤트 흐름, 트랜잭션 파이프라인까지 CQRS 아키텍처를 완성합니다.
| 장 | 주제 | 핵심 학습 내용 |
|---|---|---|
| 1 | Command Usecase | ICommandRequest, ICommandUsecase, FinResponse |
| 2 | Query Usecase | IQueryRequest, IQueryUsecase, IQueryPort 연동 |
| 3 | FinT -> FinResponse | ToFinResponse(), LINQ 모나딕 합성 |
| 4 | 도메인 이벤트 흐름 | IDomainEventCollector, Track, 발행 |
| 5 | 트랜잭션 파이프라인 | 트랜잭션 파이프라인, Command 자동커밋 |
Part 5: 도메인별 실전 예제
섹션 제목: “Part 5: 도메인별 실전 예제”지금까지 배운 CQRS 패턴을 실제 도메인에 적용합니다. 주문, 고객, 재고, 카탈로그 각 도메인에서 Command/Query 분리가 어떤 이점을 가져오는지 직접 확인합니다.
| 장 | 주제 | 핵심 학습 내용 |
|---|---|---|
| 1 | 주문 관리 | 주문 CQRS 완전 예제 |
| 2 | 고객 관리 | 고객 관리 + Specification 검색 |
| 3 | 재고 관리 | 재고 + Soft Delete + Cursor 페이징 |
| 4 | 카탈로그 검색 | 3가지 페이지네이션 비교 |
- A. CQRS vs 전통적 CRUD
- B. Repository vs Query 어댑터 선택 가이드
- C. FinT / FinResponse 타입 참조
- D. CQRS 안티패턴
- E. 용어집
- F. 참고 자료
핵심 진화 과정
섹션 제목: “핵심 진화 과정”[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장: 카탈로그 검색
Functorium CQRS 타입 계층
섹션 제목: “Functorium CQRS 타입 계층”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의 예제 프로젝트에는 단위 테스트가 포함되어 있습니다. 테스트는 단위 테스트 가이드 규칙을 따릅니다.
테스트 실행 방법
섹션 제목: “테스트 실행 방법”# 튜토리얼 전체 테스트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개)
| 장 | 테스트 프로젝트 | 주요 테스트 내용 |
|---|---|---|
| 1 | EntityAndIdentity.Tests.Unit | Entity<TId>, IEntityId 동작 검증 |
| 2 | AggregateRoot.Tests.Unit | AggregateRoot 불변식 검증 |
| 3 | DomainEvents.Tests.Unit | 도메인 이벤트 추가/삭제 검증 |
| 4 | EntityInterfaces.Tests.Unit | IAuditable, ISoftDeletable 검증 |
Part 2: Command 측 — Repository 패턴 (4개)
| 장 | 테스트 프로젝트 | 주요 테스트 내용 |
|---|---|---|
| 1 | RepositoryInterface.Tests.Unit | IRepository 8개 CRUD 검증 |
| 2 | InMemoryRepository.Tests.Unit | InMemory 구현체 검증 |
| 3 | EfCoreRepository.Tests.Unit | EF Core 구현체 검증 |
| 4 | UnitOfWork.Tests.Unit | SaveChanges, 트랜잭션 검증 |
Part 3: Query 측 — 읽기 전용 패턴 (5개)
| 장 | 테스트 프로젝트 | 주요 테스트 내용 |
|---|---|---|
| 1 | QueryPortInterface.Tests.Unit | IQueryPort Search/Stream 검증 |
| 2 | DtoSeparation.Tests.Unit | Command/Query DTO 분리 검증 |
| 3 | PaginationAndSorting.Tests.Unit | 페이지네이션, 정렬 검증 |
| 4 | InMemoryQueryAdapter.Tests.Unit | InMemory Query 어댑터 검증 |
| 5 | DapperQueryAdapter.Tests.Unit | Dapper SQL 생성 검증 |
Part 4: CQRS Usecase 통합 (5개)
| 장 | 테스트 프로젝트 | 주요 테스트 내용 |
|---|---|---|
| 1 | CommandUsecase.Tests.Unit | Command 핸들러 검증 |
| 2 | QueryUsecase.Tests.Unit | Query 핸들러 검증 |
| 3 | FinTToFinResponse.Tests.Unit | ToFinResponse 변환 검증 |
| 4 | DomainEventFlow.Tests.Unit | 이벤트 수집/발행 검증 |
| 5 | TransactionPipeline.Tests.Unit | 트랜잭션 파이프라인 검증 |
Part 5: 도메인별 실전 예제 (4개)
| 장 | 테스트 프로젝트 | 주요 테스트 내용 |
|---|---|---|
| 1 | EcommerceOrderManagement.Tests.Unit | 주문 CQRS 검증 |
| 2 | CustomerManagement.Tests.Unit | 고객 관리 검증 |
| 3 | InventoryManagement.Tests.Unit | 재고 관리 검증 |
| 4 | CatalogSearch.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/
관련 튜토리얼
섹션 제목: “관련 튜토리얼”이 튜토리얼은 다음 튜토리얼과 함께 학습하면 더 효과적입니다:
- Specification 패턴으로 도메인 규칙 구현하기: Specification 패턴 기초부터 Repository 통합까지. 이 튜토리얼의 IQueryPort, IRepository에서 Specification을 매개변수로 사용합니다.
이 튜토리얼은 Functorium 프로젝트의 실제 CQRS 프레임워크 개발 경험을 바탕으로 작성되었습니다.