f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.9 KiB
6.9 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-견고한-도메인-모델-및-api-계약-설계 | 견고한 도메인 모델 및 API 계약 설계 | 10_Wiki/Topics | verified | self |
|
none | A | 0.9 | applied |
|
2026-05-10 | pending |
|
견고한 도메인 모델 및 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.
매 응용
- e-commerce checkout (Money, Cart, Order aggregate).
- Banking (Account, Transaction, immutable ledger).
- Multi-tenant SaaS (Tenant 의 invariant 격리).
- Public API (Stripe-style versioned contract).
💻 패턴
매 Branded type (TS) — primitive obsession 의 회피
type Brand<T, B> = T & { __brand: B };
type Email = Brand<string, "Email">;
type UserId = Brand<string, "UserId">;
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)
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)
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)
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<typeof CreateOrderSchema>;
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)
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
# 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
type Page<T> = { items: T[]; nextCursor: string | null };
async function listOrders(cursor?: string): Promise<Page<OrderDto>> {
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 |