Skip to content

✍️ 필사 모드: 2026 API 스키마 지도 — JSON Schema · OpenAPI 3.1 · AsyncAPI · GraphQL · gRPC · Smithy · TypeSpec 한 번에 정리

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

프롤로그 — 스키마는 이제 코드보다 먼저 온다

2026년의 백엔드 회의는 이렇게 시작한다. "엔드포인트부터 짜지 말고, 스키마부터 보여줘."

핸들러를 먼저 쓰고 그 결과를 OpenAPI로 뽑던 시절이 있었다. spec-from-code — 코드가 진실, 스펙은 부산물. 편했다. 그리고 망가졌다. 두 팀이 서로의 응답 모양을 추측하다가 한 달짜리 통합 버그를 만들고, 클라이언트 SDK는 항상 한 박자 늦었고, 사내 위키의 "현재 API" 문서는 절반이 거짓말이었다.

2026년은 정반대다. spec-first — 스키마를 먼저 쓰고, 거기서 타입·문서·서버 스텁·클라이언트 SDK·테스트 픽스처·AI 도구 정의까지 생성한다. 진실은 스키마 파일에 있다.

그래서 한 번 정리하자. 같은 "API"라도 프로토콜에 따라 도구가 다르다. REST는 OpenAPI 3.1, 이벤트는 AsyncAPI 3.0, 서비스 간은 Protobuf, 프런트엔드 친화는 GraphQL, 그리고 그 모든 것의 기초 문법은 JSON Schema 2020-12. 거기에 AWS의 Smithy, Microsoft의 TypeSpec, 계약 테스트의 Pact — 이름은 많지만 역할은 또렷이 나뉜다.

이 글은 2026년의 API 스키마 지형도다. 각 포맷의 정체와 강점, 같은 도메인 모델을 네 가지 포맷으로 옮겨본 비교, AI 도구 호출에서 JSON Schema가 차지한 자리, 그리고 "우리 팀은 무엇을 써야 하나"에 대한 정직한 의사결정 프레임까지.


1장 · JSON Schema 2020-12 — 모든 것의 토대

먼저 한 줄로: JSON Schema는 "JSON 값의 모양을 또 다른 JSON으로 묘사하는" 표준이다. "이 필드는 정수, 이건 1~120 사이, 이건 ISO 8601 날짜" — 그런 제약을 선언으로 적는다.

draft 2020-12가 사실상 안정 베이스라인이다. 그 이후 작업은 진행 중이지만, 2026년 현재 호환성·도구 지원이 가장 두꺼운 버전이 2020-12다. OpenAPI 3.1이 이 draft에 완전히 정렬되면서 둘 사이의 미세한 충돌(예전에 nullable, example 차이로 골치 아팠던 문제들)이 거의 사라졌다.

JSON Schema가 2026년에 왜 중요해졌나? 세 가지 자리에서 동시에 쓰이기 때문이다.

  1. REST API: OpenAPI 3.1의 components.schemas 안은 그대로 JSON Schema 2020-12다.
  2. AI 도구 호출(function calling): OpenAI·Anthropic·Gemini의 도구 정의 포맷이 전부 JSON Schema 서브셋이다. 모델이 arguments를 JSON으로 채워 반환하면, 호스트가 같은 스키마로 검증한다.
  3. 설정 검증: kubeconfig·CI 워크플로·앱 설정 파일을 검증할 때 가장 흔하게 만나는 표준.

JSON Schema는 "포맷"이 아니라 문법이다. 한 번 배우면 위 세 자리 모두에서 같은 문법을 쓴다. 2026년의 백엔드 엔지니어가 가장 자주 손대게 되는 단일 표준이라고 해도 과언이 아니다.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/user.json",
  "type": "object",
  "required": ["id", "email"],
  "properties": {
    "id": { "type": "string", "format": "uuid" },
    "email": { "type": "string", "format": "email" },
    "age": { "type": "integer", "minimum": 0, "maximum": 130 },
    "roles": {
      "type": "array",
      "items": { "type": "string", "enum": ["admin", "member", "guest"] },
      "uniqueItems": true
    }
  },
  "additionalProperties": false
}

이 작은 파일 하나가 OpenAPI 안에도 들어가고, AI 도구 정의에도 들어가고, 단독 validator에도 들어간다. 재사용 가능한 단위가 곧 진실이다.


2장 · OpenAPI 3.1 — REST의 표준은 끝났다, 그리고 시작됐다

OpenAPI 3.0과 3.1의 핵심 차이는 한 줄이다. 3.1은 JSON Schema 2020-12에 완전히 정렬됐다. 3.0 시대의 nullable: true는 사라지고 "type": ["string", "null"]이 표준이다. example/examples의 의미가 정리됐다. $ref가 더 자유로워졌다 — JSON Schema 어디든 가리킬 수 있다.

