본문 바로가기
C.W.K.
Stream
Lesson 04 of 06 · published

OpenAPI와 JSON Schema — 기계가 읽을 수 있는 API 계약

~11 min · production, openapi, json-schema, spec

Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"문서화되지 않은 API에는 소비자가 의지할 명확한 계약이 없어. OpenAPI와 JSON Schema를 사용하면 문서, 클라이언트 생성기, 모의 서버, 검증 도구가 같은 인터페이스 정의를 읽을 수 있어."

OpenAPI란 무엇인가

OpenAPI는 HTTP API의 엔드포인트, 메서드, 매개변수, 요청 본문, 응답 본문, 오류 형태, 인증 방식 등을 YAML이나 JSON으로 기술하는 명세야. OpenAPI 3.1 계열의 Schema Object는 JSON Schema 2020-12와 정렬되어 있지만, 실제 지원 범위는 도구마다 확인해야 해. 하나의 OpenAPI 문서를 다음과 같은 여러 작업의 입력으로 활용할 수 있어:

  • 사람을 위한 문서 — Swagger UI나 ReDoc 같은 도구로 탐색 가능한 문서를 만들 수 있어.
  • 클라이언트 SDK — OpenAPI Generator 같은 도구로 여러 언어의 클라이언트 코드를 생성할 수 있어. 생성 결과는 검토와 테스트가 필요해.
  • 모의 서버 — Prism 같은 도구가 명세와 예시를 바탕으로 응답을 만들어 줄 수 있어.
  • 서버 뼈대 — 일부 생성기가 라우트와 모델의 초기 구조를 만들 수 있지만, 비즈니스 동작까지 구현해 주지는 않아.
  • 요청과 응답 검증 — 구현의 실제 입출력이 명세와 맞는지 런타임이나 CI에서 검사할 수 있어.
  • 계약 검사 — 제공자와 소비자가 합의한 인터페이스의 호환성을 배포 전에 확인할 수 있어.

OpenAPI 문서의 구조

openapi: 3.1.0
info:
  title: Users API
  version: 1.2.0
servers:
  - url: https://api.example.com
paths:
  /users/{uid}:
    get:
      summary: User 읽기
      parameters:
        - name: uid
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  schemas:
    User:
      type: object
      required: [id, name]
      properties:
        id:    { type: string, pattern: '^u_' }
        name:  { type: string, minLength: 1 }
        email: { type: string, format: email }
  responses:
    NotFound:
      description: Resource 못 찾음
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

여기서 User 객체의 필수 필드는 required에 들어 있는 id와 name뿐이야. email은 정의되어 있지만 필수는 아니므로 누락할 수 있어. 스키마를 재사용하면 문서, SDK, 모의 응답이 같은 형태를 참고할 수 있지. 다만 위 동결 예제의 NotFound 응답은 정의되지 않은 Error 스키마를 참조하므로 그대로는 참조 해석에 실패해. 실제 명세에서는 components.schemas에 Error를 정의해야 해.

JSON Schema — 데이터 형태와 제약을 표현하는 언어

JSON Schema는 JSON 인스턴스의 구조와 제약을 기술하는 독립적인 명세야. OpenAPI에서는 요청, 응답, 매개변수에 들어갈 데이터의 형태를 표현하는 데 활용해:

  • 타입 — 문자열, 숫자, 불리언, 배열, 객체, null 같은 값의 종류를 표현해.
  • 제약 — minLength, maxLength, 정규식 pattern, minimum, maximum, email·uri·date-time 같은 format을 기술해. format을 실제 오류로 처리하는지는 검증기 설정과 구현에 따라 달라질 수 있어.
  • 합성 — oneOf, anyOf, allOf로 스키마를 조합하고 $ref로 정의를 재사용해.

TypeScript 타입은 주로 정적 검사 단계에서 개발 도구를 돕지만, 네트워크에서 들어온 JSON은 런타임에 별도로 검증해야 해. JSON Schema 자체는 선언문이고, 실제 거부 여부와 오류 메시지는 선택한 검증기가 결정해. 또한 properties에 필드를 적는 것만으로 필수가 되지는 않아. 객체에서 반드시 있어야 할 필드는 required에 명시해야 해.

FastAPI의 자동 생성

FastAPI는 Pydantic 모델과 라우트 시그니처를 바탕으로 OpenAPI 문서를 생성해. 기본 설정에서는 /openapi.json에서 명세를, /docs에서 Swagger UI를, /redoc에서 ReDoc을 제공해. 경로나 문서 노출 여부는 애플리케이션 설정으로 바꿀 수 있고, 운영 환경에서는 인증·노출 정책도 검토해야 해.

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI(title='Users API', version='1.2.0')

class User(BaseModel):
    id: str
    name: str
    email: EmailStr | None = None

@app.get('/users/{uid}', response_model=User)
async def read_user(uid: str) -> User:
    return User(id=uid, name='Pippa')

Pydantic 모델은 JSON Schema 기반 정의로 투영되고, 라우트는 OpenAPI path operation으로, response_model은 응답 문서와 런타임 직렬화·검증 규칙으로 이어져. 위 예제에서 id와 name은 필수고, 기본값이 None인 email은 선택 필드야. 아래 동결 코드의 responses 항목은 404 응답을 문서화할 뿐 자동으로 그 모양을 강제하지 않아. 실제 HTTPException은 detail 래퍼를 포함하므로, 문서의 Error 모델과 실제 응답 형태가 일치하도록 별도의 예외 처리나 응답 모델을 구성해야 해.

