본문으로 건너뛰기

DDD 전술적 설계 개요

이 문서는 도메인 복잡성을 명확한 빌딩블록으로 분해하고, 각 빌딩블록을 Functorium 프레임워크의 타입에 매핑하는 전체 그림을 제공합니다.

“이 로직은 Entity에 넣어야 하나, Service에 넣어야 하나?” “Aggregate 경계를 어디에 두어야 하나?” “검증 실패를 예외로 던져야 하나, 결과 타입으로 반환해야 하나?”

DDD 전술적 설계는 이러한 질문에 일관된 답을 제공하는 빌딩블록 체계입니다. 이 문서는 그 빌딩블록의 전체 맵을 그리고, Functorium 프레임워크가 각 빌딩블록을 어떻게 타입으로 구현하는지 매핑합니다.

이 문서를 통해 다음을 학습합니다:

  1. DDD 전술적 설계의 빌딩블록 전체 구조 - Value Object, Entity, Aggregate, Domain Event 등의 역할과 관계
  2. Functorium 프레임워크와의 타입 매핑 - 각 빌딩블록에 대응하는 Functorium 타입과 네임스페이스
  3. 레이어 아키텍처와 빌딩블록 배치 규칙 - Domain, Application, Adapter 레이어별 책임과 의존성 방향
  4. 모듈과 프로젝트 구조 - Layer(수평) × Module(수직) 이중 축 배치 전략
  5. 유비쿼터스 언어와 네이밍 가이드 - 모든 빌딩블록의 네이밍 패턴 중앙 색인

이 문서를 이해하기 위해 다음 개념에 대한 기본적인 이해가 필요합니다:

  • DDD의 전략적 설계 개념 (Bounded Context, 유비쿼터스 언어)
  • C# 기본 문법 (클래스, 인터페이스, 제네릭)
  • 프로젝트 구조 가이드의 레이어 구조

DDD 전술적 설계의 핵심은 “도메인 복잡성을 명확한 빌딩블록으로 분해하고, 각 빌딩블록의 책임과 배치를 일관되게 유지하는 것”입니다. Functorium은 이 빌딩블록을 타입 시스템으로 강제하여, 설계 결정이 코드에 직접 반영되도록 합니다.

// Value Object 생성
var email = Email.Create("user@example.com");
// Entity/Aggregate 생성
var order = Order.Create(productId, quantity, unitPrice, shippingAddress);
// 도메인 이벤트 발행
order.AddDomainEvent(new CreatedEvent(order.Id, productId, quantity, totalAmount));
// Specification 조합
var spec = priceRange & !lowStock;
// Domain Service 사용 (Usecase 내)
private readonly OrderCreditCheckService _creditCheckService = new();
  1. Value Object 정의: SimpleValueObject<T> 상속, Create() + Validate() 구현
  2. Entity/Aggregate 정의: AggregateRoot<TId> 상속, [GenerateEntityId] 어트리뷰트 적용
  3. 도메인 이벤트 정의: Aggregate 내 중첩 sealed recordDomainEvent 상속
  4. Specification 정의: ExpressionSpecification<T> 상속, ToExpression() 구현
  5. Domain Service 정의: IDomainService 마커 인터페이스 구현, 순수 함수(기본) 또는 Repository 사용(Evans Ch.9)으로 작성
  6. Usecase 구현: ICommandUsecase<T,R> / IQueryUsecase<T,R> 상속, FinT<IO, T> LINQ 체인으로 조율
개념설명Functorium 타입
Value Object불변, 값 동등성, 자기 검증SimpleValueObject<T>, ValueObject
Entity / AggregateID 동등성, 일관성 경계Entity<TId>, AggregateRoot<TId>
Domain Event과거형, 불변, Aggregate 간 통신IDomainEvent, DomainEvent
Domain Service교차 Aggregate 도메인 로직IDomainService
Specification비즈니스 규칙 캡슐화, 조합Specification<T>, ExpressionSpecification<T>
Error 처리Railway Oriented ProgrammingFin<T>, Validation<Error, T>
Layer 구조Domain → Application → Adapter의존성 규칙: 안쪽 → 바깥 참조 금지

소프트웨어의 본질적 복잡성은 도메인에서 비롯됩니다. DDD 전술적 설계는 이 복잡성을 명확한 빌딩블록으로 분해하여 관리합니다. 각 빌딩블록은 역할과 책임이 명확하여, 개발자가 “이 코드는 어디에 두어야 하는가?”라는 질문에 일관된 답을 제공합니다.

유비쿼터스 언어와 코드의 일치

섹션 제목: “유비쿼터스 언어와 코드의 일치”

DDD는 도메인 전문가와 개발자가 동일한 언어를 사용할 것을 강조합니다. 코드에서 Email, Order, Product와 같은 도메인 용어를 직접 타입으로 표현하면, 코드가 곧 도메인 모델이 됩니다.

“이메일 형식이 올바른가?”, “재고가 충분한가?”, “주문 상태 전이가 유효한가?” 같은 비즈니스 규칙이 특정 빌딩블록(Value Object, Entity, Aggregate)에 배치되어, 규칙의 위치와 책임이 명확합니다.