2026년에 OpenAPI 3.1은 REST API 스펙의 사실상 표준이다. Swagger 시대의 흔적은 거의 사라졌고, 새 프로젝트는 거의 모두 3.1로 시작한다. OpenAPI 3.2 작업이 진행되고 있지만(폼·헤더·다중 응답 콘텐츠 같은 작은 표면을 다듬는 중), 3.1과 3.2는 깨는 변경 없이 진화하는 관계다. 지금 3.1로 시작해도 안전하다.

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0
paths:
  /users/{userId}/orders:
    get:
      summary: List orders for a user
      parameters:
        - name: userId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Order'
components:
  schemas:
    Order:
      type: object
      required: [id, userId, total]
      properties:
        id:    { type: string, format: uuid }
        userId:{ type: string, format: uuid }
        total: { type: number, minimum: 0 }
        status:
          type: string
          enum: [pending, paid, shipped, cancelled]

이 한 파일에서 생성되는 것들: TypeScript/Go/Python 클라이언트 SDK, 서버 스텁, Postman/Bruno 컬렉션, 인터랙티브 문서, mock 서버, 계약 테스트 픽스처, AI 에이전트가 호출할 도구 정의. 이게 2026년의 워크플로다 — 한 스펙에서 모든 산출물.

Swagger UI는 여전히 동작하지만, 2026년의 새 표준은 Stoplight ElementsScalar다. 둘 다 OpenAPI 3.1을 네이티브로 다루고, 다크모드·검색·요청 시도(try-it)·코드 샘플을 훨씬 깔끔하게 보여준다. Scalar는 특히 <script> 한 줄로 끝나는 가벼운 임베드로 빠르게 퍼졌다. Swagger UI는 "있어도 되는" 옵션이지, 더 이상 기본값이 아니다.


3장 · AsyncAPI 3.0 — 이벤트의 OpenAPI

REST는 OpenAPI가 있다. 그럼 카프카 토픽, MQTT 채널, WebSocket 스트림, Server-Sent Events는? 2026년의 답은 분명하다 — AsyncAPI 3.0.

AsyncAPI 2.x 시절의 가장 큰 비판은 "채널 = 토픽 = 동작 한 묶음"이라는 모호함이었다. 발행과 구독이 한 채널 정의 안에서 섞였다. 3.0은 이를 operations로 분리했다. 채널은 메시지가 흐르는 길이고, operation은 발행/수신이라는 행위다. 이 분리만으로도 카프카 같은 시스템을 모델링하기가 훨씬 깔끔해졌다.

AsyncAPI 3.0의 핵심 단어들:

  • channels — 메시지가 흐르는 경로(토픽·큐·채널).
  • operations — 그 채널에서 일어나는 행위(send·receive). 명시적이다.
  • messages — 메시지의 페이로드 스키마(여기서 JSON Schema 그대로 재사용).
  • servers — Kafka 브로커, MQTT 브로커, WebSocket 엔드포인트 등 프로토콜별 바인딩.
asyncapi: 3.0.0
info:
  title: Orders Events
  version: 1.0.0
servers:
  kafka-prod:
    host: kafka.prod.example.com:9092
    protocol: kafka
channels:
  orderCreated:
    address: orders.created.v1
    messages:
      OrderCreated:
        $ref: '#/components/messages/OrderCreated'
operations:
  publishOrderCreated:
    action: send
    channel: { $ref: '#/channels/orderCreated' }
components:
  messages:
    OrderCreated:
      payload:
        type: object
        required: [orderId, userId, createdAt]
        properties:
          orderId:   { type: string, format: uuid }
          userId:    { type: string, format: uuid }
          createdAt: { type: string, format: date-time }
          total:     { type: number, minimum: 0 }

AsyncAPI 3.0이 2026년에 자리잡은 이유는 두 가지다. 첫째, 이벤트 주도 시스템이 더 이상 "특수 케이스"가 아니다. 거의 모든 마이크로서비스 회사가 카프카·NATS·MQTT 중 하나를 쓴다. 둘째, AsyncAPI 생태계가 OpenAPI 흉내를 넘어 자기 도구를 갖췄다 — Studio, generator(서버 스텁·클라이언트), validator, 카탈로그. 사용성 격차가 거의 사라졌다.

REST와 이벤트가 같은 시스템에 섞여 있는 게 흔한 2026년에는, OpenAPI와 AsyncAPI를 한 저장소에서 함께 관리하는 패턴이 표준이다. 둘 다 JSON Schema를 기반으로 하니 메시지 페이로드를 공유하기도 쉽다.


