본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 9장: SDK 자동 생성과 개발자 경험
2026년 2월 20일·아키텍처·

9장: SDK 자동 생성과 개발자 경험

OpenAPI 스펙에서 타입 안전 SDK를 자동 생성하고, API 문서화, 인터랙티브 플레이그라운드로 개발자 경험을 최적화하는 방법을 학습합니다.

13분1,106자8개 섹션
api-designgraphqlarchitecture
공유
api-design9 / 11
1234567891011
이전8장: 레이트 리미팅과 비용 제어다음10장: API 게이트웨이와 프로덕션 인프라

학습 목표

  • OpenAPI 스펙에서 SDK를 자동 생성하는 도구와 워크플로우를 이해합니다
  • Stainless, Speakeasy, openapi-generator의 특성을 비교합니다
  • 타입 안전한 SDK 설계 원칙을 학습합니다
  • API 문서화와 인터랙티브 플레이그라운드로 DX를 향상시킵니다

SDK가 필요한 이유

AI API를 직접 HTTP로 호출하는 것은 가능하지만, 개발자에게 최적의 경험은 아닙니다. 잘 설계된 SDK는 타입 안전성, 자동 재시도, 스트리밍 처리, 인증 관리를 추상화하여 개발 생산성을 극대화합니다.

raw-http-vs-sdk.ts
typescript
// 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 객체

SDK 생성 도구 비교

Stainless

Stainless는 OpenAI와 Anthropic이 공식 SDK를 생성하는 데 사용하는 도구입니다. OpenAPI 스펙을 입력으로 받아 Python, TypeScript, Java, Go, Ruby SDK를 생성합니다.

stainless.yml
yaml
# .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-ai

Stainless가 생성하는 SDK의 주요 특성입니다.

stainless-generated-sdk.ts
typescript
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

Speakeasy는 엔터프라이즈 환경에 초점을 맞춘 SDK 생성 도구입니다. OpenAPI 스펙의 품질을 분석하고, 문서, 샘플 코드까지 함께 생성합니다.

speakeasy-usage.sh
bash
# 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

openapi-generator

오픈소스 프로젝트로, 40개 이상의 언어를 지원합니다. 커스터마이징 자유도가 높지만, 생성된 코드의 품질이 상용 도구에 비해 다소 낮을 수 있습니다.

openapi-generator-usage.sh
bash
# 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

도구 비교

특성StainlessSpeakeasyopenapi-generator
지원 언어5개 (Python, TS, Java, Go, Ruby)10+40+
라이선스상용상용오픈소스
SDK 품질최상상중
스트리밍 지원네이티브네이티브수동 구현 필요
자동 재시도내장내장수동 구현
채택 사례OpenAI, Anthropic, CloudflareVercel, Cohere다수 오픈소스
Info

SDK 생성 도구 선택은 프로젝트 규모와 예산에 따라 달라집니다. 공개 AI API를 제공하는 서비스라면 Stainless나 Speakeasy의 투자 가치가 있고, 내부 서비스라면 openapi-generator로도 충분합니다.


타입 안전 SDK 설계 원칙

생성된 SDK든 수동으로 작성한 SDK든, 다음 원칙을 따라야 합니다.

1. 빌더 패턴과 유창한 인터페이스

sdk_fluent_api.py
python
# 좋은 예: 유창한 인터페이스
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

2. 에러 타입 계층

sdk-error-types.ts
typescript
// 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) {
    // 요청 수정
  }
}

3. 자동 재시도와 백오프

sdk-retry-logic.ts
typescript
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;
  }
}

Protobuf에서 gRPC 클라이언트 생성

gRPC SDK는 Proto 파일에서 자동 생성됩니다. 2장에서 다룬 .proto 파일이 SDK의 소스입니다.

grpc-client-generation.sh
bash
# 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.proto
grpc_client_wrapper.py
python
import 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 코드 생성

GraphQL SDK는 스키마와 쿼리에서 타입과 훅을 자동 생성합니다.

graphql-codegen.sh
bash
# graphql-codegen 설정
npm install -D @graphql-codegen/cli \
  @graphql-codegen/typescript \
  @graphql-codegen/typescript-operations \
  @graphql-codegen/typescript-react-query
codegen.yml
yaml
schema: "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: true
src/queries/models.graphql
graphql
query 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 컴포넌트에서 타입 안전하게 사용합니다.

ModelList.tsx
tsx
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>
  );
}

API 문서화

Swagger UI와 Redoc

fastapi_docs.py
python
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",
    },
)
Tip

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 게이트웨이의 전체 스펙트럼을 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#api-design#graphql#architecture

관련 글

아키텍처

10장: API 게이트웨이와 프로덕션 인프라

LLM 게이트웨이를 활용한 멀티 프로바이더 라우팅, 모델 폴백, 인증/인가, 캐싱, 관측 가능성 등 프로덕션 API 인프라를 학습합니다.

2026년 2월 22일·16분
아키텍처

8장: 레이트 리미팅과 비용 제어

토큰 기반 레이트 리미팅, 토큰 버킷과 슬라이딩 윈도우 알고리즘, 사용자별 한도 설정, 비용 캡, Redis 기반 구현을 학습합니다.

2026년 2월 18일·16분
아키텍처

11장: 실전 프로젝트 — AI 서비스 API 설계

REST 공개 API와 gRPC 내부 통신을 결합한 AI 서비스 API를 설계하고, OpenAPI 스펙, FastAPI 구현, 스트리밍, 인증, SDK 생성까지 전체를 구축합니다.

2026년 2월 24일·20분
이전 글8장: 레이트 리미팅과 비용 제어
다음 글10장: API 게이트웨이와 프로덕션 인프라

댓글

목차

약 13분 남음
  • 학습 목표
  • SDK가 필요한 이유
  • SDK 생성 도구 비교
    • Stainless
    • Speakeasy
    • openapi-generator
    • 도구 비교
  • 타입 안전 SDK 설계 원칙
    • 1. 빌더 패턴과 유창한 인터페이스
    • 2. 에러 타입 계층
    • 3. 자동 재시도와 백오프
  • Protobuf에서 gRPC 클라이언트 생성
  • GraphQL 코드 생성
  • API 문서화
    • Swagger UI와 Redoc
  • 정리
    • 다음 장 미리보기