전술적 설계 없는 코드 vs 있는 코드

섹션 제목: “전술적 설계 없는 코드 vs 있는 코드”

전술적 설계가 없으면 비즈니스 로직이 서비스 계층 곳곳에 흩어집니다. 이메일 형식 검증이 컨트롤러, 서비스, 리포지토리에서 각각 다른 방식으로 수행되고, “이 규칙은 어디서 관리하는가?”라는 질문에 답할 수 없습니다.

전술적 설계를 적용하면 각 규칙이 명확한 빌딩블록에 배치됩니다. 이메일 형식은 Email Value Object에, 재고 부족 검증은 Inventory Aggregate에, 주문 생성 조율은 Usecase에 위치하므로, 코드 구조만으로도 책임 소재가 드러납니다.

지금까지 DDD 전술적 설계가 필요한 이유를 살펴보았습니다. 다음 섹션에서는 각 빌딩블록이 무엇인지, 그리고 Functorium 프레임워크에서 어떤 타입으로 구현되는지 알아봅니다.

빌딩블록역할특성
Value Object도메인 개념의 값 표현불변, 값 동등성, 자기 검증
Entity식별자를 가진 도메인 객체ID 동등성, 가변, 생명주기
Aggregate일관성 경계를 가진 객체 그룹트랜잭션 단위, 불변식 보호
Domain Event도메인에서 발생한 중요한 사건과거형, 불변, Aggregate 간 통신
Domain Service교차 Aggregate 도메인 로직 (순수 또는 Repository 사용)Stateless, IDomainService 마커
FactoryAggregate 생성/복원정적 Create(), CreateFromValidated() 메서드
RepositoryAggregate의 영속화Aggregate 단위로 저장/조회
Application Service유스케이스 조율Command/Query, 도메인 객체 위임

다음 표는 DDD 빌딩블록과 Functorium 프레임워크 타입 간의 전체 매핑을 보여줍니다. 새로운 빌딩블록을 구현할 때 이 표에서 해당 타입과 네임스페이스를 확인하세요.

DDD 빌딩블록Functorium 타입위치
Value ObjectSimpleValueObject<T>, ValueObject, ComparableSimpleValueObject<T>Functorium.Domains.ValueObjects
EntityEntity<TId>Functorium.Domains.Entities
Aggregate RootAggregateRoot<TId>Functorium.Domains.Entities
Entity IDIEntityId<T> + [GenerateEntityId]Functorium.Domains.Entities
Domain EventIDomainEvent, DomainEventFunctorium.Domains.Events
Domain ServiceIDomainServiceFunctorium.Domains.Services
SpecificationSpecification<T>Functorium.Domains.Specifications
Domain ErrorDomainError, DomainErrorTypeFunctorium.Domains.Errors
CommandICommandRequest<T>, ICommandUsecase<T,R>Functorium.Applications.Usecases
QueryIQueryRequest<T>, IQueryUsecase<T,R>Functorium.Applications.Usecases
Event HandlerIDomainEventHandler<T>Functorium.Applications.Events
Application ErrorApplicationError, ApplicationErrorTypeFunctorium.Applications.Errors
PortIObservablePortFunctorium.Abstractions.Observabilities
RepositoryIRepository<TAggregate, TId>Functorium.Domains.Repositories
Adapter[GenerateObservablePort]Adapter Layer 프로젝트
Adapter ErrorAdapterError, AdapterErrorTypeFunctorium.Adapters.Errors
검증ValidationRules<T>, TypedValidation<T,V>Functorium.Domains.ValueObjects.Validations
결과 타입Fin<T>, Validation<Error, T>, FinResponse<T>LanguageExt / Functorium

빌딩블록의 역할과 Functorium 타입 매핑을 확인했습니다. 다음 섹션에서는 Functorium이 DDD와 함수형 프로그래밍을 어떻게 결합하는지, 그 설계 철학을 살펴봅니다.

Functorium은 Domain-Driven Design(DDD)의 전술적 패턴과 함수형 프로그래밍을 결합합니다. 다음 표는 두 패러다임의 개념이 Functorium에서 어떻게 하나로 합쳐지는지 보여줍니다.

개념DDD함수형 프로그래밍Functorium
값 객체불변 객체, 값 기반 동등성불변 데이터 구조ValueObject, SimpleValueObject<T>
검증자기 검증 객체타입 안전 검증ValidationRules<T>, TypedValidation<T,V>
에러 처리예외 vs 결과Railway Oriented ProgrammingFin<T>, Validation<Error, T>
  1. 타입 안전성: 컴파일 타임에 오류 방지
  2. 불변성: 모든 값 객체는 생성 후 변경 불가
  3. 자기 검증: 잘못된 상태의 객체는 생성 불가
  4. 명시적 오류 처리: 예외 대신 결과 타입 사용

값 객체(Value Object)는 속성 값으로 동등성을 판단하는 불변 객체입니다.

