--- id: wiki-2026-0508-견고한-도메인-모델-및-api-계약-설계 title: 견고한 도메인 모델 및 API 계약 설계 category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Domain Model Design, API Contract Design, DDD] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [ddd, domain-model, api-design, contract, type-driven] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript/Java framework: Zod/Pydantic/OpenAPI --- # 견고한 도메인 모델 및 API 계약 설계 ## 매 한 줄 > **"매 type-driven domain model + 매 versioned API contract 의 결합으로 매 invalid state 를 unrepresentable 로 만든다"**. 매 DDD aggregate + value object + Zod/Pydantic schema + OpenAPI contract — 매 2024 modern stack. ## 매 핵심 ### 매 도메인 모델 의 견고함 - **Value Object**: 매 immutable + equality by value (Money, Email, OrderId). - **Entity / Aggregate Root**: 매 identity + lifecycle + invariant 보호. - **Make illegal states unrepresentable**: 매 sum type / discriminated union 사용. - **Smart Constructor**: 매 raw type → validated branded type 의 single entry. ### 매 API 계약 의 견고함 - **Schema-first**: OpenAPI / JSON Schema / GraphQL SDL — 매 source of truth. - **Backward compat**: additive change only, 매 deprecation header, version in URL/header. - **Idempotency key**: 매 mutation 의 retry-safe 화. - **Pagination + filter + sort 표준화**: cursor-based >>> offset-based. ### 매 응용 1. e-commerce checkout (Money, Cart, Order aggregate). 2. Banking (Account, Transaction, immutable ledger). 3. Multi-tenant SaaS (Tenant 의 invariant 격리). 4. Public API (Stripe-style versioned contract). ## 💻 패턴 ### 매 Branded type (TS) — primitive obsession 의 회피 ```typescript type Brand = T & { __brand: B }; type Email = Brand; type UserId = Brand; const parseEmail = (s: string): Email => { if (!/^[^@]+@[^@]+$/.test(s)) throw new Error("Invalid email"); return s as Email; }; function send(to: Email, msg: string) { /* ... */ } send("not-email" as Email, "x"); // unsafe cast 만 통과 — 매 explicit send(parseEmail("a@b.com"), "x"); // safe path ``` ### Value Object (Money) ```typescript class Money { private constructor(readonly amount: bigint, readonly currency: string) {} static of(amount: number, currency: string) { if (!Number.isFinite(amount)) throw new Error("non-finite"); return new Money(BigInt(Math.round(amount * 100)), currency); } add(other: Money) { if (this.currency !== other.currency) throw new Error("currency mismatch"); return new Money(this.amount + other.amount, this.currency); } equals(other: Money) { return this.amount === other.amount && this.currency === other.currency; } } ``` ### 매 illegal state unrepresentable (TS discriminated union) ```typescript type OrderState = | { kind: "draft"; cart: CartLine[] } | { kind: "placed"; orderId: OrderId; placedAt: Date } | { kind: "shipped"; orderId: OrderId; tracking: string } | { kind: "cancelled"; reason: string }; function summary(o: OrderState) { switch (o.kind) { case "draft": return `Draft (${o.cart.length} items)`; case "placed": return `Placed ${o.orderId}`; case "shipped": return `Shipped ${o.tracking}`; case "cancelled": return `Cancelled: ${o.reason}`; } } ``` ### Zod schema (runtime + static type) ```typescript import { z } from "zod"; const CreateOrderSchema = z.object({ customerId: z.string().uuid(), items: z.array(z.object({ sku: z.string().min(1), qty: z.number().int().positive().max(1000) })).min(1), idempotencyKey: z.string().uuid() }); type CreateOrderReq = z.infer; app.post("/orders", (req, res) => { const parsed = CreateOrderSchema.safeParse(req.body); if (!parsed.success) return res.status(400).json({ errors: parsed.error.issues }); // parsed.data is fully typed }); ``` ### Aggregate Root (invariant protection) ```typescript class Order { private constructor( readonly id: OrderId, private state: OrderState, private lines: OrderLine[] ) {} addLine(line: OrderLine) { if (this.state.kind !== "draft") throw new Error("cannot modify placed order"); if (this.lines.length >= 50) throw new Error("max 50 lines"); this.lines.push(line); } place(): void { if (this.lines.length === 0) throw new Error("empty cart"); this.state = { kind: "placed", orderId: this.id, placedAt: new Date() }; } } ``` ### OpenAPI 의 versioned contract ```yaml # openapi.yaml openapi: 3.1.0 info: { title: Orders API, version: "2026-05-01" } paths: /v1/orders: post: parameters: - in: header name: Idempotency-Key required: true schema: { type: string, format: uuid } requestBody: content: application/json: schema: { $ref: '#/components/schemas/CreateOrder' } responses: '201': { $ref: '#/components/responses/Order' } '409': { description: Idempotency conflict } ``` ### Cursor pagination ```typescript type Page = { items: T[]; nextCursor: string | null }; async function listOrders(cursor?: string): Promise> { const rows = await db.query( "SELECT * FROM orders WHERE id > $1 ORDER BY id LIMIT 51", [cursor ?? ""] ); const hasMore = rows.length === 51; const items = rows.slice(0, 50); return { items: items.map(toDto), nextCursor: hasMore ? items[items.length - 1].id : null }; } ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | 매 simple CRUD | DTO + validator (Zod/Pydantic) — DDD overkill | | 매 complex domain (banking, scheduling) | Aggregate + VO + invariant | | 매 public API | OpenAPI contract-first + version | | 매 internal RPC | gRPC / tRPC + protobuf / TS infer | | 매 event-driven | event schema (Avro/protobuf) + registry | **기본값**: 매 schema-first (Zod / OpenAPI) + branded ID + idempotency + cursor pagination. ## 🔗 Graph - 부모: [[Domain-Driven Design]] · [[API Design]] - 변형: [[Hexagonal Architecture]] · [[Event Sourcing]] · [[CQRS]] - Adjacent: [[OpenAPI]] · [[gRPC]] ## 🤖 LLM 활용 **언제**: schema generation, contract review, illegal-state 발견, migration 전략. **언제 X**: 매 internal throwaway script — 매 over-engineering. ## ❌ 안티패턴 - **Primitive Obsession**: string everywhere — UserId 와 OrderId 의 swap silent. - **Anemic Domain**: getter/setter 만, business logic 의 service 누설. - **Shared Mutable Aggregate**: 매 invariant 의 broken. - **Breaking change in v1 endpoint**: 매 client 의 parallel 운영 X. ## 🧪 검증 / 중복 - Verified (Evans *DDD* 2003, Vernon *Implementing DDD* 2013, Stripe API design guide 2024). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — branded types + Zod + OpenAPI + aggregate patterns |