본문으로 건너뛰기

Domain Develop

project-spec -> architecture-design -> domain-develop -> application-develop -> adapter-develop -> observability-develop -> test-develop

  • project-spec 스킬에서 생성한 00-project-spec.md가 있으면 자동으로 읽어 Aggregate 후보와 비즈니스 규칙을 확인합니다.
  • architecture-design 스킬에서 생성한 01-architecture-design.md가 있으면 읽어 폴더 구조와 네이밍 규칙을 확인합니다.
  • 선행 문서가 없으면 사용자에게 직접 질문합니다.

DDD 전술적 설계에 따라 도메인 코드를 작성할 때, 반복되는 패턴이 많습니다. Value Object의 Create/Validate/CreateFromValidated 삼중 팩토리, Aggregate Root의 이벤트 발행, 커맨드 메서드의 Fin<Unit> 반환 패턴 등은 빌딩블록마다 동일한 구조를 따릅니다.

/domain-develop 스킬은 이 반복을 자동화합니다. 자연어로 도메인 요구사항을 전달하면, Functorium 프레임워크 패턴에 맞는 코드, 단위 테스트, 문서를 5단계로 생성합니다.

단계작업산출물
1요구사항 분석도메인 모델 분석표, 폴더 구조
2코드 생성VO, Entity, Aggregate, Event, Error, Spec, Service
3단위 테스트 생성T1_T2_T3 명명 규칙, Shouldly 검증
4빌드/테스트 검증dotnet build + dotnet test 통과
5문서 생성 (선택)마크다운 설계 문서
빌딩블록기반 클래스설명
Simple Value ObjectSimpleValueObject<T>단일 원시 값 래핑
Composite Value ObjectValueObject여러 VO 조합
Union Value ObjectUnionValueObject / UnionValueObject<TSelf>허용 상태 조합, 상태 전이
EntityEntity<TId>Aggregate 내 자식 엔티티
Aggregate RootAggregateRoot<TId>트랜잭션 경계
Domain EventDomainEvent상태 변경 알림
Domain ErrorDomainErrorType.Custom비즈니스 규칙 위반
SpecificationExpressionSpecification<T>조회/검색 조건
Domain ServiceIDomainService교차 Aggregate 순수 로직
RepositoryIRepository<T, TId>영속화 인터페이스
패턴사용법
Fin<T> 성공/실패result.IsSucc, result.IsFail
값 추출result.ThrowIfFail()
성공 반환unit (using static LanguageExt.Prelude;)
실패 반환DomainError.For<T>(new ErrorRecord(), id, message)
EntityId 생성{Type}Id.New() (Ulid 기반)
검증 병렬 합성(Fin1, Fin2).Apply((v1, v2) => ...)
상태 전이TransitionFrom<TFrom, TTo>(mapper)

Aggregate 내부에 Child Entity 컬렉션을 관리할 때, IReadOnlyList<T> 공개 + List<T> 내부 패턴을 사용합니다.

private readonly List<OrderItem> _items = new();
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public Fin<Unit> AddItem(OrderItem item)
{
_items.Add(item);
AddDomainEvent(new ItemAddedEvent(Id, item.Id));
return unit;
}
public Fin<Unit> RemoveItem(OrderItemId itemId)
{
var item = _items.FirstOrDefault(i => i.Id == itemId);
if (item is null) return unit; // 멱등성: 없는 항목 제거는 성공
_items.Remove(item);
AddDomainEvent(new ItemRemovedEvent(Id, itemId));
return unit;
}

Application 레이어의 Request/Response DTO에 관측성 전파 어트리뷰트를 적용합니다. 이 어트리뷰트는 observability-develop 스킬에서 설계한 ctx.* 전파 전략을 코드에 반영합니다.

어트리뷰트용도예시
[CtxRoot]ctx.{field} 루트 레벨 승격[CtxRoot] string CustomerId
[CtxTarget(CtxPillar.All)]특정 Pillar에 전파[CtxTarget(CtxPillar.All)] string CustomerTier
[CtxIgnore]모든 Pillar에서 제외[CtxIgnore] string InternalMemo
/domain-develop Product aggregate with ProductName (max 100 chars), ProductPrice (positive decimal)