// 값 객체 예시: 이메일 (전체 구현은 §빠른 시작 예제 참조)
public sealed class Email : SimpleValueObject<string>
{
private Email(string value) : base(value) { }
public static Fin<Email> Create(string? value) =>
CreateFromValidation(Validate(value), v => new Email(v));
public static Validation<Error, string> Validate(string? value) =>
ValidationRules<Email>.NotEmpty(value ?? "")
.ThenMatches(EmailPattern)
.ThenMaxLength(254);
}

값 객체의 특성:

특성설명
불변성생성 후 변경 불가
값 기반 동등성속성 값으로 동등성 판단
자기 검증생성 시 유효성 검증
도메인 로직 캡슐화관련 연산 포함

Entity는 고유한 식별자(ID)를 가진 도메인 객체입니다. ID가 같으면 동일한 Entity입니다.

// Entity 예시: 주문 (검증된 VO를 받아 Aggregate 생성)
[GenerateEntityId] // OrderId 자동 생성
public sealed class Order : AggregateRoot<OrderId>
{
public ProductId ProductId { get; private set; }
public Quantity Quantity { get; private set; }
public Money UnitPrice { get; private set; }
public Money TotalAmount { get; private set; }
private Order(OrderId id, ProductId productId, Quantity quantity,
Money unitPrice, Money totalAmount) : base(id) { /* ... */ }
// Create: 검증된 VO를 받아 새 Aggregate 생성
public static Order Create(
ProductId productId, Quantity quantity,
Money unitPrice, ShippingAddress shippingAddress)
{
var totalAmount = unitPrice.Multiply(quantity);
var order = new Order(OrderId.New(), productId, quantity, unitPrice, totalAmount);
order.AddDomainEvent(new CreatedEvent(order.Id, productId, quantity, totalAmount));
return order;
}
}

Entity vs Value Object:

관점EntityValue Object
식별자ID 기반 동등성값 기반 동등성
가변성가변불변
생명주기장기 (Repository)단기 (일회성)
예시Order, User, ProductMoney, Email, Address

값 객체는 항상 유효한 상태로만 존재합니다:

// 유효하지 않은 이메일은 생성 불가
var result = Email.Create("invalid"); // Fin<Email> - 실패
var result = Email.Create("user@example.com"); // Fin<Email> - 성공

에러 처리 전략 (Railway Oriented Programming)

섹션 제목: “에러 처리 전략 (Railway Oriented Programming)”

Functorium은 예외 대신 결과 타입을 사용합니다:

입력 → [검증1] → [검증2] → [검증3] → 성공
↓ ↓ ↓
실패 실패 실패

두 가지 결과 타입:

타입용도특징
Fin<T>최종 결과성공 또는 단일 에러
Validation<Error, T>검증 결과성공 또는 여러 에러
IValueObject (인터페이스)
AbstractValueObject (추상 클래스)
├── GetEqualityComponents() - 동등성 컴포넌트
├── Equals() / GetHashCode() - 값 기반 동등성
└── == / != 연산자
└── ValueObject
├── CreateFromValidation<TValueObject, TValue>() 헬퍼
├── SimpleValueObject<T>
│ ├── protected T Value
│ ├── CreateFromValidation<TValueObject>() 헬퍼
│ └── explicit operator T
└── ComparableValueObject
├── GetComparableEqualityComponents()
├── IComparable<ComparableValueObject>
├── < / <= / > / >= 연산자
└── ComparableSimpleValueObject<T>
├── protected T Value
├── CreateFromValidation<TValueObject>() 헬퍼
└── explicit operator T
IEntity<TId> (인터페이스)
├── TId Id - Entity 식별자
├── CreateMethodName 상수
└── CreateFromValidatedMethodName 상수
└── Entity<TId> (추상 클래스)
├── Id 속성 (protected init)
├── Equals() / GetHashCode() - ID 기반 동등성
├── == / != 연산자
├── CreateFromValidation<TEntity, TValue>() 헬퍼
└── GetUnproxiedType() - ORM 프록시 지원
└── AggregateRoot<TId> : IDomainEventDrain
├── DomainEvents (읽기 전용, IHasDomainEvents)
├── AddDomainEvent() (protected)
└── ClearDomainEvents() (IDomainEventDrain)
IEntityId<T> : IParsable<T> (인터페이스) - Ulid 기반
├── Ulid Value
├── static T New()
├── static T Create(Ulid)
└── static T Create(string)
IDomainEvent : INotification (인터페이스)
├── DateTimeOffset OccurredAt
├── Ulid EventId
├── string? CorrelationId
└── string? CausationId
└── DomainEvent (abstract record)
├── 기본 생성자: OccurredAt, EventId 자동 설정
└── CorrelationId, CausationId 선택적 지정
IHasDomainEvents (읽기 전용 이벤트 조회)
└── IDomainEventDrain (internal, 이벤트 정리)
Error (LanguageExt)
├── DomainError
│ └── DomainErrorType (Presence, Length, Format, DateTime, Numeric, Range, Existence, Transition, Custom)
├── ApplicationError
│ └── ApplicationErrorType (공통, 권한, 검증, 비즈니스 규칙, 커스텀)
└── AdapterError
└── AdapterErrorType (공통, Pipeline, 외부 서비스, 데이터, 커스텀)
Specification<T> (추상 클래스)
├── abstract bool IsSatisfiedBy(T entity)
├── And() / Or() / Not() 조합 메서드
├── & / | / ! 연산자 오버로드
├── AndSpecification<T> (internal sealed)
├── OrSpecification<T> (internal sealed)
├── NotSpecification<T> (internal sealed)
├── ExpressionSpecification<T> (public abstract)
└── AllSpecification<T> (internal sealed)
+-------------------+ +-------------------+
| ValueObject | | Validation |
| |◄────────| ValidationRules |
+-------------------+ +-------------------+
│ │
│ │
▼ ▼
+-------------------+ +-------------------+
| Fin<T> / | | DomainError |
| Validation<E,T> |◄────────| |
+-------------------+ +-------------------+

