Skip to main content

API 설계 기초

API(Application Programming Interface)는 서비스와 서비스, 클라이언트와 서버가 약속된 규칙으로 소통하는 인터페이스입니다. 잘 설계된 API는 사용하기 쉽고, 변경에 강하며, 확장이 자연스럽습니다. LLM을 서비스로 제공하든, ML 파이프라인을 자동화하든, 결국 API가 서비스의 얼굴입니다.

학습 목표

  • REST 설계 원칙을 이해하고 리소스 중심 URL을 설계할 수 있다
  • REST, GraphQL, gRPC의 차이를 비교하고 적합한 상황을 판단할 수 있다
  • OpenAPI 스펙을 이해하고 자동 문서화의 가치를 설명할 수 있다
  • LLM API의 호출 패턴(스트리밍, 토큰 과금, SDK)을 이해한다

왜 중요한가

현대 소프트웨어는 API로 연결된 서비스의 네트워크입니다. 프론트엔드와 백엔드, 마이크로서비스 간, 외부 SaaS와의 연동 — 모두 API를 통합니다. AI/ML 분야에서는 이 경향이 더욱 두드러집니다. 모델 학습은 내부 API를 통해 오케스트레이션되고, 모델 서빙은 REST API로 노출되며, LLM 기능은 OpenAI 호환 API로 표준화되고 있습니다. 좋은 API 설계는 단순히 “동작하는” 수준이 아니라, **개발자 경험(DX)**을 고려한 설계입니다. 명확한 URL, 일관된 응답 형식, 충분한 에러 메시지 — 이것이 사용자가 여러분의 서비스를 선택하게 만드는 차이입니다.

REST 설계 원칙

REST(Representational State Transfer)는 웹 API의 가장 보편적인 아키텍처 스타일입니다.

핵심 원칙

원칙설명예시
리소스 중심 URLURL은 동사가 아닌 명사(리소스)를 표현/users, /models/gpt-4o
HTTP 메서드 매핑동작은 HTTP 메서드로 표현GET /users, POST /users
무상태(Stateless)각 요청은 독립적, 서버가 클라이언트 상태를 저장하지 않음모든 정보를 요청에 포함
통일된 인터페이스일관된 URL 패턴과 응답 구조모든 리소스가 같은 패턴
HATEOAS응답에 관련 리소스 링크 포함"links": {"next": "/users?page=2"}

리소스 URL 설계 패턴

# 좋은 설계 - 리소스 중심
GET    /api/v1/models                  # 모델 목록
POST   /api/v1/models                  # 모델 등록
GET    /api/v1/models/{model_id}       # 특정 모델 조회
PUT    /api/v1/models/{model_id}       # 모델 전체 수정
PATCH  /api/v1/models/{model_id}       # 모델 부분 수정
DELETE /api/v1/models/{model_id}       # 모델 삭제

# 중첩 리소스
GET    /api/v1/models/{model_id}/versions
POST   /api/v1/models/{model_id}/versions
GET    /api/v1/models/{model_id}/versions/{version_id}

# 액션이 필요한 경우 (리소스로 표현하기 어려울 때)
POST   /api/v1/models/{model_id}/deploy
POST   /api/v1/chat/completions
URL에 동사를 넣지 마세요: /getUsers, /deleteModel은 안티패턴입니다. 동작은 HTTP 메서드가 담당합니다. 단, deploy처럼 CRUD로 표현하기 어려운 액션은 예외적으로 동사를 허용합니다.

REST vs GraphQL vs gRPC 비교

항목RESTGraphQLgRPC
엔드포인트리소스별 다수단일 엔드포인트서비스별 메서드
데이터 선택서버가 결정 (오버/언더페칭)클라이언트가 필드 선택프로토콜 버퍼로 정의
프로토콜HTTP/1.1 + JSONHTTP/1.1 + JSONHTTP/2 + Protobuf (바이너리)
성능보통보통 (N+1 주의)높음 (바이너리, 스트리밍)
학습곡선낮음중간높음
타입 안전성OpenAPI로 보완스키마 내장Protobuf로 보장
스트리밍SSE, WebSocketSubscription양방향 스트리밍 네이티브
적합 사례범용 API, 외부 공개 API복잡한 데이터 관계, 모바일마이크로서비스 간 통신
AI/ML 활용모델 서빙, LLM APIML 메타데이터 조회추론 서빙 (TensorFlow Serving)
대부분의 AI/ML API는 REST를 사용합니다. OpenAI, Anthropic, HuggingFace 모두 REST API입니다. gRPC는 TensorFlow Serving, Triton Inference Server처럼 고성능 내부 추론 서빙에서 사용됩니다.

API 버전관리

API는 시간이 지나면 변경됩니다. 기존 사용자를 깨뜨리지 않으면서 발전시키는 것이 버전관리의 핵심입니다.
1

방식 1: URL 경로 버전 (가장 보편적)

GET /api/v1/models
GET /api/v2/models
가장 직관적이고 널리 사용됩니다. OpenAI(/v1/chat/completions), HuggingFace가 이 방식을 사용합니다.
2

