--- id: wiki-2026-0508-외부-라이브러리-api-설계 title: 외부 라이브러리 API 설계 category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Library API Design, Public API Design, SDK Design] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [api-design, library-design, sdk, dx] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: typescript framework: none --- # 외부 라이브러리 API 설계 ## 매 한 줄 > **"매 public surface는 영원하다"**. 외부 라이브러리(npm, PyPI, Maven 배포물)의 public API는 한 번 release되면 호환성 부담을 평생 짊어진다 — Joshua Bloch의 *"API design is hard, and the consequences of failure are forever"*. Hyrum's Law 전제 하에 minimal surface + clear versioning + predictable failure를 설계. ## 매 핵심 ### 매 5 원칙 - **Easy to use, hard to misuse** (Bloch): default가 옳고, 잘못된 사용은 type error로 차단. - **Minimal surface**: 의심스러우면 빼라 — 추가는 쉽고 제거는 breaking change. - **Orthogonal**: 한 기능 = 한 방법. 두 가지가 같은 일을 하면 사용자 confusion + maintenance 부담. - **Failure mode 명확**: throw vs Result, sync vs async, retry semantics — 문서가 아닌 type으로. - **SemVer 엄수**: minor에서 breaking 금지. CI에 API diff 검사. ### 매 layered API - **Core (low-level)**: 모든 capability 노출, escape hatch 제공. - **Sugar (high-level)**: 80% use case 한 줄 처리. Core 위에 build. - 두 layer를 분리하면 power user와 casual user 모두 만족. ### 매 응용 1. **HTTP client**: `fetch`-style core + `client.get(path, opts)` sugar. 2. **DB driver**: raw query + query builder + ORM (3 layer). 3. **AI SDK**: `messages.create` core + `messages.stream` + agent helper. ## 💻 패턴 ### Tagged result로 failure mode를 type에 박기 ```typescript // 안티: throw + any error type export async function fetchUser(id: string): Promise { ... } // 좋음: Result — 호출자가 처리 강제 export type Result = | { ok: true; value: T } | { ok: false; error: E }; export type FetchError = | { kind: 'not_found' } | { kind: 'unauthorized' } | { kind: 'network'; cause: Error }; export async function fetchUser(id: string): Promise> { // ... } // 사용자 코드 const r = await fetchUser('u1'); if (!r.ok) { switch (r.error.kind) { // 컴파일러가 exhaustive 검사 case 'not_found': return notFound(); case 'unauthorized': return redirect(); case 'network': return retry(); } } ``` ### Builder for many optional knobs ```typescript // 안티: optional 폭발 client.query('SELECT ...', undefined, undefined, 30_000, true, 'replica'); // 좋음: builder client.query('SELECT ...') .timeout(30_000) .readReplica() .stream() .execute(); ``` ### Branded types로 misuse 차단 ```typescript type UserId = string & { readonly __brand: 'UserId' }; type OrderId = string & { readonly __brand: 'OrderId' }; export const UserId = (s: string): UserId => s as UserId; export const OrderId = (s: string): OrderId => s as OrderId; export function getUser(id: UserId): Promise { ... } getUser(orderId); // ❌ 컴파일 에러 — UserId가 아님 getUser(UserId('u_42')); // ✅ ``` ### Default + override ```typescript export interface ClientOptions { baseUrl?: string; // default: 'https://api.example.com' timeoutMs?: number; // default: 30_000 fetch?: typeof globalThis.fetch; retry?: RetryPolicy | false; } const DEFAULTS = { baseUrl: 'https://api.example.com', timeoutMs: 30_000, fetch: globalThis.fetch.bind(globalThis), retry: { attempts: 3, backoff: 'exponential' } as RetryPolicy, }; export class Client { private readonly opts: Required; constructor(opts: ClientOptions = {}) { this.opts = { ...DEFAULTS, ...opts }; } } ``` ### `@deprecated` + transition path ```typescript /** * @deprecated since 2.3.0 — use {@link createClient} which returns a Result. * Will be removed in 3.0. */ export function newClient(opts: ClientOptions): Client { ... } export function createClient( opts: ClientOptions, ): Result { ... } ``` ### Public surface diff in CI (api-extractor) ```jsonc // api-extractor.json { "mainEntryPointFilePath": "/dist/index.d.ts", "apiReport": { "enabled": true, "reportFolder": "etc/" } } ``` ```bash # CI: 변경 시 api report diff → PR에서 reviewer가 명시적 승인 api-extractor run --local && git diff --exit-code etc/ ``` ### Stable v1 export root ```typescript // src/index.ts — public re-export only export { Client } from './client'; export type { ClientOptions, Result, FetchError } from './types'; // 내부 구현은 export 금지 — 사용자가 deep import 하면 lock-in ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | Library 내부 use, 사용자 < 10 | 가볍게 — minimal versioning | | OSS 공개, 사용자 unknown | 매 strict — SemVer + API diff CI | | Experimental feature | `unstable_` prefix + `@experimental` | | Breaking change 필요 | major version + codemod 제공 | **기본값**: public export는 `index.ts` 하나로 묶고, 모든 그 외 path는 internal로 간주. ## 🔗 Graph - 부모: [[API Design]] · [[Library Design]] - 변형: [[SDK Design]] · [[Public API]] - Adjacent: [[Backward Compatibility]] ## 🤖 LLM 활용 **언제**: 신규 라이브러리 export root 설계, deprecation 전략 수립, API diff PR 리뷰. **언제 X**: 단일 앱 내부 모듈 — 그 정도 ceremony는 과잉. ## ❌ 안티패턴 - **모든 internal export**: 사용자가 deep import → 내부 변경 = breaking change. - **boolean flag 폭발**: `fn(x, true, false, true)` — 의미 불명. Object option으로. - **Throw + 문서로만 명시**: type system이 강제하지 않으면 무시됨. - **Minor에서 behavior 변경**: SemVer 위반 — 신뢰 파괴. ## 🧪 검증 / 중복 - Verified (Joshua Bloch, "How to Design a Good API and Why It Matters"; Microsoft API Design Guidelines). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — Bloch 원칙·layered API·CI diff 패턴 |