설계 철학과 핵심 개념을 이해했으니, 이제 각 빌딩블록이 실제 프로젝트의 어느 레이어에 배치되는지 알아봅니다.

레이어 아키텍처와 빌딩블록 배치

섹션 제목: “레이어 아키텍처와 빌딩블록 배치”

도메인의 핵심 비즈니스 로직을 담당합니다. 외부 의존성이 없습니다.

  • 배치되는 빌딩블록: Value Object, Entity, Aggregate Root, Domain Event, Domain Error, Domain Service, Repository Interface
  • 의존성: 없음 (가장 안쪽 레이어)

유스케이스를 조율합니다. 도메인 객체에 작업을 위임합니다.

  • 배치되는 빌딩블록: Command/Query (Use Case), Event Handler, Application Error, Port Interface
  • 의존성: Domain Layer만 의존

외부 시스템과의 통신을 담당합니다.

  • 배치되는 빌딩블록: Adapter 구현체, Pipeline (자동 생성), Adapter Error
  • 의존성: Domain Layer, Application Layer 의존
Adapter Layer → Application Layer → Domain Layer
(바깥) (중간) (안쪽)

안쪽 레이어는 바깥 레이어를 절대 참조하지 않습니다. Application Layer가 Adapter 기능이 필요할 때는 Port(인터페이스)를 정의하고, Adapter Layer가 이를 구현합니다.

에릭 에반스는 Module을 도메인 개념의 응집도를 기준으로 그룹화하는 단위로 정의합니다. Module은 패키지나 네임스페이스가 아니라 의미론적 경계입니다.

원칙설명
높은 응집도같은 Module 안의 요소는 하나의 도메인 개념을 표현
낮은 결합도Module 간 의존은 최소화하고, 필요 시 Port/Interface로 소통
커뮤니케이션Module 이름이 유비쿼터스 언어를 반영하여 코드 구조만으로 도메인 경계 전달

Functorium는 Layer(수평 축)Module(수직 축) 의 이중 축으로 코드를 배치합니다.

  • Layer — .csproj 단위. 기술적 관심사(Domain, Application, Adapter)를 분리
  • Module — 폴더/네임스페이스 단위. 도메인 개념(Products, Orders 등)의 응집도를 유지
│ Products │ Inventories │ Orders │ Customers │ SharedModels │
──────────────┼───────────┼─────────────┼─────────┼───────────┼──────────────┤
Domain │ Aggregate │ Aggregate │Aggregate│ Aggregate │ VO, Entity, │
(.csproj) │ VO, Spec │ Spec │ VO │ VO, Spec │ Event │
│ Port │ Port │ Port │ Port │ │
──────────────┼───────────┼─────────────┼─────────┼───────────┼──────────────┤
Application │ Command │ Command │ Command │ Command │ │
(.csproj) │ Query │ Query │ Query │ Query │ │
│ EventHdlr │ EventHdlr │EventHdlr│ EventHdlr │ │
──────────────┼───────────┼─────────────┼─────────┼───────────┼──────────────┤
Adapter │ Endpoint │ Endpoint │Endpoint │ Endpoint │ │
(.csproj ×3) │ Repo │ Repo │ Repo │ Repo │ │
│ QueryAdpt │ QueryAdpt │ │ │ │
──────────────┴───────────┴─────────────┴─────────┴───────────┴──────────────┘

매핑 규칙:

단위분리 기준예시
Layer (수평).csproj기술적 관심사, 의존성 방향LayeredArch.Domain, LayeredArch.Application
Module (수직)폴더/네임스페이스도메인 개념 응집도AggregateRoots/Products/, Usecases/Products/

SingleHost 프로젝트의 실제 모듈 구성입니다.

ModuleDomainApplicationAdapter
ProductsAggregateRoots/Products/ (Aggregate, Ports, Specs, VOs)Usecases/Products/ (Commands, Queries, Dtos, Ports)Endpoints, Repository, Query
InventoriesAggregateRoots/Inventories/ (Aggregate, Ports, Specs)Usecases/Inventories/ (Commands, Queries, Dtos, Ports)Endpoints, Repository, Query
OrdersAggregateRoots/Orders/ (Aggregate, Ports, VOs)Usecases/Orders/ (Commands, Queries)Endpoints, Repository
CustomersAggregateRoots/Customers/ (Aggregate, Ports, Specs, VOs)Usecases/Customers/ (Commands, Queries)Endpoints, Repository
SharedModelsSharedModels/ (공유 VO, Entity, Event)