방식 2: 헤더 버전

GET /api/models
Accept: application/vnd.myapi.v2+json
URL을 깔끔하게 유지하지만, 테스트와 디버깅이 불편합니다.
3

방식 3: 쿼리 파라미터 버전

GET /api/models?version=2
간단하지만, 캐싱이 복잡해지고 REST 관점에서 리소스 식별이 모호해집니다.
4

Deprecation 정책 수립

이전 버전을 즉시 제거하지 말고, 최소 6~12개월의 유예 기간을 둡니다. Sunset 헤더와 Deprecation 헤더로 클라이언트에 알리고, 문서와 변경 로그(changelog)에 마이그레이션 가이드를 제공합니다.

OpenAPI와 Swagger

OpenAPI Specification(OAS)은 REST API를 기계가 읽을 수 있는 형식으로 기술하는 표준입니다.
# openapi.yaml 예시 (간략)
openapi: "3.1.0"
info:
  title: "ML Model API"
  version: "1.0.0"
paths:
  /models:
    get:
      summary: "모델 목록 조회"
      responses:
        "200":
          description: "성공"
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Model"
  /models/{model_id}/predict:
    post:
      summary: "모델 추론"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PredictRequest"
FastAPI는 OpenAPI를 자동 생성합니다:
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="ML Model API", version="1.0.0")

class PredictRequest(BaseModel):
    text: str
    max_tokens: int = 100

class PredictResponse(BaseModel):
    prediction: str
    confidence: float

@app.post("/models/{model_id}/predict", response_model=PredictResponse)
async def predict(model_id: str, request: PredictRequest):
    # 모델 추론 로직
    return PredictResponse(prediction="긍정", confidence=0.95)

# /docs → Swagger UI 자동 생성
# /openapi.json → OpenAPI 스펙 자동 생성

인증과 Rate Limiting

인증 방식 요약

방식헤더적합 상황
API KeyX-API-Key: key123 또는 Authorization: Bearer key123서버 간 통신, 간단한 인증
Bearer Token (JWT)Authorization: Bearer eyJhbG...사용자별 인증, 세션 관리
OAuth 2.0Authorization: Bearer access_token제3자 서비스 연동, 사용자 동의

Rate Limiting 설계

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import time

# 간단한 인메모리 Rate Limiter (프로덕션에서는 Redis 사용)
rate_limits: dict[str, list[float]] = {}

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    client_ip = request.client.host
    now = time.time()
    window = 60  # 1분
    max_requests = 60  # 분당 60회

    # 이전 기록 정리 및 현재 요청 추가
    rate_limits.setdefault(client_ip, [])
    rate_limits[client_ip] = [t for t in rate_limits[client_ip] if now - t < window]

    if len(rate_limits[client_ip]) >= max_requests:
        return JSONResponse(
            status_code=429,
            content={"error": "rate_limit_exceeded", "retry_after": window},
            headers={
                "X-RateLimit-Limit": str(max_requests),
                "X-RateLimit-Remaining": "0",
                "Retry-After": str(window),
            },
        )

    rate_limits[client_ip].append(now)
    response = await call_next(request)
    response.headers["X-RateLimit-Limit"] = str(max_requests)
    response.headers["X-RateLimit-Remaining"] = str(max_requests - len(rate_limits[client_ip]))
    return response

LLM API 호출 패턴

스트리밍 (Server-Sent Events)

LLM은 토큰을 순차적으로 생성하므로, 전체 응답을 기다리지 않고 스트리밍으로 받을 수 있습니다.
from openai import OpenAI

client = OpenAI()

# 스트리밍 응답
stream = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Python의 장점 3가지"}],
    stream=True,  # SSE 스트리밍 활성화
    max_tokens=500,
)

for chunk in stream:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)

토큰 기반 과금

개념설명
Input Tokens프롬프트(시스템 + 사용자 메시지)의 토큰 수
Output Tokens모델이 생성한 응답의 토큰 수
max_tokens생성할 최대 출력 토큰 수 (비용/시간 제어)
총 비용(input_tokens x input_price) + (output_tokens x output_price)
max_tokens를 설정하지 않으면 모델이 자체 판단으로 생성을 멈출 때까지 토큰을 소비합니다. 비용과 응답 시간을 제어하려면 반드시 설정하세요.

비동기 배치 처리

대량의 요청을 처리할 때는 비동기 + 배치가 효율적입니다.
import asyncio
from openai import AsyncOpenAI

client = AsyncOpenAI()

async def process_single(prompt: str) -> str:
    response = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=200,
    )
    return response.choices[0].message.content

async def process_batch(prompts: list[str], concurrency: int = 5) -> list[str]:
    semaphore = asyncio.Semaphore(concurrency)  # 동시 요청 제한

    async def limited_process(prompt):
        async with semaphore:
            return await process_single(prompt)

    tasks = [limited_process(p) for p in prompts]
    return await asyncio.gather(*tasks)

# 사용
prompts = ["질문1", "질문2", "질문3", "질문4", "질문5"]
results = asyncio.run(process_batch(prompts, concurrency=3))

