Files
2nd/10_Wiki/Topics/Domain_Programming/Programming & Language/견고한 도메인 모델 및 API 계약 설계.md
T
Antigravity Agent c24165b8bc 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>
2026-07-11 11:05:56 +09:00

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
Domain Model Design
API Contract Design
DDD
none A 0.9 applied
ddd
domain-model
api-design
contract
type-driven
2026-05-10 pending
language framework
TypeScript/Java 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 의 회피

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