패턴: 각 Module은 Domain → Application → Adapter 전 Layer를 관통하는 수직 슬라이스입니다. 폴더 이름이 곧 Module 이름이고, Module 이름이 곧 유비쿼터스 언어입니다.

호스트 프로젝트의 도메인 계층은 다음과 같은 폴더 구조를 따릅니다.

참조 예시 (01-SingleHost LayeredArch.Domain/):

LayeredArch.Domain/
├── AggregateRoots/
│ ├── Customers/
│ │ ├── Customer.cs
│ │ ├── ICustomerRepository.cs
│ │ ├── Specifications/
│ │ │ └── CustomerEmailSpec.cs
│ │ └── ValueObjects/
│ │ ├── CustomerName.cs
│ │ └── Email.cs
│ ├── Inventories/
│ │ ├── Inventory.cs
│ │ ├── IInventoryRepository.cs
│ │ └── Specifications/
│ │ └── InventoryLowStockSpec.cs
│ ├── Orders/
│ │ ├── Order.cs
│ │ ├── IOrderRepository.cs
│ │ └── ValueObjects/
│ │ └── ShippingAddress.cs
│ └── Products/
│ ├── Product.cs
│ ├── IProductRepository.cs
│ ├── Specifications/
│ │ ├── ProductNameSpec.cs
│ │ ├── ProductNameUniqueSpec.cs
│ │ └── ProductPriceRangeSpec.cs
│ └── ValueObjects/
│ ├── ProductDescription.cs
│ └── ProductName.cs
├── SharedModels/
│ ├── Entities/
│ │ ├── Tag.cs
│ │ └── ValueObjects/
│ │ └── TagName.cs
│ ├── Services/
│ │ └── OrderCreditCheckService.cs
│ └── ValueObjects/
│ ├── Money.cs
│ └── Quantity.cs
├── DOMAIN-GLOSSARY.md
├── Using.cs
└── AssemblyReference.cs

구조 요약:

  • AggregateRoots/{Aggregate}/ — 애그리거트 루트, 리포지토리 인터페이스, 하위 Specifications/ValueObjects/
  • SharedModels/ — 여러 애그리거트가 공유하는 Entities/, Services/, ValueObjects/
  • 루트 — DOMAIN-GLOSSARY.md, Using.cs, AssemblyReference.cs

Module 내부 배치 (기본)

  • 특정 Aggregate 전용 타입 → 해당 Aggregate 폴더 내부
  • 예: ProductNameAggregateRoots/Products/ValueObjects/

SharedModels 이동 기준

  • 2개 이상 Aggregate에서 공유하는 타입 → SharedModels/
  • 예: Money, QuantitySharedModels/ValueObjects/

프로젝트 루트 이동 기준

  • 교차 Aggregate Port → Domain/Ports/ (예: IProductCatalog — Order에서 Product 검증용)
  • Domain Service → Domain/Services/ (예: OrderCreditCheckService — 교차 Aggregate 순수 로직)

처음에는 Aggregate 전용으로 배치하고, 공유가 필요해지면 SharedModels로 이동합니다. 이 규칙의 상세 판단 기준은 01-project-structure.md FAQ §3을 참조하세요.

서비스가 성장할 때 모듈 구조의 3단계 진화 경로입니다.

단계구조설명
1단계단일 Aggregate하나의 Aggregate가 하나의 Module. SingleHost 초기 Product 구조
2단계Multi-Aggregate 동일 서비스여러 Aggregate가 폴더로 분리되지만 동일 서비스(프로세스) 내 배치. SingleHost 현재 구조
3단계별도 Bounded ContextModule이 독립 서비스(.sln)로 분리. Context Map 패턴 필요

2단계 → 3단계 분리 판단 기준:

기준동일 서비스 유지별도 서비스 분리
배포 주기동일Module별 독립 배포 필요
트랜잭션 경계Aggregate 간 같은 DB 공유 가능독립 DB/스키마 필요
팀 소유권같은 팀다른 팀이 독립적으로 개발
유비쿼터스 언어용어 충돌 없음같은 용어가 다른 의미
데이터 저장소동종 (예: 모두 PostgreSQL)이종 (예: SQL + NoSQL)

참고: 3단계 Bounded Context 분리 패턴(Context Map, ACL 등)은 아래 §8 Bounded Context 경계 정의에서 다룹니다.

유비쿼터스 언어와 네이밍 가이드

섹션 제목: “유비쿼터스 언어와 네이밍 가이드”

각 빌딩블록의 상세 네이밍 규칙은 개별 가이드에 기술되어 있습니다. 이 섹션은 모든 빌딩블록의 네이밍 패턴을 한 곳에서 참조할 수 있는 중앙 색인 역할을 합니다.

다음 표는 모든 빌딩블록의 네이밍 규칙을 한 곳에 모은 중앙 색인입니다. 새 타입을 추가할 때 이 표를 참조하여 일관된 이름을 부여하세요.