4장 · GraphQL SDL — 프런트엔드와의 평화 협정

GraphQL은 2015년 페이스북이 발표했고, 2019년 GraphQL Foundation으로 옮겨갔으며, 2026년에도 여전히 강력한 영역을 갖고 있다. 특히 여러 클라이언트가 다른 모양의 데이터를 필요로 하는 곳 — BFF, 모바일 앱이 여러 종류 있는 콘텐츠 플랫폼, 대시보드.

GraphQL의 스키마는 **SDL(Schema Definition Language)**이라는 자체 DSL로 쓴다. OpenAPI/AsyncAPI/JSON Schema와 달리 JSON이 아니다.

type User {
  id: ID!
  email: String!
  age: Int
  orders(status: OrderStatus, limit: Int = 20): [Order!]!
}

type Order {
  id: ID!
  total: Float!
  status: OrderStatus!
  createdAt: DateTime!
}

enum OrderStatus {
  PENDING
  PAID
  SHIPPED
  CANCELLED
}

type Query {
  user(id: ID!): User
  orders(userId: ID!, status: OrderStatus): [Order!]!
}

scalar DateTime

GraphQL의 매력은 introspection — 스키마 자체가 API에서 쿼리 가능하다는 점이다. 클라이언트 도구는 라이브 서버에 붙어 스키마를 끌어오고, 코드젠을 돌려 타입 안전한 훅을 만든다. Apollo Studio, GraphiQL, GraphQL Code Generator, urql, Relay — 도구 생태계는 두껍다.

2026년의 GraphQL 트렌드:

  • Federation v2가 정착했다 — 여러 서비스의 GraphQL을 하나의 슈퍼그래프로 합치는 표준.
  • Persisted queries가 보안·성능의 기본값이 됐다 — 임의의 쿼리를 서버에 보내지 않고, 빌드 타임에 등록된 쿼리 해시만 보낸다.
  • REST와 공존한다. "전부 GraphQL로 갈아엎자"는 흐름은 사라졌고, "프런트엔드가 자주 변하는 곳만 GraphQL, 백엔드 간은 gRPC/REST"가 흔한 패턴이다.

GraphQL의 약점도 정직하게 보자. 캐싱이 까다롭다(URL이 하나라 HTTP 캐싱이 단순하지 않다). N+1 문제는 DataLoader 같은 패턴으로 풀어야 한다. 쿼리 비용 분석을 하지 않으면 클라이언트 한 명이 서버를 무너뜨릴 수 있다. 그리고 모든 곳에 맞지는 않는다 — CRUD 위주의 단순한 API라면 OpenAPI 쪽이 거의 항상 단순하다.


5장 · gRPC + Protobuf — 서비스 간의 기본값

서비스 간 호출이라면 2026년에도 여전히 gRPC + Protobuf가 기본값이다. 이유는 단순하다 — 작고, 빠르고, 다국어이고, 스트리밍이 일급이다.

Protobuf는 바이너리 스키마다. .proto 파일에 메시지·서비스를 정의하면, protoc(또는 더 흔히 Buf)가 언어별 코드를 만든다. 와이어 위에서는 JSON보다 훨씬 작고(필드 이름이 정수 태그로 압축), 파싱이 훨씬 빠르다.

syntax = "proto3";

package shop.v1;

import "google/protobuf/timestamp.proto";

message User {
  string id    = 1;
  string email = 2;
  int32  age   = 3;
  repeated string roles = 4;
}

message Order {
  string id      = 1;
  string user_id = 2;
  double total   = 3;
  OrderStatus status = 4;
  google.protobuf.Timestamp created_at = 5;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING     = 1;
  ORDER_STATUS_PAID        = 2;
  ORDER_STATUS_SHIPPED     = 3;
  ORDER_STATUS_CANCELLED   = 4;
}

service OrdersService {
  rpc GetOrders(GetOrdersRequest) returns (GetOrdersResponse);
  rpc StreamOrders(stream GetOrdersRequest) returns (stream Order);
}

message GetOrdersRequest  { string user_id = 1; }
message GetOrdersResponse { repeated Order orders = 1; }

2026년 Protobuf 생태계의 중심은 거의 Buf다. buf build, buf lint, buf breaking, buf generateprotoc의 사용성 문제를 모두 해결한 도구 모음. Buf Schema Registry는 protobuf 스키마의 깃허브 역할을 한다 — 버전·릴리스·종속성을 관리한다.

