OpenAPI는 HTTP API를 평범한 YAML 또는 JSON 파일로 기술하는 형식입니다. API가 어떤 경로를 공개하는지, 각 경로가 어떤 메서드를 받는지, 요청에 어떤 필드가 실리는지, 응답으로 무엇이 돌아오는지를 그 파일에 적어 둡니다. 사람도 프로그램도 읽을 수 있기 때문에 문서, 클라이언트 라이브러리, 테스트용 목 서버를 모두 같은 원본 하나에서 만들어 낼 수 있습니다.
정식 명칭은 OpenAPI Specification, 줄여서 OAS입니다. Linux Foundation 산하의 개방형 거버넌스 프로젝트인 OpenAPI Initiative가 관리합니다.
아래에서는 문서가 어떻게 구성되는지, OpenAPI와 Swagger가 어떻게 다른지, 3.1과 3.2에서 무엇이 바뀌었는지, 그리고 이 형식의 핵심 약속인 "문서가 낡지 않는다"가 왜 저절로 지켜지지 않는지를 다룹니다.
OpenAPI 문서의 모습
동작하는 파일은 생각보다 짧습니다.
openapi: 3.1.0
info:
title: 링크
version: "1.0"
paths:
/links:
post:
summary: 단축 링크 생성
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [url]
properties:
url:
type: string
format: uri
responses:
'201':
description: 링크가 생성됨
최상위 필수 필드는 openapi와 info 둘뿐이고, 그 밖에 paths, components, webhooks 중 최소 하나를 포함해야 합니다. OAS 3.1.1 명세가 그렇게 규정합니다.
바로 이 첫 줄들에 함정이 하나 숨어 있습니다. openapi와 info.version은 서로 다른 것입니다. 앞의 것은 형식 자체의 버전으로, 도구는 이 값을 보고 문서를 어떻게 해석할지 판단합니다. 뒤의 것은 여러분 API의 버전입니다. 명세에 분명히 적혀 있지만 직관은 반대로 속삭이기 때문에 openapi: 1.0이라고 선언한 파일이 심심찮게 나옵니다.
규모가 커지면 명세는 보통 $ref로 여러 파일에 나눕니다. 루트 문서가 ./paths/links.yaml을 가리키고, 그것이 다시 ./schemas/link.yaml을 가리키는 식입니다. 편리하지만 문제도 함께 생깁니다. 이 이야기는 마지막에 하겠습니다.
OpenAPI와 Swagger는 같은 것이 아닙니다
이 주제에서 가장 흔한 혼동입니다.
| 무엇인가 | 누가 관리하는가 | |
|---|---|---|
| OpenAPI | 표준 그 자체, 즉 형식 | OpenAPI Initiative, Linux Foundation |
| Swagger | 그 형식을 읽는 도구 모음 | SmartBear |
혼란의 원인은 내력에 있습니다. 처음에 Swagger는 명세와 도구를 한꺼번에 가리키는 말이었습니다. 이후 명세가 OpenAPI Initiative로 넘어가 OpenAPI Specification이라는 이름을 얻었고, Swagger라는 이름은 도구 쪽에 남았습니다. Swagger UI, Swagger Editor 같은 것들이죠.
실무에서 "우리는 Swagger를 씁니다"는 거의 언제나 "OpenAPI 파일이 있고 그것을 Swagger UI로 보여 준다"는 뜻입니다. 둘이 묶여 있는 것은 아닙니다. 같은 파일을 Redoc이나 Scalar, 클라이언트 생성기로 열어도 똑같이 동작합니다.
버전: 3.0, 3.1, 3.2
갈래 사이의 차이는 겉모습 문제가 아닙니다.
3.0은 지금도 가장 널리 쓰입니다. 약점은 데이터 스키마입니다. JSON Schema를 닮았으면서도 완전히 호환되지는 않아서 도구 사이의 상호 운용이 자꾸 깨집니다.
3.1이 그 틈을 메웠습니다. Schema 객체가 JSON Schema Draft 2020-12의 상위 집합이 되어, 이미 JSON Schema로 관리하던 스키마를 다시 쓰지 않고 그대로 재사용할 수 있습니다. 현행 개정판 3.1.1은 2024년 10월 24일에 공개됐습니다.
3.2는 2025년 9월에 나왔습니다. OpenAPI Initiative의 발표에서 눈에 띄는 것들을 꼽으면 이렇습니다.
- 매개변수가 URL에 들어가지 않는 멱등 조회를 위한
query메서드; - 비표준 메서드를 위한
additionalOperations; - 스트리밍 미디어 타입: Server-Sent Events, JSON Lines, JSON 시퀀스;
summary,parent,kind로 계층을 갖게 된 태그;- 입력이 불편한 기기를 위한 OAuth 2.0 디바이스 플로.
버전은 숫자가 큰 쪽이 아니라 손에 있는 도구가 실제로 지원하는 쪽으로 고르는 편이 낫습니다. 생태계의 3.1 지원은 탄탄하고, 3.2는 더 새로운 만큼 일부 도구가 아직 따라오는 중입니다. 저희 명세는 3.1.0으로 선언되어 있습니다.
실제로 무엇을 얻는가
파일 하나가 여러 일을 동시에 해냅니다.
- 대화형 문서 — Swagger UI나 Redoc이 파일에서 곧바로 페이지를 만들어 주고, 요청을 시험해 보는 양식까지 딸려 옵니다;
- 클라이언트 라이브러리 — 생성기가 원하는 언어로 SDK를 뽑아 주므로 래퍼를 손으로 쓸 일이 없습니다;
- 목 서버 — 백엔드가 완성되기 전에 기술만 보고 목 서버를 띄울 수 있어 프런트엔드가 기다리지 않습니다;
- 계약 테스트 — 테스트가 실제 응답과 스키마를 맞대어 보고, 응답이 기술과 어긋나기 시작한 순간을 잡아냅니다.
OpenAPI를 지지하는 핵심 논거가 이 목록에서 나옵니다. 문서가 누군가 갱신을 잊어버리는 별개의 글이 아니게 된다는 것이죠.
논거 자체는 옳습니다. 다만 저절로 이루어지지는 않습니다.
명세와 코드를 잇는 것은 아무것도 없다
"OpenAPI란 무엇인가"를 다루는 글들이 거의 건드리지 않는 부분이자, 운영에서 실제로 무는 부분입니다.
명세 파일은 그냥 파일입니다. 라우터는 그 존재를 모릅니다. 컨트롤러도 마찬가지입니다. 엔드포인트를 지워도, 새로 더해도, 필수 필드를 선택 필드로 바꿔도 테스트는 하나도 깨지지 않습니다. 문서는 스스로 동기화되지 않습니다. 누군가 고칠 것을 기억하는 동안만 정확한 상태로 남습니다.
어긋남은 양방향으로 생기고, 방향마다 해악이 다릅니다.
- 적혀 있는데 동작하지 않는다. 개발자가 문서를 읽고 요청을 보냈더니 404가 돌아옵니다. 나머지 문서에 대한 신뢰가 그 자리에서 무너집니다. 여기가 거짓이면 다른 곳은 어떨까요.
- 동작하는데 적혀 있지 않다. 그것을 찾는 사람에게 그 기능은 없는 것과 같습니다. 아무도 발견하지 못하고, 아무도 대가를 치르지 않습니다.
더 위험한 쪽은 두 번째입니다. 아무도 불평하지 않기 때문입니다. 불평할 사람도 없습니다. 사용자는 그 엔드포인트가 있다는 사실 자체를 모르니까요.
저희가 바로 그랬습니다. Lix.li API에는 전환 관련 엔드포인트 세 개가 멀쩡히 돌아가고 있었습니다. 이벤트 목록 하나에 포스트백 수신이 단건과 일괄로 둘이었죠. 그런데 그중 어느 것도 명세에 없었습니다. 코드는 동작했고 테스트도 통과했으며, 그러는 동안 API 소개 페이지에는 코드에 존재하지도 않는 전혀 다른 경로 두 개가 걸려 있었습니다. 이 사실은 장애 신고가 아니라 전수 대조 과정에서 드러났습니다.
어긋남을 자동으로 잡아내기
이것은 각오가 아니라 테스트로 해결됩니다. 발상은 단순합니다. 명세에서 경로 목록을 뽑고, 라우터에서 규칙 목록을 뽑아 두 집합을 양방향으로 맞대어 보는 것입니다.
openapi.yaml의 경로 → 해당 라우트가 있는가? → 없으면 문서가 거짓말 중
라우터의 라우트 → 명세에 적혀 있는가? → 없으면 기능이 보이지 않음
"적힌 것은 동작한다" 쪽은 실제 요청으로 확인하는 편이 편합니다. 자격 증명 없이 엔드포인트를 호출해 응답이 404가 아니라 401인지 확인하면 됩니다. 이 구분이 중요합니다. 401은 라우트를 찾았고 키를 요구한다는 뜻이고, 404는 그런 경로가 없다는 뜻입니다. 이 과정에서 기록되는 것은 없습니다. 인증이 컨트롤러보다 먼저 실행되기 때문입니다.
반대 방향 검사는 라우트 설정을 읽으면 됩니다. 이 작업은 예외를 명시적으로 나열하도록 강제하기도 합니다. 공개 계약에 들어갈 이유가 없는 내부 엔드포인트들 말입니다. 이 예외 목록은 그 자체로 값어치가 있습니다. 무엇을 우리의 공개 API로 볼 것인지 한 번은 제대로 결정하게 만들기 때문입니다.
전환 건을 겪은 뒤 저희는 이 테스트를 작성했습니다. 값어치는 곧바로 증명됐습니다. 명세에서 전환 관련 경로를 다시 빼면 테스트가 실패하면서 빠진 경로 두 개를 하나씩 짚어 줍니다.
뒤늦게 알게 되는 함정 둘
$ref와 상대 경로. 명세를 여러 파일로 나누면 내부 참조는 그 파일이 제공된 주소를 기준으로 해석됩니다. /openapi에 놓인 루트 문서는 ./paths/links.yaml을 /paths/links.yaml에서 찾고 아무것도 발견하지 못합니다. 브라우저에서는 멀쩡해 보입니다. Swagger UI가 파일을 원래 위치에서 불러오기 때문입니다. 그러나 같은 문서를 가리킨 클라이언트 생성기는 걸려 넘어집니다. 명세는 자기 디렉터리에서만 제공하든지, 외부 $ref가 없는 단일 파일로 묶든지 해야 합니다.
Swagger UI는 크롤러에게 비어 있습니다. Swagger UI 페이지는 몇 줄의 마크업과 내용을 브라우저에서 그려 내는 스크립트가 전부입니다. 전달되는 HTML에는 엔드포인트 경로가 하나도 없습니다. 읽는 사람에게는 멀쩡한 페이지지만 색인 관점에서는 백지입니다. 여기에 robots.txt의 흔한 실수가 겹칩니다. JSON 엔드포인트를 가리려고 쓴 Disallow: /api/ 규칙이 같은 접두사 아래 있는 사람용 문서까지 함께 가려 버리는 것이죠. 막아야 할 것은 버전이 붙은 정확한 경로, 예를 들어 /api/1.0/이지 구역 전체가 아닙니다.
어디서부터 시작할까
API는 이미 있는데 기술이 없다면 순서는 대략 이렇습니다.
- 엔드포인트 하나를 손으로 적어 봅니다. 구조를 파악하기에는 그것으로 충분합니다.
- 손에 있는 도구가 무리 없이 다루는 버전을 고릅니다. 기본값은 3.1입니다.
- 파일이 한 화면에 들어가지 않게 되면
$ref로 나누고, 어느 주소에서 제공할지도 그 자리에서 정합니다. - 명세와 라우터를 양방향으로 대조하는 테스트를 둡니다. 이 단계를 밟기 전까지 문서가 최신이라는 말은 전부 약속일 뿐 성질이 아닙니다.
실제 API에서 어떤 모습인지는 Lix.li API 문서에서 볼 수 있습니다. 링크, 그룹, A/B 테스트, 전환을 다룹니다. 이 API를 중심으로 만들어진 서비스는 단축 링크 API 페이지에, 가까운 주제로는 webhook이란 무엇인가와 301, 302, 307, 308 리다이렉션과 요청 메서드 분석이 있습니다.