빌딩블록네이밍 패턴예시상세 참조
Value Object{Concept}ProductName, Email05a-value-objects.md
Entity{EntityName}Tag06b-entity-aggregate-core.md
Aggregate Root{Aggregate}Product, Order06b-entity-aggregate-core.md
Entity ID{Aggregate}Id + [GenerateEntityId]ProductId, OrderId06b-entity-aggregate-core.md
Domain Event{Aggregate}.{PastTense}Event (nested record)Product.CreatedEvent07-domain-events.md
Domain ErrorDomainError.For<{Type}>()DomainError.For<Email>()08b-error-system-domain-app.md
Domain Service{DomainConcept}Service : IDomainServiceOrderCreditCheckService09-domain-services.md
Specification{Aggregate}{Concept}SpecProductNameUniqueSpec10-specifications.md
Command{Verb}{Aggregate}Command (nested Request/Response/Usecase)CreateProductCommand11-usecases-and-cqrs.md
Query{Get/Search}{Description}Query (nested Request/Response/Usecase)SearchProductsQuery11-usecases-and-cqrs.md
Event HandlerOn{DomainEvent}OnProductCreated01-project-structure.md
Repository InterfaceI{Aggregate}RepositoryIProductRepository12-ports.md
Repository Impl{Technology}{Aggregate}RepositoryEfCoreProductRepository13-adapters.md
Query Adapter InterfaceI{Aggregate}QueryIProductQuery12-ports.md
Query Adapter Impl{Technology}{Aggregate}QueryDapperProductQuery13-adapters.md
Cross-Aggregate PortI{Concept}IProductCatalog12-ports.md
Endpoint{Verb}{Aggregate}EndpointCreateProductEndpoint01-project-structure.md
Persistence Model{Aggregate}ModelProductModel13-adapters.md
Mapper{Aggregate}MapperProductMapper13-adapters.md
Module (폴더)복수 명사 (유비쿼터스 언어)Products/, Orders/§6

도메인 전문가와 개발자가 공유하는 용어집을 유지하면, 코드 네이밍과 비즈니스 용어의 괴리를 방지할 수 있습니다.

도메인 용어정의코드 타입비고
상품판매 카탈로그의 개별 항목Product (Aggregate)
재고상품의 가용 수량Inventory (Aggregate)Product와 1:1
주문고객의 구매 요청Order (Aggregate)
금액통화 + 수치 조합Money (Value Object)SharedModels
수량0 이상의 정수 값Quantity (Value Object)SharedModels

활용: 프로젝트별 용어집을 위 형식으로 작성하여 도메인 전문가와 공유합니다. 용어가 변경되면 코드 타입명도 함께 변경합니다.

  • 용어집은 도메인 전문가와 개발자가 반복적으로 합의하여 유지합니다.
  • 코드에서 도메인 용어와 다른 이름을 사용하면 커뮤니케이션 비용이 증가합니다. 용어 충돌 발견 시 즉시 용어집을 갱신하고 코드를 리네이밍합니다.
  • 새 빌딩블록 추가 시 위의 네이밍 패턴 테이블을 참조하여 일관된 이름을 부여합니다.

현재 SingleHost 프로젝트는 단일 Bounded Context 내에서 여러 Module(Products, Orders 등)을 운영합니다. 이 섹션은 서비스가 성장하여 다중 Bounded Context로 분리될 때 적용할 Context Map 패턴을 정의하고, 기존 코드에서 이미 존재하는 선행 패턴을 식별합니다.

패턴설명Functorium 매핑
Shared Kernel두 BC가 공유하는 도메인 모델 부분집합SharedModels/ 폴더 (Money, Quantity)
Customer-Supplier상류 BC가 하류 BC에 API 제공미구현 (향후 서비스 간 REST API)
Anti-Corruption Layer (ACL)외부 모델 오염 방지 변환 계층IProductCatalog Port + EF Core Mapper
Open Host Service표준 프로토콜로 공개 API 제공REST Endpoints
Published LanguageBC 간 공유 언어 (이벤트/스키마)Domain Events (향후 Integration Event)
Conformist하류가 상류 모델을 그대로 수용미구현
Separate WaysBC 간 통합 없이 독립 운영미구현

기존 코드에는 이미 Context Map 패턴의 단일 서비스 내 선행 구현이 존재합니다. 서비스 분리 시 이 패턴들이 BC 간 통합 지점이 됩니다.

Shared Kernel → SharedModels/ValueObjects/

Money, Quantity 등 여러 Module이 공유하는 Value Object가 SharedModels/ 폴더에 배치되어 있습니다. 서비스 분리 시 NuGet 패키지로 추출하거나 각 BC에 복제하는 결정이 필요합니다.

ACL (mini) → IProductCatalog Port + Adapter

Order Module이 Product 데이터를 조회할 때 IProductCatalog Port를 통해 접근합니다. 현재는 동일 프로세스 내 EF Core 구현이지만, 서비스 분리 시 원격 API 호출 + 응답 변환 계층(ACL)으로 교체됩니다.

Domain Events as Published Language

현재 Domain Event는 in-process Mediator로 발행됩니다. 서비스 분리 시 메시지 브로커(RabbitMQ, Kafka 등)를 통한 Integration Event로 전환되며, 이때 Domain Event와 Integration Event의 분리가 필요합니다.

