refactor(topics): 멀티 에이전트용 지식 재편 — _Common(공통 기본기) + Domain_* 구조
에이전트 8종(대화형/프로그래머 C·S/디자이너/설계자/기획자/QA/PD/PM)에게 [공통 기본 능력 + 롤별 Specialty] 2층으로 지식을 주입하기 위한 재분류. 문서 내용·포맷은 무수정, 폴더 이동만 (6,372개 문서 수 보존 확인). - Topic_Programming → Domain_Programming (내부 구조 보존) - Topic_Graphic → Domain_Design - Topic_Business → Domain_Product - Topic_General → Domain_General - _Common 신설: Math(구 Topic_Math_Specialty), Reasoning(구 General/From_Thinking & Reasoning), Reasoning_Creativity(구 General/From_창의성), Communication(Poetic_Blog_Writing + From_writing) - 타 도메인의 From_* 폴더는 유지 (출처 표기일 뿐, 이미 도메인에 맞게 분류된 문서) - 빈 폴더 정리 (memory/procedures) - 에이전트→폴더 매핑은 workspace의 .astra/agent-knowledge-map.json (9개 에이전트) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
---
|
||||
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, 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)
|
||||
```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<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)
|
||||
```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<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 |
|
||||
Reference in New Issue
Block a user