필사 모드: 2026 API 스키마 지도 — JSON Schema · OpenAPI 3.1 · AsyncAPI · GraphQL · gRPC · Smithy · TypeSpec 한 번에 정리
한국어프롤로그 — 스키마는 이제 코드보다 먼저 온다
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 Elements**와 **Scalar**다. 둘 다 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;
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 generate` — `protoc`의 사용성 문제를 모두 해결한 도구 모음. 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보다 한 단계 위다.
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.schemas`를 *AI 도구 정의*로 매핑한다 — 한 핸들러가 사람과 모델 양쪽에 노출된다.
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 문서 UI | **Scalar**, **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 → REST | **Connect** (Buf), gRPC-Gateway |
| AsyncAPI 편집기 | AsyncAPI Studio |
| AsyncAPI 코드젠 | AsyncAPI Generator |
| GraphQL 도구 | Apollo Studio, GraphiQL, GraphQL Code Generator, Hasura, Relay |
| TypeSpec | `tsp` CLI, VS Code 확장 |
| Smithy | `smithy 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. `example` → `examples` (스칼라 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
- [JSON Schema — draft 2020-12 specification](https://json-schema.org/specification)
- [JSON Schema — Getting Started](https://json-schema.org/learn/getting-started-step-by-step)
- [OpenAPI Specification 3.1.0](https://spec.openapis.org/oas/v3.1.0)
- [OpenAPI Initiative](https://www.openapis.org/)
- [OpenAPI 3.2 work-in-progress](https://github.com/OAI/OpenAPI-Specification)
- [AsyncAPI 3.0 specification](https://www.asyncapi.com/docs/reference/specification/v3.0.0)
- [AsyncAPI Initiative](https://www.asyncapi.com/)
- [AsyncAPI 3.0 release notes](https://www.asyncapi.com/blog/asyncapi-v3-major-release)
- [GraphQL spec — June 2018 (current)](https://spec.graphql.org/October2021/)
- [GraphQL Foundation](https://graphql.org/)
- [Apollo Federation v2 docs](https://www.apollographql.com/docs/federation/)
- [Protocol Buffers — google/protobuf](https://protobuf.dev/)
- [gRPC project home](https://grpc.io/)
- [Buf — Protobuf toolchain](https://buf.build/)
- [Buf Schema Registry (BSR)](https://buf.build/product/bsr)
- [Connect protocol — Buf](https://connectrpc.com/)
- [Smithy — AWS API IDL](https://smithy.io/)
- [Smithy GitHub — smithy-lang/smithy](https://github.com/smithy-lang/smithy)
- [TypeSpec — Microsoft](https://typespec.io/)
- [TypeSpec GitHub — microsoft/typespec](https://github.com/microsoft/typespec)
- [Pact docs](https://docs.pact.io/)
- [PactFlow](https://pactflow.io/)
- [Stoplight Elements](https://stoplight.io/open-source/elements)
- [Scalar — API docs](https://scalar.com/)
- [Redocly](https://redocly.com/)
- [Spectral — OpenAPI/AsyncAPI linter](https://stoplight.io/open-source/spectral)
- [oasdiff — OpenAPI diff](https://www.oasdiff.com/)
- [OpenAI — function calling](https://platform.openai.com/docs/guides/function-calling)
- [Anthropic — tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use)
- [Gemini — function calling](https://ai.google.dev/gemini-api/docs/function-calling)
- [openapi-typescript](https://github.com/openapi-ts/openapi-typescript)
- [openapi-generator](https://openapi-generator.tech/)
현재 단락 (1/498)
2026년의 백엔드 회의는 이렇게 시작한다. "엔드포인트부터 짜지 말고, 스키마부터 보여줘."