다중 Bounded Context로 분리될 때의 개념적 프로젝트 구조입니다.

Services/
├── ProductCatalog/ ← BC 1 (기존 3-Layer 구조 동일)
│ ├── ProductCatalog.Domain/
│ ├── ProductCatalog.Application/
│ └── ProductCatalog.Adapters.*/
├── OrderManagement/ ← BC 2
│ ├── OrderManagement.Domain/
│ ├── OrderManagement.Application/
│ └── OrderManagement.Adapters.*/
SharedModels/ ← 공유 NuGet 패키지
IntegrationEvents/ ← Published Language (BC 간 공유 이벤트 스키마)

각 BC는 §5의 3-Layer 구조(Domain → Application → Adapter)를 그대로 유지합니다. BC 간 통신만 Cross-Aggregate Port 대신 Integration Event 또는 REST API로 교체됩니다.

§6의 Multi-Aggregate 확장 가이드에서 3단계 분리 판단 기준(WHEN) 을 제시했습니다. 이 섹션의 Context Map 패턴은 분리를 결정한 후 어떻게(HOW) 구현할지를 안내합니다.

  • WHEN: 배포 주기, 트랜잭션 경계, 팀 소유권, 유비쿼터스 언어 충돌, 데이터 저장소 이종성 → §6 판단 기준 테이블
  • HOW: Shared Kernel, ACL, Published Language, Open Host Service → 이 섹션의 Context Map 패턴
using Functorium.Domains.ValueObjects;
using Functorium.Domains.ValueObjects.Validations.Typed;
using System.Text.RegularExpressions;
public sealed class Email : SimpleValueObject<string>
{
private static readonly Regex EmailPattern = new(
@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
RegexOptions.Compiled);
private const int MaxLength = 254;
// private 생성자 - 외부 생성 차단
private Email(string value) : base(value) { }
// 팩토리 메서드
public static Fin<Email> Create(string? value) =>
CreateFromValidation(Validate(value), v => new Email(v));
// 검증 메서드 (원시 타입 반환)
public static Validation<Error, string> Validate(string? value) =>
ValidationRules<Email>.NotEmpty(value ?? "")
.ThenNormalize(v => v.ToLowerInvariant())
.ThenMatches(EmailPattern)
.ThenMaxLength(MaxLength);
// 암시적 변환 (선택적)
public static implicit operator string(Email email) => email.Value;
}
// 성공
var email = Email.Create("User@Example.COM");
email.IfSucc(e => Console.WriteLine(e)); // user@example.com
// 실패
var invalid = Email.Create("invalid-email");
invalid.IfFail(e => Console.WriteLine(e.Code)); // DomainErrors.Email.InvalidFormat
using Functorium.Testing.Assertions.Errors;
using static Functorium.Domains.Errors.DomainErrorType;
[Fact]
public void Create_ShouldFail_WhenEmailIsEmpty()
{
// Arrange
var emptyEmail = "";
// Act
var result = Email.Create(emptyEmail);
// Assert
result.ShouldBeDomainError<Email, Email>(new Empty());
}
[Fact]
public void Create_ShouldSucceed_WhenEmailIsValid()
{
// Arrange
var validEmail = "user@example.com";
// Act
var result = Email.Create(validEmail);
// Assert
result.IsSucc.ShouldBeTrue();
}
문서설명주요 내용
05a-value-objects.md값 객체 구현기반 클래스, 검증 시스템, 구현 패턴, 실전 예제
05b-value-objects-validation.md값 객체 검증·열거형열거형 구현, Application 검증, FAQ
06a-aggregate-design.mdAggregate 설계설계 원칙, 경계 설정, 안티패턴
06b-entity-aggregate-core.mdEntity/Aggregate 핵심 패턴클래스 계층, ID 시스템, 생성 패턴, 도메인 이벤트
06c-entity-aggregate-advanced.mdEntity/Aggregate 고급 패턴Cross-Aggregate 관계, 부가 인터페이스, 실전 예제
07-domain-events.md도메인 이벤트이벤트 정의, 발행, 핸들러 구현
08a-error-system.md에러 시스템: 기초와 네이밍에러 처리 원칙, Fin 패턴, 네이밍 규칙
08b-error-system-domain-app.md에러 시스템: Domain/Application 에러Domain/Application/Event 에러 정의와 테스트
08c-error-system-adapter-testing.md에러 시스템: Adapter 에러와 테스트Adapter 에러, Custom 에러, 테스트 모범 사례, 체크리스트
11-usecases-and-cqrs.mdUsecase 구현CQRS 패턴, Apply 병합
12-ports.mdPort 아키텍처Port 정의, IObservablePort 계층
13-adapters.mdAdapter 구현Repository, External API, Messaging, Query
14a-adapter-pipeline-di.mdAdapter 연결Pipeline, DI, Options
14b-adapter-testing.mdAdapter 테스트단위 테스트, E2E Walkthrough
09-domain-services.md도메인 서비스IDomainService, 교차 Aggregate 로직, Usecase 통합
10-specifications.mdSpecification 패턴비즈니스 규칙 캡슐화, And/Or/Not 조합, Repository 통합
15a-unit-testing.md단위 테스트테스트 규칙, 네이밍, 체크리스트
16-testing-library.md테스트 라이브러리로그/아키텍처/소스생성기/Job 테스트

