대부분의 API 버그는 코딩 실수가 아닙니다. 합의 오류입니다. 프론트엔드가 userId를 예상했지만 백엔드는 user_id를 보냈고, QA에서 발견할 때까지 아무도 알아채지 못하는 경우가 대표적입니다. 스펙 우선(Spec-first) API 개발은 문서를 마지막에 작성하는 대신, 계약을 가장 먼저 작성해 이런 문제를 줄입니다.
이 글에서는 작은 OpenAPI 스펙을 직접 작성하고, 이 단일 파일로 서버 코드가 존재하기 전에 목(mock), 테스트, 문서를 생성하는 흐름을 구현 중심으로 살펴봅니다. 같은 접근 방식은 스펙 주도 개발(spec-driven development), 디자인 우선(design-first), 계약 우선(contract-first)이라고도 부릅니다. 핵심은 동일합니다. 인터페이스에 먼저 합의하고, 그 계약에 맞춰 구현합니다.
스펙 우선 API 개발이란 무엇인가요?
스펙 우선 API 개발은 엔드포인트를 구현하기 전에 기계가 읽을 수 있는 계약을 작성하는 방식입니다. 일반적으로 OpenAPI 문서를 사용합니다.
이 계약에는 다음 항목이 포함됩니다.
- 경로(path)
- HTTP 메서드
- 쿼리 및 경로 매개변수
- 요청 본문
- 응답 본문
- 상태 코드
- 공통 스키마
- 오류 응답
스펙은 코드 뒤에 붙는 문서가 아닙니다. 개발의 기준점입니다.
실제 워크플로는 다음처럼 진행됩니다.
- API 계약을 OpenAPI로 작성합니다.
- 프론트엔드는 스펙 기반 목 서버로 UI를 개발합니다.
- 백엔드는 스펙을 만족하도록 구현합니다.
- QA는 스펙을 기준으로 테스트 케이스를 작성합니다.
- CI에서 실제 응답이 스펙과 일치하는지 검증합니다.
- 문서는 동일한 스펙에서 자동 렌더링합니다.
이렇게 하면 통합 단계에서 발견되던 필드명, 타입, 상태 코드 불일치를 프로젝트 초반에 검토할 수 있습니다.
스펙 우선 vs 코드 우선 라이프사이클
두 접근 방식 모두 같은 API를 만들 수 있습니다. 차이는 계약이 언제 확정되는지와 팀이 언제 병렬로 작업할 수 있는지입니다.
코드 우선 방식에서는 보통 다음 순서로 진행됩니다.
- 백엔드가 핸들러를 구현합니다.
- 문서를 나중에 작성합니다.
- 프론트엔드와 QA가 실제 동작을 확인하며 맞춥니다.
- 불일치가 통합 단계에서 발견됩니다.
스펙 우선 방식에서는 순서가 바뀝니다.
- API 스펙을 먼저 작성합니다.
- 리뷰를 통해 계약을 확정합니다.
- 목, 테스트, 문서를 스펙에서 생성합니다.
- 각 팀이 같은 파일을 기준으로 병렬 작업합니다.
- 구현 결과를 스펙과 비교해 검증합니다.
계약이 먼저 존재하면 프론트엔드, 백엔드, QA가 서로를 기다리지 않고 하나의 공유된 정의에 따라 작업할 수 있습니다.
작업된 OpenAPI 예시
작은 /users API를 단계별로 설계해 보겠습니다.
목표는 다음 두 작업입니다.
-
GET /users: 사용자 목록 조회 -
POST /users: 사용자 생성
1. OpenAPI 문서 헤더 작성
먼저 OpenAPI 버전, API 이름, 버전, 서버 URL을 정의합니다.
openapi: 3.0.3
info:
title: Users API
version: 1.0.0
servers:
- url: https://api.example.com/v1
여기까지는 API의 기본 메타데이터입니다. 아직 엔드포인트는 정의하지 않았습니다.
2. 공통 User 스키마 정의
다음으로 components.schemas 아래에 User 스키마를 정의합니다.
components:
schemas:
User:
type: object
required:
- id
- email
- createdAt
properties:
id:
type: string
format: uuid
email:
type: string
format: email
name:
type: string
createdAt:
type: string
format: date-time
이 스키마는 여러 응답에서 재사용할 수 있습니다. 같은 구조를 여러 곳에 복사하지 않고 $ref로 참조하면 계약 변경 시 수정 범위를 줄일 수 있습니다.
예를 들어 email 필드를 변경해야 한다면 User 스키마 한 곳만 수정하면 됩니다.
3. GET /users 정의
이제 사용자 목록을 반환하는 GET /users를 추가합니다.
paths:
/users:
get:
summary: List users
operationId: listUsers
parameters:
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
"200":
description: A list of users
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
여기서 중요한 부분은 다음입니다.
-
limit은 쿼리 매개변수입니다. - 기본값은
20입니다. - 최대값은
100입니다. - 응답은
User배열입니다. -
User구조는 직접 다시 쓰지 않고$ref로 참조합니다.
프론트엔드 입장에서는 이 스펙만 보고 다음과 같이 호출 형태를 예측할 수 있습니다.
GET /users?limit=20
응답 형태도 명확합니다.
[
{
"id": "a2d5f7cc-1b9b-4d1a-a0a4-89a0b6c3c001",
"email": "user@example.com",
"name": "Jane Doe",
"createdAt": "2026-06-01T10:00:00Z"
}
]
4. POST /users 정의
다음으로 사용자를 생성하는 POST /users를 추가합니다.
paths:
/users:
post:
summary: Create a user
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
properties:
email:
type: string
format: email
name:
type: string
responses:
"201":
description: User created
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"400":
description: Invalid request body
이 계약은 다음을 명확히 합니다.
- 요청 본문은 JSON입니다.
-
email은 필수입니다. -
name은 선택 사항입니다. - 생성 성공 시
201을 반환합니다. - 응답 본문은
User스키마입니다. - 잘못된 요청은
400을 반환합니다.
클라이언트 요청 예시는 다음과 같습니다.
POST /users
Content-Type: application/json
{
"email": "new-user@example.com",
"name": "New User"
}
5. 전체 OpenAPI 파일
위 조각을 합치면 다음과 같은 완전한 스펙이 됩니다.
openapi: 3.0.3
info:
title: Users API
version: 1.0.0
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: List users
operationId: listUsers
parameters:
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
"200":
description: A list of users
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
post:
summary: Create a user
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
properties:
email:
type: string
format: email
name:
type: string
responses:
"201":
description: User created
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"400":
description: Invalid request body
components:
schemas:
User:
type: object
required:
- id
- email
- createdAt
properties:
id:
type: string
format: uuid
email:
type: string
format: email
name:
type: string
createdAt:
type: string
format: date-time
아직 서버 코드를 작성하지 않았지만 다음 합의는 이미 끝났습니다.
- 엔드포인트 경로
- 요청 형식
- 응답 형식
- 필수 필드
- 데이터 타입
- 상태 코드
- 공통 모델
이 파일이 이후 구현, 테스트, 문서의 기준이 됩니다.
스펙으로부터 목(mock), 테스트, 문서를 생성
스펙을 먼저 작성하는 이유는 하나의 파일이 여러 산출물을 동시에 만들기 때문입니다.
1. 목 서버 생성
목 서버는 OpenAPI 스펙을 읽고 스키마와 일치하는 예시 응답을 반환합니다.
예를 들어 User 스키마에 다음 힌트가 있습니다.
id:
type: string
format: uuid
email:
type: string
format: email
createdAt:
type: string
format: date-time
도구는 이를 기반으로 UUID, 이메일, 날짜 형식의 샘플 데이터를 생성할 수 있습니다.
프론트엔드는 백엔드 구현을 기다리지 않고 다음 API를 호출할 수 있습니다.
curl "https://mock.example.com/users?limit=20"
예상 응답은 스펙에 맞는 사용자 배열입니다.
[
{
"id": "c9dd46f7-0cc5-4cf2-93bb-df6e65e38f5b",
"email": "mock@example.com",
"name": "Mock User",
"createdAt": "2026-06-01T12:00:00Z"
}
]
스펙이 바뀌면 목 응답도 함께 바뀝니다. 따라서 프론트엔드는 실제 백엔드가 완성되기 전에도 최신 계약에 맞춰 개발할 수 있습니다.
2. 계약 테스트 작성
스펙은 테스트 오라클 역할도 합니다. 구현이 계약을 만족하는지 확인하면 됩니다.
예를 들어 POST /users에 대해 확인할 항목은 다음과 같습니다.
- 올바른 요청이면
201을 반환하는가? - 응답 본문이
User스키마와 일치하는가? -
email이 없으면400을 반환하는가?
테스트 관점에서는 다음과 같은 케이스가 필요합니다.
POST /users with valid body
→ expect status 201
→ expect response body matches components.schemas.User
POST /users without email
→ expect status 400
핵심은 어설션을 임의로 만드는 것이 아니라, 이미 합의한 OpenAPI 스펙을 기준으로 실제 응답을 검증하는 것입니다.
3. 문서 자동 생성
API 참조 문서는 동일한 OpenAPI 파일에서 렌더링할 수 있습니다.
문서에 표시되는 항목은 모두 스펙에서 나옵니다.
- 엔드포인트 목록
- 요청 파라미터
- 요청 본문
- 응답 예시
- 상태 코드
- 스키마 설명
별도 문서 파일을 수동으로 유지하지 않으면 문서와 구현 사이의 불일치를 줄일 수 있습니다.
이것이 스펙 우선 방식이 깃(Git) 네이티브 API 워크플로와 잘 어울리는 이유입니다. 스펙은 일반 텍스트 파일이므로 계약 변경 사항은 풀 리퀘스트에서 검토 가능한 diff로 나타납니다.
예를 들어 누군가 email을 emailAddress로 바꾸거나 필수 필드를 제거하면 리뷰 단계에서 확인할 수 있습니다.
Apidog에서 수행하기
Apidog는 스펙 우선 모드(Spec-First Mode)를 통해 이 흐름을 지원합니다. OpenAPI 파일을 단순한 내보내기 결과물로 취급하지 않고, 프로젝트의 기준 계약으로 다룹니다.
실무에서는 다음 순서로 사용할 수 있습니다.
1. OpenAPI 스펙 작성 또는 붙여넣기
Apidog 편집기에 /users 스펙을 작성하거나 기존 YAML 파일을 붙여넣습니다.
스펙에는 최소한 다음이 포함되어야 합니다.
openapiinfoserverspathscomponents.schemas
2. 스펙 파싱 확인
Apidog가 스펙을 파싱하면 엔드포인트, 요청, 응답, 스키마가 워크스페이스에 반영됩니다.
이 단계에서 확인할 항목은 다음입니다.
-
/users경로가 정상적으로 보이는가? -
GET과POST작업이 모두 생성되었는가? -
User스키마가 공통 컴포넌트로 인식되는가? - 필수 필드와 타입이 올바르게 표시되는가?
3. 목 서버로 프론트엔드 연결
스펙이 준비되면 목 서버를 사용해 프론트엔드가 API를 호출할 수 있습니다.
프론트엔드는 실제 백엔드 URL 대신 목 서버 URL을 사용합니다.
const res = await fetch("https://mock.example.com/users?limit=20");
const users = await res.json();
console.log(users);
이때 응답은 OpenAPI 스키마를 기준으로 생성됩니다.
4. 문서 확인
생성된 문서는 스펙을 기준으로 업데이트됩니다.
따라서 문서를 따로 편집하기보다 스펙을 수정하는 방식으로 유지하는 것이 좋습니다.
예를 들어 POST /users에 새로운 필드를 추가하려면 문서에서 직접 수정하지 않고 OpenAPI 스펙의 requestBody를 수정합니다.
properties:
email:
type: string
format: email
name:
type: string
role:
type: string
스펙이 변경되면 문서도 같은 기준으로 갱신됩니다.
5. 실제 백엔드 응답 검증
백엔드 구현이 준비되면 스펙의 작업을 실제 백엔드에 대한 테스트 케이스로 실행합니다.
검증 목표는 다음입니다.
- 실제 상태 코드가 스펙과 일치하는가?
- 응답 본문이 선언된 스키마와 일치하는가?
- 필수 필드가 누락되지 않았는가?
- 타입이 맞는가?
- 오류 케이스가 선언대로 동작하는가?
예를 들어 실제 POST /users 응답이 다음과 같다면 문제가 있습니다.
{
"id": "123",
"email": "user@example.com"
}
스펙상 createdAt은 필수이므로 이 응답은 User 스키마를 만족하지 않습니다.
required:
- id
- email
- createdAt
이런 불일치를 CI나 테스트 단계에서 잡는 것이 계약 테스트의 목적입니다.
6. Git과 양방향 동기화
스펙을 정직하게 유지하는 방법은 Git에 보관하고 변경 사항을 리뷰하는 것입니다.
Apidog의 양방향 Git 동기화를 사용하면 변경 사항이 양쪽으로 흐를 수 있습니다.
- 저장소에서 YAML을 수정하고 푸시하면 Apidog가 변경 사항을 반영합니다.
- Apidog에서 수정하면 팀이 검토할 수 있는 커밋으로 저장됩니다.
- 계약은 두 곳에 따로 존재하지 않고 하나의 소스로 관리됩니다.
순수한 디자인 우선 접근 방식과의 차이를 더 알고 싶다면 Apidog의 스펙 우선 vs 디자인 우선을 참고하십시오.
스펙 우선 체크리스트
스펙이 구현 준비 상태인지 확인하려면 다음 항목을 점검하십시오.
- [ ] 스펙이 OpenAPI 스키마 기준으로 오류 없이 검증됩니다.
- [ ] 모든 엔드포인트에 성공 응답이 있습니다.
- [ ] 모든 엔드포인트에 최소 하나 이상의 오류 응답이 있습니다.
- [ ] 공통 객체는
components.schemas에 정의되어 있습니다. - [ ] 공통 객체는 복사하지 않고
$ref로 참조합니다. - [ ] 필수 필드는
required에 명시되어 있습니다. - [ ]
uuid,email,date-time같은format이 필요한 곳에 설정되어 있습니다. - [ ] 쿼리 매개변수에는 타입, 기본값, 제한값이 명시되어 있습니다.
- [ ] 스펙 파일은 버전 관리에 커밋되어 있습니다.
- [ ] 스펙 변경은 풀 리퀘스트에서 리뷰됩니다.
- [ ] 스펙 기반 목 서버가 실행됩니다.
- [ ] 프론트엔드가 목 서버를 호출할 수 있습니다.
- [ ] 실제 백엔드 응답을 스펙과 비교하는 계약 테스트가 있습니다.
- [ ] 게시된 문서는 동일한 스펙 파일에서 렌더링됩니다.
- [ ] 수동으로 유지되는 별도 문서 사본이 없습니다.
이 항목들이 충족되면 팀은 추측이 아니라 하나의 계약을 기준으로 병렬 개발할 수 있습니다.
자주 묻는 질문
스펙 우선 API 개발은 디자인 우선과 동일한가요?
대체로 그렇습니다.
“디자인 우선”과 “계약 우선”은 구현 전에 인터페이스를 먼저 정의한다는 원칙을 설명합니다. “스펙 우선”은 그 산출물이 OpenAPI 스펙 파일이라는 점을 더 직접적으로 표현한 이름입니다.
실무에서는 세 용어가 자주 혼용됩니다.
YAML을 직접 작성해야 하나요?
아니요.
시각적 편집기에서 스펙을 작성하고 YAML을 생성할 수도 있고, YAML을 직접 작성할 수도 있습니다.
중요한 것은 작성 방식이 아니라 다음 조건입니다.
- 코드 작성 전에 계약이 존재해야 합니다.
- 팀이 해당 계약에 합의해야 합니다.
- 구현과 테스트가 그 계약을 기준으로 동작해야 합니다.
많은 팀은 시각적 편집기로 초안을 만들고, 리뷰 단계에서 YAML을 직접 확인하며 다듬는 방식을 함께 사용합니다.
스펙과 코드가 서로 멀어지는 것을 어떻게 막을 수 있나요?
스펙을 진실의 원천으로 만들고 자동 검증을 추가해야 합니다.
실무적으로는 다음 방식이 효과적입니다.
- OpenAPI 스펙을 Git 저장소에 보관합니다.
- 모든 계약 변경을 풀 리퀘스트로 리뷰합니다.
- CI에서 계약 테스트를 실행합니다.
- 실제 응답이 스키마와 일치하지 않으면 빌드를 실패시킵니다.
- 문서는 스펙에서 자동 생성합니다.
- 도구와 Git 간 동기화를 유지합니다.
이렇게 하면 스펙, 코드, 문서가 서로 다른 방향으로 변경되는 문제를 줄일 수 있습니다.
마무리
스펙 우선 API 개발은 개발 순서를 조금 바꾸는 방식입니다. 하지만 효과는 큽니다.
먼저 계약을 작성합니다.
팀이 계약에 합의합니다.
프론트엔드, 백엔드, QA가 같은 스펙을 기준으로 병렬 작업합니다.
구현 결과는 계약 테스트로 검증합니다.
문서는 동일한 파일에서 생성합니다.
전체 흐름을 직접 적용해 보고 싶다면 Apidog에서 스펙 우선 모드(Spec-First Mode)를 열고 자신의 저장소를 연결해 보십시오.


Top comments (0)