OpenAPI 스펙에서 타입 안전 SDK를 자동 생성하고, API 문서화, 인터랙티브 플레이그라운드로 개발자 경험을 최적화하는 방법을 학습합니다.
AI API를 직접 HTTP로 호출하는 것은 가능하지만, 개발자에게 최적의 경험은 아닙니다. 잘 설계된 SDK는 타입 안전성, 자동 재시도, 스트리밍 처리, 인증 관리를 추상화하여 개발 생산성을 극대화합니다.
// 1. Raw HTTP 호출 — 장황하고 오류 가능성 높음
const response = await fetch("https://api.example.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
"X-API-Version": "2026-03-01",
},
body: JSON.stringify({
model: "claude-4",
messages: [{ role: "user", content: "Hello" }],
max_tokens: 1024,
}),
});
if (!response.ok) {
// 에러 처리 직접 구현 필요
const error = await response.json();
throw new Error(error.error.message);
}
const data = await response.json();
// data는 any 타입 — 타입 안전성 없음
// 2. SDK 사용 — 간결하고 타입 안전
const message = await client.chat.completions.create({
model: "claude-4",
messages: [{ role: "user", content: "Hello" }],
maxTokens: 1024,
});
// message는 완전한 타입 정보를 가진 ChatCompletion 객체Stainless는 OpenAI와 Anthropic이 공식 SDK를 생성하는 데 사용하는 도구입니다. OpenAPI 스펙을 입력으로 받아 Python, TypeScript, Java, Go, Ruby SDK를 생성합니다.
# .stainless.yml 설정 파일
organization:
name: example-ai
docs_url: https://docs.example.com
resources:
chat:
completions:
methods:
create:
path: /v1/chat/completions
method: POST
sdk:
package_name:
python: example-ai
typescript: example-ai
java: com.example.ai
go: github.com/example/ai-go
features:
- streaming
- pagination
- retries
- timeout
python:
project_name: example-ai-python
pypi_name: example-ai
typescript:
project_name: example-ai-node
npm_name: example-aiStainless가 생성하는 SDK의 주요 특성입니다.
import ExampleAI from "example-ai";
const client = new ExampleAI({
apiKey: process.env.EXAMPLE_AI_API_KEY,
// 자동 재시도 설정
maxRetries: 3,
// 타임아웃
timeout: 60_000,
});
// 동기 완성
const completion = await client.chat.completions.create({
model: "claude-4",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "API 설계 팁을 알려주세요" },
],
maxTokens: 1024,
});
console.log(completion.choices[0].message.content);
// 스트리밍 완성
const stream = await client.chat.completions.create({
model: "claude-4",
messages: [{ role: "user", content: "API 설계 팁을 알려주세요" }],
maxTokens: 1024,
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}Speakeasy는 엔터프라이즈 환경에 초점을 맞춘 SDK 생성 도구입니다. OpenAPI 스펙의 품질을 분석하고, 문서, 샘플 코드까지 함께 생성합니다.
# SDK 생성
speakeasy generate sdk \
--schema openapi.yaml \
--lang typescript \
--out ./sdks/typescript
# OpenAPI 스펙 품질 분석
speakeasy validate openapi \
--schema openapi.yaml
# 변경 사항 비교
speakeasy suggest openapi \
--schema openapi.yaml오픈소스 프로젝트로, 40개 이상의 언어를 지원합니다. 커스터마이징 자유도가 높지만, 생성된 코드의 품질이 상용 도구에 비해 다소 낮을 수 있습니다.
# Docker로 실행
docker run --rm \
-v "${PWD}:/local" \
openapitools/openapi-generator-cli generate \
-i /local/openapi.yaml \
-g typescript-fetch \
-o /local/sdks/typescript \
--additional-properties=supportsES6=true,npmName=example-ai
# Python SDK 생성
docker run --rm \
-v "${PWD}:/local" \
openapitools/openapi-generator-cli generate \
-i /local/openapi.yaml \
-g python \
-o /local/sdks/python \
--additional-properties=packageName=example_ai| 특성 | Stainless | Speakeasy | openapi-generator |
|---|---|---|---|
| 지원 언어 | 5개 (Python, TS, Java, Go, Ruby) | 10+ | 40+ |
| 라이선스 | 상용 | 상용 | 오픈소스 |
| SDK 품질 | 최상 | 상 | 중 |
| 스트리밍 지원 | 네이티브 | 네이티브 | 수동 구현 필요 |
| 자동 재시도 | 내장 | 내장 | 수동 구현 |
| 채택 사례 | OpenAI, Anthropic, Cloudflare | Vercel, Cohere | 다수 오픈소스 |
SDK 생성 도구 선택은 프로젝트 규모와 예산에 따라 달라집니다. 공개 AI API를 제공하는 서비스라면 Stainless나 Speakeasy의 투자 가치가 있고, 내부 서비스라면 openapi-generator로도 충분합니다.
생성된 SDK든 수동으로 작성한 SDK든, 다음 원칙을 따라야 합니다.
# 좋은 예: 유창한 인터페이스
response = (
client.chat.completions
.create(
model="claude-4",
messages=[
Message.system("You are a helpful assistant"),
Message.user("API 설계 팁을 알려주세요"),
],
)
.with_temperature(0.7)
.with_max_tokens(1024)
.with_timeout(30)
.send()
)
# Python SDK: Pydantic 모델로 입출력 타입 보장
class ChatCompletionRequest(BaseModel):
model: str
messages: list[Message]
temperature: float = Field(default=1.0, ge=0, le=2)
max_tokens: int | None = Field(default=None, ge=1)
stream: bool = False
tools: list[Tool] | None = None
response_format: ResponseFormat | None = None// SDK 에러 계층 구조
abstract class ExampleAIError extends Error {
abstract readonly status: number;
abstract readonly code: string;
}
class AuthenticationError extends ExampleAIError {
readonly status = 401;
readonly code = "authentication_error";
}
class RateLimitError extends ExampleAIError {
readonly status = 429;
readonly code = "rate_limit_exceeded";
readonly retryAfter: number;
constructor(message: string, retryAfter: number) {
super(message);
this.retryAfter = retryAfter;
}
}
class BadRequestError extends ExampleAIError {
readonly status = 400;
readonly code = "bad_request";
}
class InternalServerError extends ExampleAIError {
readonly status = 500;
readonly code = "internal_error";
}
// 사용자 코드에서 타입 안전한 에러 처리
try {
const response = await client.chat.completions.create(request);
} catch (error) {
if (error instanceof RateLimitError) {
// 타입 안전: retryAfter 속성 접근 가능
await sleep(error.retryAfter * 1000);
// 재시도
} else if (error instanceof AuthenticationError) {
// API 키 갱신
} else if (error instanceof BadRequestError) {
// 요청 수정
}
}class RetryPolicy {
private maxRetries: number;
private baseDelay: number;
private retryableStatuses = new Set([408, 429, 500, 502, 503, 504]);
constructor(maxRetries = 3, baseDelay = 500) {
this.maxRetries = maxRetries;
this.baseDelay = baseDelay;
}
async execute<T>(fn: () => Promise<T>): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (!this.shouldRetry(error as ExampleAIError, attempt)) {
throw error;
}
const delay = this.calculateDelay(error as ExampleAIError, attempt);
await sleep(delay);
}
}
throw lastError;
}
private shouldRetry(error: ExampleAIError, attempt: number): boolean {
if (attempt >= this.maxRetries) return false;
return this.retryableStatuses.has(error.status);
}
private calculateDelay(error: ExampleAIError, attempt: number): number {
// 429 에러는 Retry-After 헤더 존중
if (error instanceof RateLimitError && error.retryAfter) {
return error.retryAfter * 1000;
}
// 지수 백오프 + 지터
const exponentialDelay = this.baseDelay * Math.pow(2, attempt);
const jitter = Math.random() * this.baseDelay;
return exponentialDelay + jitter;
}
}gRPC SDK는 Proto 파일에서 자동 생성됩니다. 2장에서 다룬 .proto 파일이 SDK의 소스입니다.
# Python gRPC 클라이언트 생성
python -m grpc_tools.protoc \
-I./proto \
--python_out=./sdk/python \
--grpc_python_out=./sdk/python \
--pyi_out=./sdk/python \
proto/ai_service.proto
# Go gRPC 클라이언트 생성
protoc \
-I./proto \
--go_out=./sdk/go --go_opt=paths=source_relative \
--go-grpc_out=./sdk/go --go-grpc_opt=paths=source_relative \
proto/ai_service.protoimport grpc
from gen import ai_service_pb2 as pb
from gen import ai_service_pb2_grpc as stub
class AIServiceClient:
"""gRPC 클라이언트 래퍼 — SDK와 동일한 인터페이스 제공"""
def __init__(self, host: str, api_key: str):
self.channel = grpc.aio.insecure_channel(host)
self.stub = stub.InferenceServiceStub(self.channel)
self.api_key = api_key
@property
def _metadata(self) -> list[tuple[str, str]]:
return [("x-api-key", self.api_key)]
async def complete(
self,
model: str,
messages: list[dict],
temperature: float = 1.0,
max_tokens: int | None = None,
) -> CompleteResult:
request = pb.CompleteRequest(
model=model,
messages=[
pb.Message(role=m["role"], content=m["content"])
for m in messages
],
temperature=temperature,
)
if max_tokens:
request.max_tokens = max_tokens
response = await self.stub.Complete(
request,
metadata=self._metadata,
timeout=60.0,
)
return CompleteResult.from_proto(response)
async def stream_complete(
self,
model: str,
messages: list[dict],
**kwargs,
):
request = pb.CompleteRequest(
model=model,
messages=[
pb.Message(role=m["role"], content=m["content"])
for m in messages
],
**kwargs,
)
async for chunk in self.stub.StreamComplete(
request,
metadata=self._metadata,
timeout=120.0,
):
yield StreamChunk.from_proto(chunk)
async def close(self):
await self.channel.close()GraphQL SDK는 스키마와 쿼리에서 타입과 훅을 자동 생성합니다.
# graphql-codegen 설정
npm install -D @graphql-codegen/cli \
@graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-queryschema: "https://api.example.com/graphql"
documents: "src/**/*.graphql"
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-query
config:
fetcher:
func: "../lib/graphql-client#fetchGraphQL"
reactQueryVersion: 5
addInfiniteQuery: truequery ListModels($first: Int, $after: String, $provider: ModelProvider) {
models(first: $first, after: $after, provider: $provider) {
edges {
node {
id
name
provider
contextWindow
pricing {
inputPerMillion
outputPerMillion
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}생성된 코드를 React 컴포넌트에서 타입 안전하게 사용합니다.
import { useListModelsQuery } from "../generated/graphql";
function ModelList() {
const { data, isLoading, fetchNextPage, hasNextPage } =
useListModelsQuery(
{ first: 20, provider: "ANTHROPIC" },
{ staleTime: 60_000 }
);
if (isLoading) return <Skeleton />;
return (
<div>
{data?.models.edges.map(({ node }) => (
<ModelCard
key={node.id}
name={node.name}
provider={node.provider}
contextWindow={node.contextWindow}
pricing={node.pricing}
/>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()}>더 보기</button>
)}
</div>
);
}from fastapi import FastAPI
app = FastAPI(
title="AI Service API",
description="""
AI 추론 서비스 API입니다.
## 인증
모든 요청에 Bearer 토큰이 필요합니다.
## 레이트 리밋
- Free: 20 RPM, 10K TPM
- Pro: 100 RPM, 100K TPM
""",
version="1.0.0",
docs_url="/docs", # Swagger UI
redoc_url="/redoc", # Redoc
openapi_url="/openapi.json",
contact={
"name": "API Support",
"email": "api@example.com",
},
license_info={
"name": "MIT",
},
)API 문서에는 단순한 스펙 나열이 아니라, 실제 사용 시나리오별 예제(quickstart, 스트리밍, 도구 호출, 배치 처리)를 포함해야 합니다. OpenAI와 Anthropic의 API 문서가 업계 벤치마크로 여겨지는 이유는, 개념 설명과 실행 가능한 코드 예제를 함께 제공하기 때문입니다.
이 장에서는 API 스펙에서 SDK를 자동 생성하는 도구(Stainless, Speakeasy, openapi-generator)를 비교하고, 타입 안전한 SDK 설계 원칙(빌더 패턴, 에러 계층, 자동 재시도)을 살펴보았습니다. gRPC와 GraphQL의 코드 생성 워크플로우와 API 문서화 방법도 다루었습니다.
개발자 경험(DX)은 API의 채택률을 결정하는 핵심 요소입니다. 타입 안전한 SDK, 인터랙티브 문서, 실행 가능한 코드 예제를 제공하면 개발자의 첫 API 호출까지 시간(Time To First Call)을 극적으로 줄일 수 있습니다.
10장에서는 프로덕션 환경의 API 인프라를 다룹니다. LLM 게이트웨이(LiteLLM, Bifrost)를 활용한 멀티 프로바이더 라우팅, 모델 폴백, 인증/인가, 로드밸런싱, 캐싱, 관측 가능성까지 API 게이트웨이의 전체 스펙트럼을 살펴봅니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
LLM 게이트웨이를 활용한 멀티 프로바이더 라우팅, 모델 폴백, 인증/인가, 캐싱, 관측 가능성 등 프로덕션 API 인프라를 학습합니다.
토큰 기반 레이트 리미팅, 토큰 버킷과 슬라이딩 윈도우 알고리즘, 사용자별 한도 설정, 비용 캡, Redis 기반 구현을 학습합니다.
REST 공개 API와 gRPC 내부 통신을 결합한 AI 서비스 API를 설계하고, OpenAPI 스펙, FastAPI 구현, 스트리밍, 인증, SDK 생성까지 전체를 구축합니다.