SDK vs HTTP 직접 호출

항목SDK (openai 패키지)HTTP 직접 (requests)
코드량적음많음
타입 안전성자동 완성, 타입 힌트없음 (수동 파싱)
에러 처리구조화된 예외 클래스수동 상태코드 확인
재시도/백오프내장직접 구현
스트리밍이터레이터 제공SSE 직접 파싱
유연성SDK 지원 범위 내완전한 제어
디버깅추상화되어 내부 확인 어려움원시 요청/응답 확인 가능
# SDK 방식 - 간결하고 안전
from openai import OpenAI, RateLimitError

client = OpenAI()
try:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "Hello"}],
    )
    print(response.choices[0].message.content)
except RateLimitError as e:
    print(f"Rate limited: {e}")

# HTTP 직접 호출 - 유연하지만 수동 관리 필요
import requests

response = requests.post(
    "https://api.openai.com/v1/chat/completions",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    },
    json={
        "model": "gpt-4o",
        "messages": [{"role": "user", "content": "Hello"}],
    },
    timeout=(5, 120),
)
if response.status_code == 429:
    retry_after = response.headers.get("Retry-After", 60)
    # 재시도 로직 직접 구현 필요

에러 응답 설계

일관된 에러 형식은 클라이언트 개발자 경험을 크게 향상시킵니다.

RFC 7807 Problem Details 형식

{
  "type": "https://api.example.com/errors/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "분당 60회 요청 제한을 초과했습니다. 43초 후 재시도하세요.",
  "instance": "/v1/chat/completions",
  "retry_after": 43
}
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "type": f"https://api.example.com/errors/{exc.status_code}",
            "title": exc.detail if isinstance(exc.detail, str) else "Error",
            "status": exc.status_code,
            "detail": str(exc.detail),
            "instance": str(request.url.path),
        },
    )
컬렉션은 리소스의 집합입니다: /users, /models. 복수형 명사를 사용합니다. 리소스는 컬렉션의 개별 항목입니다: /users/123, /models/gpt-4o. GET /users는 목록을 반환하고, GET /users/123은 특정 사용자를 반환합니다. 계층 관계는 URL 경로로 표현합니다: /users/123/orders (123번 사용자의 주문 목록).
오프셋 기반: ?page=2&per_page=20 — 구현 간단하나 대량 데이터에서 성능 저하. 커서 기반: ?cursor=abc123&limit=20 — 일관된 성능, 실시간 데이터에 적합. 키셋 기반: ?after_id=100&limit=20 — DB 인덱스 활용 가능, 빠름. 응답에 반드시 total_count, has_more, next_cursor 메타데이터를 포함하세요.
HATEOAS(Hypermedia as the Engine of Application State)는 응답에 다음 가능한 액션의 링크를 포함하는 원칙입니다. 이론적으로 API 탐색성이 좋지만, 실무에서는 클라이언트가 URL을 하드코딩하는 것이 대부분이라 링크를 따라가는 구현을 하는 경우가 드뭅니다. 대신 OpenAPI 문서가 그 역할을 대신합니다. GitHub API처럼 부분적으로 채택하는 사례도 있습니다 (Link 헤더로 페이지네이션 링크 제공).
많은 LLM 서빙 도구(vLLM, Ollama, LiteLLM)가 OpenAI API 형식과 호환되는 엔드포인트를 제공합니다. 이는 클라이언트 코드 변경 없이 모델을 교체할 수 있게 해줍니다. base_url만 바꾸면 OpenAI → 오픈소스 모델 → 사내 모델로 전환이 가능합니다. 이것이 API 표준화의 실질적인 가치입니다.
상태코드만으로는 에러 원인을 구체적으로 파악하기 어렵습니다. 애플리케이션 수준의 에러 코드를 별도로 정의하면 디버깅이 쉬워집니다:
  • INVALID_API_KEY (401), RATE_LIMIT_EXCEEDED (429), MODEL_NOT_FOUND (404)
  • CONTEXT_LENGTH_EXCEEDED (400), CONTENT_FILTER_TRIGGERED (400) OpenAI는 error.code 필드에 invalid_api_key, model_not_found 등을 반환합니다.

체크리스트

  • REST의 리소스 중심 URL 설계 원칙을 따라 API를 설계할 수 있다
  • REST, GraphQL, gRPC의 차이와 각각의 적합한 사용 상황을 설명할 수 있다
  • API 버전관리 전략(URL 경로, 헤더, 쿼리)의 장단점을 비교할 수 있다
  • FastAPI에서 OpenAPI 자동 문서화가 어떻게 작동하는지 이해한다
  • Rate Limiting의 필요성과 429 응답 설계를 설명할 수 있다
  • LLM API의 스트리밍 호출과 토큰 기반 과금 구조를 이해한다
  • SDK와 HTTP 직접 호출의 장단점을 비교하여 상황에 맞게 선택할 수 있다
  • RFC 7807 스타일의 일관된 에러 응답 형식을 설계할 수 있다

다음 문서