OpenAPI 문서는 API 계약의 중요한 표현물이므로 코드와 함께 버전 관리하고 실제 동작과 계속 맞춰야 해. 코드에서 생성하든 명세를 먼저 작성하든 변경 내역을 비교하고, 호환성을 깨는 차이를 검사하며, 문서와 SDK를 재생성해. 명세 파일이 존재한다는 사실만으로 구현이 계약을 지킨다는 보장은 없어.

명세 우선과 코드 우선

  • 코드 우선 — FastAPI처럼 모델과 라우트 구현에서 명세를 생성하는 방식이야. 구현과 문서를 가깝게 유지하기 쉽지만, 자동 생성 결과에 빠진 응답이나 오류 형태가 없는지 검토해야 해.
  • 명세 우선 — 구현 전에 OpenAPI 문서로 인터페이스를 합의하고, 필요하면 서버 뼈대와 클라이언트 SDK를 생성하는 방식이야. 여러 팀과 외부 소비자가 병렬로 작업할 때 유용하지만, 생성 코드와 실제 구현의 일치 여부를 계속 검사해야 해.

어느 방식이 더 엄격한지는 작업 절차와 자동 검사에 달려 있어. 팀 규모, 소비자 수, 변경 승인 과정, 기존 코드의 상태를 기준으로 선택하고, 최종적으로는 배포된 API와 배포된 명세가 일치하도록 관리해야 해.

cwkPippa의 OpenAPI 활용

cwkPippa의 FastAPI 백엔드는 /openapi.json에서 OpenAPI 문서를, /docs에서 Swagger UI를 제공할 수 있어. 엔드포인트가 받는 매개변수와 본문 형태를 확인하고 직접 요청을 시험하는 데 유용하지. 하지만 자동 생성 문서는 실제 오류 응답, 인증 요구 사항, 동적 동작을 빠짐없이 표현하는지 별도로 검토해야 해. 내부용 API라도 계약을 소비하는 프런트엔드와 도구가 있다면 명세 변경을 추적하고 실제 응답과 비교할 가치가 있어.

Code

FastAPI — Pydantic 모델과 라우트에서 OpenAPI 생성·python
# FastAPI — Pydantic + route 시그너처에서 OpenAPI 자동
from fastapi import FastAPI, HTTPException, Path
from pydantic import BaseModel, EmailStr, Field

app = FastAPI(
    title='Users API',
    version='1.2.0',
    description='User 관리.',
)

class User(BaseModel):
    id: str = Field(..., pattern='^u_')
    name: str = Field(..., min_length=1)
    email: EmailStr | None = None

class Error(BaseModel):
    code: str
    message: str

@app.get(
    '/users/{uid}',
    response_model=User,
    responses={404: {'model': Error}},
)
async def read_user(uid: str = Path(..., pattern='^u_')) -> User:
    if uid == 'u_missing':
        raise HTTPException(404, detail={'code': 'not_found', 'message': '...'})
    return User(id=uid, name='Pippa')

# 방문:
# http://localhost:8000/openapi.json  — 전체 OpenAPI 3.1 spec
# http://localhost:8000/docs          — Swagger UI (브라우저 API explorer)
# http://localhost:8000/redoc          — ReDoc (대안 docs UI)
OpenAPI Generator — 명세에서 타입이 있는 클라이언트 생성·bash
# openapi-generator — spec 에서 typed Python client 생성
npm install -g @openapitools/openapi-generator-cli
openapi-generator-cli generate \
  -i http://localhost:8000/openapi.json \
  -g python \
  -o ./generated-client

# 이제 모든 endpoint 위한 typed method 가진 Python SDK 있음:
from generated_client import ApiClient, UsersApi
client = UsersApi(ApiClient())
user = client.read_user(uid='u_42')  # typed, autocompleted
print(user.name)

# 사용 가능 generator: python, typescript-axios, java, go, swift, kotlin,
# csharp, rust, php, ruby, dart, ... 50+ 언어.
OpenAPI 기반 모의 서버와 계약 검사·bash
# Prism — OpenAPI spec 에서 mock server
npm install -g @stoplight/prism-cli
prism mock https://api.example.com/openapi.json
# 이제 모든 endpoint 위해 schema-shaped fake 데이터로 응답하는
# http://localhost:4010 의 fake API 있음. 실제 backend 준비 전
# 이것에 대해 client 코드 test.

# Schemathesis — spec 에서 property-based test 생성
pip install schemathesis
schemathesis run http://localhost:8000/openapi.json
# 실제 API 를 spec 에서 유도된 request 로 fuzz; test 안 한 경우 잡음.

External links

Exercise

FastAPI 애플리케이션 하나를 고르거나 엔드포인트 세 개짜리 데모를 만들어 /openapi.json/docs를 확인해 봐. 그 명세에서 OpenAPI Generator CLI로 TypeScript 클라이언트를 생성하고, 짧은 스크립트에서 엔드포인트 하나를 호출해. 보너스로 response_model에서 필수인 name을 누락한 객체를 반환해 응답 검증 실패를 확인한 다음, 선택 필드인 email만 누락한 객체는 정상 응답이 되는지 비교해 봐.
Hint
openapi-generator-cli generate -i http://localhost:8000/openapi.json -g typescript-axios -o ./client로 클라이언트를 만들 수 있어. 생성된 메서드와 타입을 사용해 호출하되 실제 서버 응답도 확인해. response_model과 맞지 않는 응답은 FastAPI에서 ResponseValidationError를 일으킬 수 있어. 반면 기본값이 있는 email: EmailStr | None = None은 선택 필드라서 누락 자체가 오류가 아니야. 명세 생성과 런타임 검증은 연결되어 있지만 동일한 단계는 아니라는 점을 관찰해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.