언제 gRPC인가:

  • 서비스 간 호출 — 마이크로서비스 내부 망.
  • 양방향 스트리밍 — 채팅, 라이브 데이터, 게임 서버.
  • 고성능이 정말 중요한 곳 — RPS가 매우 높거나 페이로드가 크거나.

언제 gRPC가 아닌가:

  • 브라우저에서 직접 호출 — gRPC-Web이 있지만 여전히 번거롭다. 차라리 REST/GraphQL.
  • 3자 공개 API — 외부 개발자가 쓰기엔 진입 장벽이 높다.
  • 디버깅을 평문으로 하고 싶을 때 — 와이어가 바이너리라 curl로 그냥 볼 수 없다.

흥미로운 흐름: Connect(Buf가 만든 프로토콜)가 등장하면서 같은 .proto에서 gRPC와 REST/JSON을 동시에 노출할 수 있다. 내부망은 gRPC로, 외부 클라이언트는 같은 핸들러를 JSON으로 — 한 코드베이스로 두 마리 토끼를 잡는다.


6장 · 같은 모델, 네 가지 포맷 — User-with-orders

추상적으로 비교하지 말고 같은 도메인을 옮겨보자. 사용자가 주문 목록을 갖는다.

6.1 JSON Schema (단독)

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "Order": {
      "type": "object",
      "required": ["id", "total", "status"],
      "properties": {
        "id":     { "type": "string", "format": "uuid" },
        "total":  { "type": "number", "minimum": 0 },
        "status": { "type": "string", "enum": ["pending","paid","shipped","cancelled"] }
      }
    }
  },
  "type": "object",
  "required": ["id", "email", "orders"],
  "properties": {
    "id":    { "type": "string", "format": "uuid" },
    "email": { "type": "string", "format": "email" },
    "orders":{ "type": "array", "items": { "$ref": "#/$defs/Order" } }
  }
}

6.2 OpenAPI 3.1 (REST)

paths:
  /users/{id}:
    get:
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UserWithOrders' }
components:
  schemas:
    UserWithOrders:
      type: object
      required: [id, email, orders]
      properties:
        id:    { type: string, format: uuid }
        email: { type: string, format: email }
        orders:
          type: array
          items: { $ref: '#/components/schemas/Order' }
    Order:
      type: object
      required: [id, total, status]
      properties:
        id:     { type: string, format: uuid }
        total:  { type: number, minimum: 0 }
        status: { type: string, enum: [pending, paid, shipped, cancelled] }

6.3 GraphQL SDL

type User {
  id: ID!
  email: String!
  orders: [Order!]!
}

type Order {
  id: ID!
  total: Float!
  status: OrderStatus!
}

enum OrderStatus { PENDING PAID SHIPPED CANCELLED }

type Query {
  user(id: ID!): User
}

6.4 Protobuf

message User {
  string id    = 1;
  string email = 2;
  repeated Order orders = 3;
}

message Order {
  string id     = 1;
  double total  = 2;
  OrderStatus status = 3;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING     = 1;
  ORDER_STATUS_PAID        = 2;
  ORDER_STATUS_SHIPPED     = 3;
  ORDER_STATUS_CANCELLED   = 4;
}

같은 모델, 네 가지 표현. 정보량은 비슷하지만 누구를 위해 쓰는가가 다르다. JSON Schema는 공통 문법, OpenAPI는 HTTP 노출, GraphQL은 클라이언트 친화, Protobuf는 와이어 효율. 도구를 고르는 일은 데이터 모양이 아니라 대화 상대를 정하는 일이다.


7장 · Smithy — AWS의 API IDL

Smithy는 AWS가 만든 API 정의 언어다. 처음엔 AWS 내부에서 자기 SDK들을 자동 생성하기 위해 만든 도구였지만, 2020년대 후반부터 외부로 풀려 활발히 진화하고 있다.

Smithy의 철학은 "프로토콜에 불가지인 모델". .smithy 파일로 모델을 한 번 정의하면, 거기서 여러 프로토콜로 투영할 수 있다 — REST(JSON over HTTP), AWS 고유 프로토콜, gRPC. OpenAPI는 결과물로 뽑힌다.

$version: "2.0"
namespace shop

service Orders {
    version: "2026-05-14"
    operations: [GetUser]
}

@http(method: "GET", uri: "/users/{userId}")
operation GetUser {
    input := {
        @httpLabel
        @required
        userId: String
    }
    output := {
        @required
        id: String
        @required
        email: String
        @required
        orders: OrderList
    }
}

list OrderList { member: Order }

structure Order {
    @required
    id: String
    @required
    total: Double
    @required
    status: OrderStatus
}

enum OrderStatus {
    PENDING
    PAID
    SHIPPED
    CANCELLED
}