인자 없이 /domain-develop만 호출하면, 스킬이 대화형으로 요구사항을 수집합니다.

  1. 분석 결과 제시 — Aggregate, Value Object, Event, Error 등 식별 결과를 표로 보여줍니다
  2. 사용자 확인 — 분석 결과를 확인한 후 코드 생성으로 진행합니다
  3. 코드 + 테스트 생성 — 빌딩블록별로 코드와 테스트를 생성합니다
  4. 빌드/테스트 검증dotnet builddotnet test를 실행하여 통과를 확인합니다

예제 1: 기초 — Aggregate와 Value Object

섹션 제목: “예제 1: 기초 — Aggregate와 Value Object”

가장 기본적인 Aggregate 패턴입니다. 단일 원시 값을 감싸는 Simple Value Object 두 개와, 이를 속성으로 가지는 Aggregate Root를 생성합니다. Functorium 도메인 개발의 출발점이 되는 구조입니다.

/domain-develop Product aggregate with ProductName (max 100 chars), ProductPrice (positive decimal).
Create, UpdateName, UpdatePrice 커맨드 메서드
빌딩블록타입설명
Simple VOProductNameSimpleValueObject<string>, 100자 제한
Simple VOProductPriceSimpleValueObject<decimal>, 양수 검증
AggregateProductAggregateRoot<ProductId>, 3개 커맨드 메서드
Domain EventCreatedEvent, NameUpdatedEvent, PriceUpdatedEvent상태 변경 알림
단위 테스트약 36개VO 검증 + Aggregate 커맨드 + FinApply

Simple Value Object — 단일 원시 값 래핑, Create/Validate/CreateFromValidated 삼중 팩토리:

public sealed class ProductName : SimpleValueObject<string>
{
public const int MaxLength = 100;
private ProductName(string value) : base(value) { }
public static Fin<ProductName> Create(string? value) =>
CreateFromValidation(Validate(value), v => new ProductName(v));
public static Validation<Error, string> Validate(string? value) =>
ValidationRules<ProductName>
.NotNull(value)
.ThenNotEmpty()
.ThenMaxLength(MaxLength)
.ThenNormalize(v => v.Trim());
}

커맨드 메서드Fin<Unit> 반환, 상태 변경 + 이벤트 발행:

public Fin<Unit> UpdateName(ProductName newName, DateTime now)
{
Name = newName;
UpdatedAt = now;
AddDomainEvent(new NameUpdatedEvent(Id, newName));
return unit;
}

예제 2: 중급 — Union 타입과 Child Entity

섹션 제목: “예제 2: 중급 — Union 타입과 Child Entity”

예제 1에 세 가지 패턴을 추가합니다. 상태 전이를 타입으로 인코딩하는 Union Value Object, 여러 VO를 조합하는 Composite Value Object, Aggregate 내부에 소속되는 Child Entity입니다. 이 세 패턴이 결합되면, “Pending 주문만 확인 가능”이나 “배송 주소는 4개 필드가 모두 유효해야 생성 가능” 같은 규칙을 컴파일 타임에 보장할 수 있습니다.

/domain-develop Order aggregate with OrderStatus union
(Pending → Confirmed → Shipped → Cancelled 상태 전이),
ShippingAddress composite VO (Street, City, State, ZipCode),
OrderItem child entity (ProductName, Quantity, UnitPrice).
Confirm과 Ship 커맨드 메서드
빌딩블록타입설명
Union VOOrderStatusUnionValueObject<OrderStatus>, 4개 상태, 전이 메서드
Composite VOShippingAddressValueObject, 4개 하위 VO 조합
Child EntityOrderItemEntity<OrderItemId>, 부모 Aggregate 통해 관리
AggregateOrderAggregateRoot<OrderId>, 상태 전이 기반 커맨드

Union Value Object 상태 전이TransitionFrom으로 허용된 전이만 성공, 나머지는 InvalidTransition 에러 반환:

[UnionType]
public abstract partial record OrderStatus : UnionValueObject<OrderStatus>
{
public sealed record Pending(DateTime CreatedAt) : OrderStatus;
public sealed record Confirmed(DateTime ConfirmedAt) : OrderStatus;
public sealed record Shipped(DateTime ShippedAt) : OrderStatus;
public sealed record Cancelled(DateTime CancelledAt) : OrderStatus;
private OrderStatus() { }
public Fin<Confirmed> Confirm(DateTime now) =>
TransitionFrom<Pending, Confirmed>(
_ => new Confirmed(now));
public Fin<Shipped> Ship(DateTime now) =>
TransitionFrom<Confirmed, Shipped>(
_ => new Shipped(now));
}

Composite Value Object — 하위 VO의 Validate.Apply()로 병렬 합성하여 에러를 누적 수집:

public sealed class ShippingAddress : ValueObject
{
public Street Street { get; }
public City City { get; }
public State State { get; }
public ZipCode ZipCode { get; }
private ShippingAddress(Street street, City city, State state, ZipCode zipCode)
{
Street = street;
City = city;
State = state;
ZipCode = zipCode;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return State;
yield return ZipCode;
}
public static Validation<Error, (string, string, string, string)> Validate(
string? street, string? city, string? state, string? zipCode) =>
(Street.Validate(street), City.Validate(city),
State.Validate(state), ZipCode.Validate(zipCode))
.Apply((s, c, st, z) => (s, c, st, z));
public static Fin<ShippingAddress> Create(
string? street, string? city, string? state, string? zipCode) =>
CreateFromValidation<ShippingAddress, (string, string, string, string)>(
Validate(street, city, state, zipCode),
v => new ShippingAddress(
Street.CreateFromValidated(v.Item1),
City.CreateFromValidated(v.Item2),
State.CreateFromValidated(v.Item3),
ZipCode.CreateFromValidated(v.Item4)));
}

Child EntityEntity<TId> 상속, 이벤트 발행 없이 부모 Aggregate를 통해 관리:

[GenerateEntityId]
public sealed class OrderItem : Entity<OrderItemId>
{
public ProductName ProductName { get; }
public Quantity Quantity { get; private set; }
public UnitPrice UnitPrice { get; }
private OrderItem(OrderItemId id, ProductName name, Quantity qty, UnitPrice price)
: base(id)
{
ProductName = name;
Quantity = qty;
UnitPrice = price;
}
public static OrderItem Create(ProductName name, Quantity qty, UnitPrice price) =>
new(OrderItemId.New(), name, qty, price);
}

예제 3: 고급 — Domain Service와 Specification

섹션 제목: “예제 3: 고급 — Domain Service와 Specification”

Aggregate 하나로 해결할 수 없는 로직이 등장합니다. Specification은 조회 조건을 객체로 캡슐화하고, Domain Service는 여러 Aggregate에 걸친 순수 로직을 담습니다. 이 두 패턴은 Aggregate의 경계를 존중하면서 교차 관심사를 처리하는 방법입니다.

/domain-develop Inventory aggregate with StockQuantity VO (non-negative int),
LowStockThreshold VO (positive int).
InventoryLowStockSpec specification (재고가 임계값 이하),
InventoryTransferService domain service (source → target 재고 이동).
Restock, Transfer 커맨드 메서드
빌딩블록타입설명
Simple VOStockQuantitySimpleValueObject<int>, 0 이상
Simple VOLowStockThresholdSimpleValueObject<int>, 양수
SpecificationInventoryLowStockSpecExpressionSpecification<Inventory>, 재고 ≤ 임계값
Domain ServiceInventoryTransferServiceIDomainService, 재고 부족 시 실패 반환
AggregateInventoryAggregateRoot<InventoryId>, Restock/Deduct 커맨드

Specification — VO를 primitive로 변환하여 Expression 클로저에 캡처, EF Core 쿼리 변환 가능:

public sealed class InventoryLowStockSpec : ExpressionSpecification<Inventory>
{
public LowStockThreshold Threshold { get; }
public InventoryLowStockSpec(LowStockThreshold threshold) =>
Threshold = threshold;
public override Expression<Func<Inventory, bool>> ToExpression()
{
int thresholdValue = Threshold;
return inventory => inventory.StockQuantity <= thresholdValue;
}
}

Domain Service — 상태 없는 순수 함수, 교차 Aggregate 로직을 Fin<Unit>으로 표현:

public sealed class InventoryTransferService : IDomainService
{
public sealed record InsufficientStock : DomainErrorType.Custom;
public Fin<Unit> Transfer(
Inventory source, Inventory target, StockQuantity amount, DateTime now)
{
if (source.StockQuantity < amount)
return DomainError.For<InventoryTransferService>(
new InsufficientStock(),
source.Id.ToString(),
$"재고 부족: 현재 {(int)source.StockQuantity}, 요청 {(int)amount}");
source.Deduct(amount, now);
target.Restock(amount, now);
return unit;
}
}

예제 4: 실전 — 연락처 도메인 전체 구현

섹션 제목: “예제 4: 실전 — 연락처 도메인 전체 구현”

앞의 세 예제에서 다룬 모든 패턴이 하나의 도메인에 결합됩니다. 타입으로 도메인 설계하기 예제의 비즈니스 요구사항을 그대로 프롬프트로 전달하여, 전체 도메인 모델을 자동 생성합니다. 9개 Value Object, 2개 Union, 1개 Child Entity, 1개 Aggregate, 1개 Domain Service, 1개 Specification — 그리고 114개 단위 테스트까지 한 번의 스킬 호출로 만들어냅니다.

/domain-develop Contact aggregate 연락처 관리 도메인을 구현해줘.
## 연락처 정보 구성
- 개인 이름: 이름(First Name, 필수), 성(Last Name, 필수), 중간 이니셜(Middle Initial, 선택)
- 이메일 주소: 표준 이메일 형식
- 우편 주소: 주소(Address), 도시(City), 주 코드(State, 2자리 대문자), 우편번호(Zip, 5자리 숫자)
- 메모: 자유 형식 텍스트, 500자 이하
## 비즈니스 규칙
1. 데이터 유효성: 이름/성 50자 이하, 이메일 표준 형식, 주 코드 2자리 대문자, 우편번호 5자리 숫자
2. 연락 수단: 최소 하나 필수 (이메일만 / 우편 주소만 / 둘 다). 연락 수단 없는 연락처는 존재 불가
3. 이메일 인증: 미인증 → 인증 단방향 전이. 인증 시점 기록. 이미 인증된 이메일 재인증 불가
4. 연락처 수명: 이름 변경, 논리 삭제(삭제자+시점), 복원 가능. 삭제된 연락처 수정 불가. 삭제/복원 멱등
5. 메모 관리: 추가/제거 가능, 삭제된 연락처 불가, 존재하지 않는 메모 제거 멱등
6. 이메일 고유성: 중복 불가, 자기 자신 제외
## 존재불가 상태
- 이메일 없는데 인증된 상태
- 연락 수단 없는 연락처
- 인증 → 미인증 역전
- 삭제된 연락처에서 행위 수행
- 동일 이메일 중복 연락처

이 프롬프트는 타입으로 도메인 설계하기 예제와 동일한 도메인 모델을 생성합니다:

빌딩블록타입설명
Simple VO (9개)FirstName, LastName, MiddleInitial, EmailAddress, Street, City, StateCode, ZipCode, NoteContent원시 값 검증
Composite VO (2개)PersonalName, PostalAddressVO 조합
Union VO (2개)ContactInfo (이메일만/우편주소만/둘다), EmailStatus (미인증/인증)허용 상태 조합, 단방향 전이
Child EntityContactNote메모 관리
AggregateContactCreate, UpdateName, VerifyEmail, SoftDelete, Restore, AddNote, RemoveNote
Domain ServiceContactEmailUniquenessService이메일 고유성 검증
SpecificationContactByEmailSpec이메일 기반 조회
단위 테스트약 114개비즈니스 규칙 6개 그룹 + 시나리오 10개 전체 검증

이 예제는 단순한 CRUD가 아닙니다. “연락 수단 없는 연락처는 존재할 수 없다”는 규칙은 Union 타입으로, “인증은 단방향”이라는 규칙은 상태 전이로, “삭제된 연락처 수정 불가”는 Aggregate 가드 조건으로 각각 타입 시스템에 인코딩됩니다. 잘못된 상태가 컴파일 타임에 차단되는 것을 확인하려면, 타입으로 도메인 설계하기 예제의 전체 설계 과정을 참조하십시오.