8.0 KiB
8.0 KiB
id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
| id | title | category | status | canonical_id | aliases | duplicate_of | source_trust_level | confidence_score | verification_status | tags | raw_sources | last_reinforced | github_commit | tech_stack | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| wiki-2026-0508-encapsulation-of-domain-invarian | Encapsulation of Domain Invariants | 10_Wiki/Topics | verified | self |
|
none | A | 0.93 | applied |
|
2026-05-10 | pending |
|
Encapsulation of Domain Invariants
매 한 줄
"매 business rule 의 type / object 의 enforce". Evans DDD. 매 'parse, don't validate' (Wlaschin). 매 invariant = 매 always true. 매 value object + aggregate 의 의 매 protect. 매 modern: 매 branded type + Zod + Result type.
매 핵심
매 invariant type
- Class invariant: 매 instance 의 always true.
- Aggregate invariant: 매 group 의 consistent.
- System invariant: 매 cross-aggregate.
- Temporal invariant: 매 lifecycle 의 phase.
매 'Parse, don't validate'
- 매 raw input → 매 typed value object (parse).
- 매 type 의 의 의 의 invariant 의 carry.
- 매 every code 매 validate 의 X.
매 응용
- Email / phone: 매 type-safe value.
- Money: 매 amount + currency.
- Identifier: 매 UserId vs OrderId.
- Range: 매 age 0-150.
- Enum: 매 Status (limited).
- Aggregate: 매 Order + LineItems consistent.
매 modern variant
- TypeScript branded type.
- Zod runtime parse.
- Result / Either type.
- Smart constructor.
- Refinement type (advanced).
💻 패턴
Value object (TypeScript)
class Email {
private constructor(private readonly value: string) {}
static parse(raw: string): Email | Error {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw)) return new Error('Invalid email');
return new Email(raw.toLowerCase().trim());
}
toString() { return this.value; }
equals(o: Email) { return this.value === o.value; }
}
Branded type
type Email = string & { readonly __brand: 'Email' };
type UserId = string & { readonly __brand: 'UserId' };
function parseEmail(raw: string): Email {
if (!isValidEmail(raw)) throw new Error('Invalid');
return raw as Email;
}
// 매 type-level safety
function send(to: Email) { ... }
send(userId); // ❌ Type error
send(parseEmail('a@b.com')); // ✅
Money (currency-aware)
class Money {
private constructor(
public readonly amount: number, // 매 cents (integer)
public readonly currency: 'USD' | 'EUR' | 'JPY',
) {}
static of(amount: number, currency: Money['currency']): Money {
if (!Number.isInteger(amount)) throw new Error('Use cents');
if (amount < 0) throw new Error('Negative not allowed');
return new Money(amount, currency);
}
plus(other: Money): Money {
if (this.currency !== other.currency) throw new Error('Mixed currency');
return new Money(this.amount + other.amount, this.currency);
}
}
Aggregate (DDD Order)
class Order {
private constructor(
private id: OrderId,
private items: LineItem[],
private status: OrderStatus,
) {
this.checkInvariants();
}
private checkInvariants() {
if (this.items.length === 0 && this.status === 'PLACED') {
throw new Error('Placed order must have items');
}
const total = this.totalCents();
if (total < 0) throw new Error('Invariant: non-negative total');
}
addItem(item: LineItem) {
if (this.status !== 'DRAFT') throw new Error('Cannot add to non-draft');
this.items.push(item);
this.checkInvariants();
}
place() {
if (this.items.length === 0) throw new Error('Empty order');
this.status = 'PLACED';
this.checkInvariants();
}
}
Zod (runtime parse)
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid().brand('UserId'),
email: z.string().email(),
age: z.number().int().min(0).max(150),
});
type User = z.infer<typeof UserSchema>;
function fromInput(raw: unknown): User {
return UserSchema.parse(raw); // 매 throws if invalid
}
Result type (no exceptions)
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
class Email {
static parse(raw: string): Result<Email, string> {
if (!isValid(raw)) return { ok: false, error: 'Invalid email' };
return { ok: true, value: new Email(raw) };
}
}
const r = Email.parse(input);
if (!r.ok) return alert(r.error);
send(r.value);
Smart constructor (Python)
from dataclasses import dataclass
@dataclass(frozen=True)
class Email:
value: str
@classmethod
def create(cls, raw: str) -> 'Email':
if '@' not in raw: raise ValueError('Invalid email')
return cls(value=raw.lower().strip())
# 매 Pydantic v2
from pydantic import BaseModel, EmailStr, Field
class User(BaseModel):
email: EmailStr
age: int = Field(ge=0, le=150)
Range type
class Age {
private constructor(public readonly value: number) {}
static of(n: number): Age {
if (!Number.isInteger(n) || n < 0 || n > 150) throw new Error('Invalid age');
return new Age(n);
}
}
State machine (lifecycle)
type DraftOrder = { status: 'DRAFT'; items: LineItem[] };
type PlacedOrder = { status: 'PLACED'; items: LineItem[]; placedAt: Date };
type ShippedOrder = { status: 'SHIPPED'; items: LineItem[]; shippedAt: Date };
type Order = DraftOrder | PlacedOrder | ShippedOrder;
function place(o: DraftOrder): PlacedOrder {
if (o.items.length === 0) throw new Error('Empty');
return { status: 'PLACED', items: o.items, placedAt: new Date() };
}
// 매 type 의 transition 의 enforce
Aggregate transactional boundary
async function placeOrder(orderId: OrderId) {
await db.transaction(async tx => {
const order = await tx.order.find(orderId); // 매 lock
order.place(); // 매 invariant check
await tx.order.save(order);
});
}
Refinement check (advanced)
// 매 TypeScript 4.9+ — satisfies
const config = {
retries: 3,
timeout: 5000,
} satisfies { retries: number; timeout: number };
Avoid primitive obsession
// 매 ❌
function transfer(from: string, to: string, amount: number, currency: string) { ... }
transfer('user1', '5000', 'EUR', 100); // 매 args swapped!
// 매 ✅
function transfer(from: UserId, to: UserId, amount: Money) { ... }
// 매 type system 의 prevent
매 결정 기준
| 상황 | Approach |
|---|---|
| Primitive obsession | Branded / value object |
| Complex constraint | Smart constructor + Result |
| Schema input | Zod / Pydantic |
| Multi-field rule | Aggregate |
| Lifecycle | State machine type |
| Currency / measure | Value object with unit |
기본값: 매 input boundary parse + 매 branded ID + 매 value object for measures + 매 aggregate for invariant + 매 state-machine type for lifecycle.
🔗 Graph
- 부모: Domain-Driven-Design · Encapsulation-and-Information-Hiding
- 변형: Value-Object · Aggregate · Smart-Constructor
- 응용: Type-Driven-Design · Branded-Types
- Adjacent: Anaemic Domain Model · Parse-Dont-Validate · Refinement-Type · Result-Type
🤖 LLM 활용
언제: 매 domain-rich. 매 type-safe API. 매 critical invariant. 언제 X: 매 CRUD-only. 매 throwaway.
❌ 안티패턴
- Primitive obsession: 매 string 매 string.
- Validate everywhere: 매 once parse → trust type.
- Anemic value object: 매 getter/setter only.
- Cross-aggregate transaction: 매 boundary 의 violate.
- Nullable invariant: 매 if-checking 의 cascade.
🧪 검증 / 중복
- Verified (Evans DDD, Wlaschin Domain Modeling, Pydantic v2).
- 신뢰도 A.
🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-04-20 | Auto-reinforced |
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — DDD invariant + 매 value object / branded / Zod / Result / state code |