Smithy의 강점:

  • 트레이트 시스템이 OpenAPI보다 강력하다 — 권한, 페이지네이션, idempotency 같은 횡단 관심사를 깔끔히 표현.
  • AWS SDK들(JS·Python·Go·Rust 등)이 모두 Smithy로 생성된다 — 산업적 검증.
  • 모델 → 여러 산출물 — OpenAPI도, 클라이언트 SDK도, 서버 스텁도 한 모델에서.

약점: 생태계가 OpenAPI보다 좁다. AWS 외부에서의 채택은 천천히 늘고 있지만 OpenAPI 3.1의 보편성에는 미치지 못한다. "AWS SDK를 만든다"는 시나리오가 아니라면 OpenAPI로 충분한 경우가 많다.


8장 · TypeSpec — Microsoft의 OpenAPI 저작 대안

OpenAPI 3.1 YAML을 손으로 쓰는 건 — 솔직히 — 길고, 반복적이고, 오타가 나기 쉽다. 그 통증의 답으로 Microsoft가 만든 게 TypeSpec(이전 이름 Cadl)이다.

TypeSpec은 TypeScript 비슷한 간결한 DSL로 API를 쓰고, 거기서 OpenAPI(또는 JSON Schema·Protobuf)를 생성한다. 작성성·재사용성·도구 지원이 OpenAPI 원시 YAML보다 한 단계 위다.

import "@typespec/http";
using TypeSpec.Http;

@service({ title: "Orders API" })
namespace ShopApi;

model Order {
  id: string;
  total: float64;
  status: OrderStatus;
}

enum OrderStatus { Pending, Paid, Shipped, Cancelled }

model User {
  id: string;
  email: string;
  orders: Order[];
}

@route("/users/{userId}")
interface Users {
  @get op getUser(@path userId: string): User;
}

같은 정보량의 OpenAPI YAML보다 훨씬 짧다. 그리고 컴파일된다 — TypeSpec 컴파일러가 OpenAPI 3.1, JSON Schema 2020-12, Protobuf, 클라이언트 SDK를 동시에 뽑는다. Azure의 새 서비스들은 거의 모두 TypeSpec으로 작성된다.

언제 TypeSpec인가:

  • API가 크고 복잡하다 — 수십 개 엔드포인트, 공통 모델·응답 패턴이 많다.
  • 한 정의에서 여러 산출물을 뽑고 싶다 — OpenAPI + Protobuf + 클라이언트.
  • 팀이 TypeScript에 익숙해 DSL이 친숙하다.

언제 TypeSpec이 아닌가:

  • API가 작다 — OpenAPI YAML 한 파일이 더 단순할 수 있다.
  • 도구 체인을 단순하게 유지하고 싶다 — TypeSpec은 빌드 단계를 하나 더 추가한다.

9장 · Pact — 계약 테스트, 스키마의 두 번째 사용처

지금까지의 포맷들은 모두 문서 + 코드 생성용이다. 그런데 스키마의 또 다른 강력한 용도가 있다 — 계약 테스트(consumer-driven contract testing). 그 대표가 Pact다.

상황: 서비스 A가 서비스 B를 호출한다. B가 응답 모양을 바꾸면 A가 깨진다. "통합 테스트"는 두 서비스를 동시에 띄워 호출해보지만 — 느리고 깨지기 쉽다. Pact는 다른 접근을 한다.

소비자 주도 계약: A(소비자)가 "나는 B에게 이렇게 요청하고, 이런 모양의 응답을 기대한다"를 계약 파일로 적는다. 이 계약을 Pact Broker에 올린다. B(제공자)는 CI에서 자기 코드가 그 계약을 만족시키는지 검증한다. B가 응답 모양을 바꾸면 — 그리고 A가 그걸 의존한다면 — B의 CI에서 즉시 실패한다.

// 소비자 측 테스트
provider
  .uponReceiving('a request for a user')
  .withRequest({ method: 'GET', path: '/users/123' })
  .willRespondWith({
    status: 200,
    body: { id: '123', email: 'a@b.com', orders: eachLike({ id: like('o1'), total: like(99.0) }) },
  })

Pact는 OpenAPI/AsyncAPI와 경쟁하는 게 아니라 보완한다. OpenAPI는 "API가 이래야 한다"는 정의고, Pact는 "이 소비자가 실제로 이걸 의존한다"는 증거다. 둘을 같이 쓰는 팀이 늘었다.


10장 · AI 도구 호출 — JSON Schema의 두 번째 봄

