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 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년의 백엔드 회의는 이렇게 시작한다. "엔드포인트부터 짜지 말고, 스키마부터 보여줘."

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