LayeredArch 예제 프로젝트에서 실제 구현을 확인할 수 있습니다:

개념예제 파일
값 객체Tests.Hosts/01-SingleHost/LayeredArch.Domain/ValueObjects/
EntityTests.Hosts/01-SingleHost/LayeredArch.Domain/Entities/Product.cs
RepositoryTests.Hosts/01-SingleHost/Src/LayeredArch.Domain/AggregateRoots/Products/Ports/IProductRepository.cs
Repository (공통)Src/Functorium/Domains/Repositories/IRepository.cs
UsecaseTests.Hosts/01-SingleHost/LayeredArch.Application/Usecases/Products/
Domain ServiceTests.Hosts/01-SingleHost/Src/LayeredArch.Domain/Services/OrderCreditCheckService.cs
SpecificationTests.Hosts/01-SingleHost/Src/LayeredArch.Domain/AggregateRoots/Products/Specifications/
AdapterTests.Hosts/01-SingleHost/LayeredArch.Adapters.Persistence/Repositories/

Value Object의 Create()가 항상 실패한다

섹션 제목: “Value Object의 Create()가 항상 실패한다”

원인: Validate() 메서드에서 null이나 빈 문자열을 처리하지 않거나, 정규식 패턴이 잘못되었을 수 있습니다.

해결: Validate() 메서드에서 null 처리를 확인하고, ValidationRules<T>.NotEmpty(value ?? "") 패턴을 사용하세요. 정규식 패턴은 별도 단위 테스트로 검증하세요.

Entity의 ID가 비교되지 않는다 (동등성 실패)

섹션 제목: “Entity의 ID가 비교되지 않는다 (동등성 실패)”

원인: [GenerateEntityId] 어트리뷰트 없이 직접 ID 타입을 정의했거나, IEntityId<T>를 구현하지 않았을 수 있습니다.

해결: Entity ID는 반드시 [GenerateEntityId] 소스 생성기를 사용하세요. 소스 생성기가 Equals(), GetHashCode(), ==, != 연산자를 자동 생성합니다.

도메인 로직을 어디에 배치해야 할지 모르겠다

섹션 제목: “도메인 로직을 어디에 배치해야 할지 모르겠다”

원인: 빌딩블록 간 역할 구분이 명확하지 않을 때 발생합니다.

해결: 다음 판단 기준을 따르세요:

  1. 단일 Aggregate 내부 → Entity 메서드 또는 Value Object
  2. 여러 Aggregate 참조 + I/O 없음 → Domain Service
  3. I/O 필요 (Repository, 외부 API) → Usecase에서 조율
  4. 상태 변경 후 부수 효과 → Domain Event + Event Handler

Q1. Value Object와 Entity의 선택 기준은?

섹션 제목: “Q1. Value Object와 Entity의 선택 기준은?”

식별자(ID)가 필요한지 여부가 핵심입니다. Money, Email처럼 값 자체로 동등성을 판단하면 Value Object, Order, Product처럼 고유 ID로 추적해야 하면 Entity입니다. 일반적으로 Value Object가 더 많고, Entity가 소수여야 합니다.

Q2. Aggregate 경계를 어떻게 설정하나요?

섹션 제목: “Q2. Aggregate 경계를 어떻게 설정하나요?”

하나의 트랜잭션에서 일관성을 보장해야 하는 범위가 Aggregate 경계입니다. Aggregate를 작게 유지하고, Aggregate 간 참조는 ID만 사용하세요. 상세 설계 원칙은 06a-aggregate-design.md를 참조하세요.

Q3. SharedModels에 배치해야 할 타입의 기준은?

섹션 제목: “Q3. SharedModels에 배치해야 할 타입의 기준은?”

2개 이상의 Aggregate에서 공유하는 Value Object나 Entity가 대상입니다. 처음에는 특정 Aggregate 내부에 배치하고, 실제로 공유가 필요해진 시점에 SharedModels/로 이동하세요.

Q4. Fin<T>Validation<Error, T>는 언제 사용하나요?

섹션 제목: “Q4. Fin<T>와 Validation<Error, T>는 언제 사용하나요?”

Fin<T>는 최종 결과(성공 또는 단일 에러)에, Validation<Error, T>는 검증 결과(여러 에러 누적)에 사용합니다. Value Object의 Create()Fin<T>를, Validate()Validation<Error, T>를 반환합니다.

Q5. 다중 Bounded Context로 분리하는 시점은?

섹션 제목: “Q5. 다중 Bounded Context로 분리하는 시점은?”

배포 주기가 다르거나, 팀 소유권이 분리되거나, 동일 용어가 다른 의미로 쓰이거나, 이종 데이터 저장소가 필요한 경우 분리를 검토하세요. 분리 방법은 Context Map 패턴(Shared Kernel, ACL, Published Language 등)을 적용합니다.