2024~2025년에 LLM이 도구를 호출하기 시작하면서 JSON Schema에 두 번째 봄이 왔다. OpenAI·Anthropic·Gemini의 도구 정의 포맷은 전부 JSON Schema 서브셋이다. 모델은 그 스키마에 맞춰 arguments를 JSON으로 채워 반환한다.

같은 User-with-orders 도메인에서 "사용자 주문 목록 조회" 도구를 정의해 보자.

{
  "name": "get_user_orders",
  "description": "Return all orders for the given user",
  "input_schema": {
    "type": "object",
    "required": ["user_id"],
    "properties": {
      "user_id": { "type": "string", "format": "uuid" },
      "status": {
        "type": "string",
        "enum": ["pending","paid","shipped","cancelled"]
      },
      "limit":  { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
    },
    "additionalProperties": false
  }
}

이게 OpenAPI 안의 components.schemas와 거의 똑같다는 것에 주목하자. 2026년의 깔끔한 워크플로는 이렇다:

  1. OpenAPI 3.1로 진실의 원본을 둔다.
  2. 같은 스펙에서 서버 스텁, 클라이언트 SDK를 뽑는다.
  3. 같은 스펙의 components.schemasAI 도구 정의로 매핑한다 — 한 핸들러가 사람과 모델 양쪽에 노출된다.
  4. 같은 스키마로 도구 호출 인자를 검증한다 — 모델이 잘못 채우면 즉시 거부.

"API"와 "AI 에이전트의 도구"가 같은 스키마를 공유한다는 건 2026년의 흔한 현실이다. JSON Schema 2020-12를 단단히 이해하는 게 그래서 백엔드 엔지니어에게 베이스 스킬이 됐다.


11장 · Spec-first vs Code-first — 끝나지 않은 논쟁

OpenAPI/AsyncAPI 시대의 큰 논쟁이 두 가지 진영이다.

Code-first: 코드(데코레이터·타입)를 먼저 쓰고, 거기서 스펙을 생성한다. FastAPI, NestJS, tRPC가 대표. 장점은 단일 진실 — 코드가 스펙이다. 단점은 스펙이 코드의 그림자가 되어, 비기술 이해관계자와의 협의가 늦어지고, 언어 표현력에 갇힌다.

Spec-first: 스펙을 먼저 쓰고, 거기서 서버 스텁과 클라이언트를 생성한다. OpenAPI YAML, TypeSpec, Smithy가 대표. 장점은 프로토콜 명세가 코드보다 먼저 존재한다 — API 디자이너·QA·외부 클라이언트가 동시에 본다. 단점은 두 곳에서 진실이 미끄러질 위험 — 스펙과 핸들러가 어긋날 수 있다.

2026년의 균형점:

  • 외부에 노출되는 API, 여러 클라이언트(언어·팀)가 있는 API → spec-first가 거의 항상 유리.
  • 내부 단일 서비스, 빠른 프로토타이핑, 풀스택 한 팀code-first가 빠르다.
  • 하이브리드 가능: code-first로 시작해 스펙이 안정되면 spec-first로 고착하는 흐름.

도구 차이도 좁아졌다. TypeSpec·Smithy 같은 고수준 spec-first 도구는 작성 마찰을 크게 줄였고, FastAPI 같은 대표적 code-first 도구는 거꾸로 스펙 정합성 검증을 강화했다. 진영 논쟁보다 "이 API의 수명클라이언트 수"를 보는 게 정직하다.


12장 · 도구 체인 — 2026년 표준 스택

영역표준에 가까운 도구
OpenAPI 문서 UIScalar, Stoplight Elements (Swagger UI는 레거시)
OpenAPI 편집기Stoplight Studio, Insomnia, Postman
코드 생성(클라이언트)openapi-typescript, oapi-codegen, openapi-generator
Mock 서버Prism (Stoplight), Mockoon
계약 테스트Pact (Pact Broker, PactFlow)
Protobuf 도구체인Buf (lint, breaking-change, BSR), protoc (저수준)
Protobuf → RESTConnect (Buf), gRPC-Gateway
AsyncAPI 편집기AsyncAPI Studio
AsyncAPI 코드젠AsyncAPI Generator
GraphQL 도구Apollo Studio, GraphiQL, GraphQL Code Generator, Hasura, Relay
TypeSpectsp CLI, VS Code 확장
Smithysmithy build, IntelliJ 플러그인

Swagger UI를 기본으로 두는 시대는 끝났다. Stoplight Elements와 Scalar 둘 다 OpenAPI 3.1을 깔끔히 다루고, 다크모드·검색·코드 샘플·테마가 훨씬 낫다. 새 프로젝트에서 Scalar의 임베드 스니펫 한 줄로 문서 페이지가 끝난다.


13장 · 의사결정 프레임 — 우리 팀은 무엇을 써야 하나

같은 "API"라도 대화 상대가 다르면 답이 달라진다.

질문 1: 대화 상대는 누구인가?
  ├─ 외부 개발자(공개 API)           → OpenAPI 3.1 + Scalar/Stoplight
  ├─ 사내 프런트엔드(여러 모양 필요) → GraphQL (Apollo Federation)
  ├─ 내부 서비스 ↔ 서비스           → gRPC + Protobuf (Buf)
  ├─ 이벤트(카프카·MQTT·WebSocket)   → AsyncAPI 3.0
  └─ AI 에이전트의 도구             → JSON Schema 2020-12 (OpenAPI에서 재사용)

질문 2: API의 수명은?
  ├─ 단기 프로토타입          → code-first (FastAPI 등)로 빠르게
  └─ 장기·외부 노출           → spec-first (OpenAPI/TypeSpec/Smithy)로 단단히

질문 3: 한 정의에서 여러 산출물이 필요한가?
  ├─ 예 (REST + gRPC + 클라이언트 SDK) → TypeSpec 또는 Smithy
  └─ 아니오                            → OpenAPI 단독으로 충분

질문 4: 클라이언트가 깨지는 걸 얼마나 두려워하는가?
  ├─ 매우 두렵다 → Pact로 계약 테스트 추가
  └─ 적당히       → 스펙 + 코드젠으로 충분

추가로, 2026년에 권장하지 않는 조합:

  • 새 프로젝트를 **OpenAPI 2.0(Swagger 2)**으로 시작하는 것 — 10년 묵은 규격이다. 무조건 3.1.
  • gRPC를 브라우저에 직접 노출 — gRPC-Web/Connect를 거치거나 GraphQL/REST를 별도 BFF로.
  • protoc를 직접 호출 — Buf가 거의 모든 면에서 우위.
  • Swagger UI를 새 표준으로 두는 것 — 2026년에는 Scalar 또는 Stoplight Elements.
  • GraphQL의 임의 쿼리를 외부에 그대로 — persisted queries로 화이트리스트.

14장 · 마이그레이션 — 흔한 경로 세 가지

A. OpenAPI 3.0 → 3.1

가장 흔한 마이그레이션이다. 호환성이 매우 높아 대부분 기계적으로 끝난다:

  1. nullable: true"type": ["string", "null"] (또는 anyOf로 null).
  2. exampleexamples (스칼라 1개라면 그대로 둬도 됨).
  3. 일부 도구의 메타 어노테이션(x- 확장) 점검.
  4. CI에 OpenAPI 3.1 검증기 추가.

B. 단일 OpenAPI → OpenAPI + AsyncAPI 분리

REST와 이벤트가 한 파일에 어지럽게 섞여 있던 코드베이스에서, AsyncAPI로 이벤트를 분리한다:

  1. 카프카 토픽·메시지를 추출해 AsyncAPI 3.0 파일로 옮긴다.
  2. 메시지 페이로드 스키마는 공유 JSON Schema 파일로 떼어내 둘 다 $ref로 가리킨다.
  3. 한 저장소에 openapi.yaml, asyncapi.yaml, schemas/가 나란히 산다.

C. 손으로 쓰는 OpenAPI → TypeSpec/Smithy로 끌어올리기

API가 커지고 반복이 심해질 때:

  1. 기존 OpenAPI를 TypeSpec/Smithy로 임포트한다(공식 변환기 존재).
  2. 공통 모델·트레이트(권한·페이지네이션·idempotency)를 횡단으로 추출.
  3. TypeSpec/Smithy를 진실의 원본으로, OpenAPI는 생성 산출물로 격하.

세 경로 모두 큰 빅뱅이 아니라 점진적이다. 2026년의 흔한 실수는 "한 번에 다 갈아엎자"인데, 거의 항상 실패한다. 한 영역(예: 한 서비스, 한 토픽)을 옮긴 뒤 패턴을 굳히고 확장한다.


15장 · 신뢰성 — 스펙이 실제로 지켜지게 만들기

스키마가 진실이 되려면 지켜져야 한다. 2026년의 흔한 가드레일:

  1. CI에서 OpenAPI/AsyncAPI 린트 — Spectral, Redocly가 표준. 스타일·금지 패턴·필수 필드 검사.
  2. Breaking-change 검출 — Buf의 buf breaking, OpenAPI는 oasdiff. PR이 스펙을 깨면 즉시 빨간불.
  3. 서버 응답을 실제로 스펙과 대조 — 라이브 트래픽 일부를 스펙 검증기에 흘려본다(예: Optic, prism --validate).
  4. 계약 테스트(Pact) — 소비자가 실제로 의존하는 모양만 잡아낸다.
  5. AI 도구 호출의 인자 검증 — 모델이 잘못 채우면 즉시 거부하고 모델에게 정정 요청.

이 다섯이 다 있을 필요는 없지만 하나는 있어야 한다. "스펙이 있다"와 "스펙이 지켜진다"는 다른 일이다.


에필로그 — 스키마가 진실이다

2026년 백엔드의 한 줄 정리: API의 단위는 엔드포인트가 아니라 스키마다.

올바른 스키마 한 파일에서 — 문서·클라이언트·서버 스텁·테스트 픽스처·AI 도구 정의가 전부 나온다. 좋은 스키마는 외부 개발자에게 "지금 무엇이 있는가"를 보여주고, 내부 팀에게 "무엇이 깨지면 안 되는가"를 알려주고, AI 에이전트에게 "어떻게 부르면 되는가"를 가르친다.

도구는 많다 — JSON Schema, OpenAPI 3.1, AsyncAPI 3.0, GraphQL, Protobuf, Smithy, TypeSpec, Pact. 외워야 할 이름이 늘어난 게 아니라, 역할이 분화된 것이다. 한 도구가 모든 상황을 이기는 일은 없다. 좋은 백엔드 엔지니어는 대화 상대에 따라 도구를 고른다.

14개 항목 체크리스트

  1. 새 프로젝트의 OpenAPI는 3.1이다(2.0/3.0이 아니라).
  2. 같은 저장소에 REST(OpenAPI)와 이벤트(AsyncAPI)가 나란히 있다.
  3. 메시지 페이로드 스키마는 공유 JSON Schema로 떼어 양쪽이 $ref한다.
  4. Swagger UI 대신 Scalar 또는 Stoplight Elements를 쓴다.
  5. CI에 OpenAPI/AsyncAPI 린트(Spectral 등)가 있다.
  6. Breaking-change 감지가 CI에 있다(buf breaking, oasdiff).
  7. 클라이언트 SDK가 스펙에서 자동 생성된다.
  8. 서버 스텁/타입도 스펙에서 생성된다(손코딩 금지).
  9. Protobuf 도구체인은 Buf다 — protoc를 직접 쓰지 않는다.
  10. gRPC를 브라우저에 직접 노출하지 않는다(Connect/gRPC-Web/BFF).
  11. AI 도구 정의가 같은 JSON Schema 컴포넌트를 재사용한다.
  12. AI 도구 호출의 인자를 실행 전에 스키마로 검증한다.
  13. 핵심 통합에는 Pact(또는 동등한 계약 테스트)가 있다.
  14. GraphQL은 임의 쿼리가 아니라 persisted queries로 화이트리스트된다.

안티패턴 10가지

  1. 코드와 스펙을 둘 다 손으로 유지하려는 시도 — 반드시 미끄러진다.
  2. 모든 곳에 GraphQL을 — CRUD에는 OpenAPI가 더 단순하다.
  3. 모든 곳에 gRPC를 — 브라우저·외부 개발자에겐 마찰이 크다.
  4. OpenAPI는 있지만 검증되지 않는 상태 — 그건 위키 문서다, 스펙이 아니다.
  5. 이벤트를 OpenAPI에 억지로 박는 것 — AsyncAPI 3.0이 답이다.
  6. Swagger 2를 오늘 시작하는 것 — 10년 전 규격이다.
  7. protoc를 손으로 호출 — Buf가 모든 면에서 우위.
  8. Smithy를 AWS SDK 만들 때가 아닌데 강제로 — 생태계 가성비가 낮다.
  9. AI 도구 정의를 OpenAPI와 별도로 유지 — 같은 컴포넌트 재사용이 정답.
  10. 한 번에 모든 API를 spec-first로 갈아엎는 빅뱅 — 점진적이지 않으면 실패.

다음 글 예고

다음 글 후보: Connect 프로토콜 심층 — Buf가 만든 gRPC + REST의 통합, AsyncAPI 3.0으로 사내 이벤트 카탈로그 만들기, AI 도구 호출 스키마 설계 — JSON Schema를 모델 친화적으로 쓰는 법.

"엔드포인트가 아니라 스키마다. 2026년의 좋은 API는 손으로 코딩되는 게 아니라, 스키마에서 떨어진다."

— 2026 API 스키마 지도, 끝.

참고 / References

현재 단락 (1/498)

2026년의 백엔드 회의는 이렇게 시작한다. "엔드포인트부터 짜지 말고, 스키마부터 보여줘."

작성 글자: 0원문 글자: 18,472작성 단락: 0/498