Swagger와 OpenAPI란?
백엔드 서버를 개발할 때 API를 구현하는 것만큼 중요한 작업이 API의 규격을 정의하고 공유하는 것이다.
클라이언트 개발자는 서버에 어떤 URL로 요청해야 하는지, 어떤 데이터를 전달해야 하는지, 어떤 응답을 받을 수 있는지 알아야 한다. 서버 개발자 역시 API의 입력값, 응답 형식, 인증 방식, 오류 코드를 일관되게 관리해야 한다.
이러한 API의 규격을 정의하는 표준이 OpenAPI이며, 이를 시각화하고 편집하거나 활용하는 도구가 Swagger다.
OpenAPI와 Swagger의 차이
| 구분 | 설명 |
|---|---|
| OpenAPI Specification | REST API의 구조와 동작을 정의하는 명세 표준 |
| Swagger UI | OpenAPI 명세를 기반으로 API 문서를 시각화하고 테스트하는 도구 |
| Swagger Editor | OpenAPI 명세를 작성하고 검증하는 편집 도구 |
| Swagger Codegen | 명세를 기반으로 클라이언트 SDK나 서버 코드를 생성하는 도구 |
| OpenAPI Generator | 다양한 언어와 프레임워크를 대상으로 코드를 생성하는 도구 |
OpenAPI는 API의 계약을 정의하고, Swagger 도구는 해당 계약을 작성하거나 확인하고 활용할 수 있도록 지원한다.
예를 들어 게임 서버에서 다음과 같은 API를 제공한다고 가정하자.
POST /users: 사용자 생성GET /users/{userId}: 사용자 조회PATCH /users/{userId}: 사용자 정보 수정DELETE /users/{userId}: 사용자 삭제
이 API들의 요청 형식, 응답 데이터, 인증 요구 사항, 오류 응답 등을 OpenAPI 명세로 정의할 수 있다.
이렇게 작성된 명세는 프론트엔드 개발자와 백엔드 개발자가 동일한 API 계약을 기준으로 작업할 수 있게 해준다.
OpenAPI 명세의 기본 구조
OpenAPI 명세는 일반적으로 YAML 또는 JSON 형식으로 작성한다. YAML은 들여쓰기를 통해 계층 구조를 표현하기 때문에 사람이 읽고 작성하기 편리하다.
다음은 OpenAPI 3.0 기반의 간단한 예시다.
openapi: 3.0.3
info:
title: Game Server API
description: 게임 서버 API 명세
version: 1.0.0
servers:
- url: https://api.example.com
description: Production Server
paths:
/users/{userId}:
get:
summary: 사용자 조회
operationId: getUser
parameters:
- name: userId
in: path
required: true
schema:
type: string
responses:
"200":
description: 사용자 조회 성공
"404":
description: 사용자를 찾을 수 없음
components:
schemas:
User:
type: object
properties:
id:
type: string
nickname:
type: string
각 항목의 역할은 다음과 같다.
openapi
사용하는 OpenAPI 명세 버전을 지정한다. 여기서는 OpenAPI 3.0.3을 사용한다.
info
API의 이름, 설명, 문서 버전 등을 정의한다.
servers
API 요청을 보낼 서버의 기본 URL을 정의한다. 개발 서버와 운영 서버를 각각 등록할 수도 있다.
paths
API의 경로와 HTTP 메서드, 요청 매개변수, 응답 등을 정의한다.
components
여러 API에서 공통으로 사용하는 스키마, 응답, 매개변수, 인증 방식을 정의한다.
이러한 구조를 활용하면 API의 전체 계약을 하나의 명세로 관리할 수 있다.
Paths와 Operation 정의
OpenAPI에서 paths는 API의 URL 경로를 정의하는 핵심 영역이다.
각 경로 아래에 HTTP 메서드를 선언하고, 해당 요청의 목적과 입력값, 반환값을 정의한다.
paths:
/users:
post:
summary: 사용자 생성
operationId: createUser
tags:
- Users
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserRequest"
responses:
"201":
description: 사용자 생성 성공
content:
application/json:
schema:
$ref: "#/components/schemas/UserResponse"
주요 속성은 다음과 같다.
summary: API의 간단한 설명description: API의 상세한 설명operationId: API 작업을 식별하는 고유한 이름tags: API를 기능별로 그룹화하는 분류requestBody: 요청 본문 정의parameters: 경로, 쿼리, 헤더 등의 매개변수 정의responses: HTTP 상태 코드별 응답 정의
특히 operationId는 코드 생성 도구에서 메서드 이름으로 사용될 수 있으므로 중복되지 않도록 관리하는 것이 좋다.
예를 들어 createUser, getUser, updateUser처럼 동작을 명확하게 표현하면 생성된 SDK에서도 API의 목적을 쉽게 파악할 수 있다.
Request와 Response 스키마 정의
API 명세에서 중요한 부분은 요청과 응답의 데이터 구조를 정확하게 정의하는 것이다.
클라이언트가 어떤 데이터를 보내야 하는지, 서버가 어떤 데이터를 반환하는지 명시해야 한다.
요청 스키마 정의
사용자 생성 API를 예로 들어보자.
components:
schemas:
CreateUserRequest:
type: object
required:
- nickname
properties:
nickname:
type: string
minLength: 2
maxLength: 12
description: 사용자 닉네임
example: Player01
이 명세는 다음과 같은 규칙을 정의한다.
- 요청 데이터는 객체 형태다.
nickname은 필수 항목이다.- 닉네임은 문자열이어야 한다.
- 닉네임의 길이는 2~12자다.
다음은 유효한 요청의 예시다.
{
"nickname": "Player01"
}
반면 다음 요청은 명세상 유효하지 않다.
{
"nickname": ""
}
minLength: 2 조건을 만족하지 않기 때문이다.
다만 OpenAPI에 검증 규칙을 작성했다고 해서 서버의 입력값 검증이 자동으로 실행되는 것은 아니다. 서버는 실제 요청을 처리할 때 해당 규칙을 검증해야 한다.
응답 스키마 정의
사용자 생성에 성공했을 때 반환하는 데이터도 정의할 수 있다.
components:
schemas:
UserResponse:
type: object
required:
- id
- nickname
properties:
id:
type: string
description: 사용자 고유 식별자
example: user_123
nickname:
type: string
example: Player01
응답 예시는 다음과 같다.
{
"id": "user_123",
"nickname": "Player01"
}
요청 스키마와 응답 스키마를 분리하면 클라이언트가 전달해야 하는 데이터와 서버가 반환하는 데이터를 명확하게 구분할 수 있다.
또한 데이터베이스 모델을 그대로 API 응답으로 노출하지 않고, API 전용 DTO(Data Transfer Object)를 정의하는 설계와도 잘 맞는다.
components/schemas와 공통 응답
components는 여러 API에서 반복적으로 사용하는 정의를 한곳에 모으는 영역이다.
대표적으로 다음 항목을 관리할 수 있다.
schemas: 요청 및 응답 데이터 구조responses: 공통 응답 정의parameters: 공통 매개변수 정의securitySchemes: 인증 방식 정의headers: 공통 헤더 정의examples: 재사용 가능한 예시
공통 오류 응답 정의
게임 서버에서는 닉네임 중복, 입력값 오류, 인증 실패, 리소스 부재 등 다양한 오류가 발생한다.
이때 모든 API가 서로 다른 오류 구조를 반환하면 클라이언트의 오류 처리 로직이 복잡해진다.
따라서 공통 오류 응답 구조를 정의할 수 있다.
components:
schemas:
ErrorResponse:
type: object
required:
- code
- message
properties:
code:
type: string
description: 애플리케이션 오류 코드
example: NICKNAME_DUPLICATED
message:
type: string
description: 오류 설명
example: 이미 사용 중인 닉네임입니다.
오류 응답은 다음과 같은 형태가 된다.
{
"code": "NICKNAME_DUPLICATED",
"message": "이미 사용 중인 닉네임입니다."
}
HTTP 상태 코드와 애플리케이션 오류 코드는 역할이 다르다.
- HTTP 상태 코드
409: 요청이 현재 리소스 상태와 충돌함 - 애플리케이션 오류 코드
NICKNAME_DUPLICATED: 닉네임 중복이라는 구체적인 업무 오류
HTTP 상태 코드는 통신 결과를 표현하고, 애플리케이션 오류 코드는 클라이언트가 구체적인 실패 원인을 식별하도록 돕는다.
공통 응답 재사용
같은 오류 스키마를 여러 API에서 참조할 수 있다.
paths:
/users:
post:
summary: 사용자 생성
responses:
"201":
description: 사용자 생성 성공
content:
application/json:
schema:
$ref: "#/components/schemas/UserResponse"
"400":
description: 잘못된 요청
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"409":
description: 닉네임 중복
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
$ref는 이미 정의한 스키마를 참조하는 기능이다.
이를 사용하면 동일한 데이터 구조를 반복해서 작성할 필요가 없다. 공통 오류 형식이 변경되더라도 참조하는 API들이 동일한 구조를 사용하도록 관리할 수 있다.
공통 응답을 더 적극적으로 재사용하려면 components/responses에 상태 코드별 응답 정의를 등록할 수도 있다.
components:
responses:
BadRequest:
description: 잘못된 요청
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
Unauthorized:
description: 인증 실패
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
API에서는 다음과 같이 참조한다.
responses:
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
이렇게 구성하면 API마다 반복되는 응답 정의를 줄이고 명세의 일관성을 유지할 수 있다.
인증 방식 명세
인증이 필요한 API는 OpenAPI의 securitySchemes를 사용해 인증 방식을 정의할 수 있다.
JWT 기반 Bearer 인증을 예로 들어보자.
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
이 설정은 HTTP Bearer 인증을 사용하며 토큰 형식으로 JWT를 사용한다는 의미다.
인증이 필요한 API에 다음과 같이 적용할 수 있다.
paths:
/users/me:
get:
summary: 내 정보 조회
security:
- BearerAuth: []
responses:
"200":
description: 조회 성공
"401":
description: 인증 실패
클라이언트는 다음과 같은 HTTP 헤더를 사용해 요청한다.
GET /users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Swagger UI에서도 토큰을 입력해 인증이 필요한 API를 테스트할 수 있다.
전역 인증과 개별 인증
모든 API에 기본적으로 인증을 요구하려면 최상위 영역에 security를 설정할 수 있다.
security:
- BearerAuth: []
이 경우 개별 API에 별도의 설정이 없다면 전역 인증 정책을 따른다.
반대로 로그인이나 회원가입처럼 인증 없이 접근할 수 있는 API는 다음과 같이 설정한다.
paths:
/auth/login:
post:
summary: 로그인
security: []
이 설정은 해당 API에 적용되는 상위 인증 요구 사항을 비운다.
중요한 점은 OpenAPI의 인증 명세가 실제 서버의 인증 및 권한 검사를 대신하지 않는다는 것이다. 서버는 토큰의 유효성, 만료 여부, 사용자 권한 등을 실제 요청에서 검증해야 한다.
Swagger UI와 Swagger Editor
OpenAPI 명세를 작성한 뒤에는 Swagger 도구를 활용해 문서를 확인하고 테스트할 수 있다.
Swagger UI
Swagger UI는 OpenAPI 명세를 웹 문서로 변환한다.
개발자는 API 목록을 확인하고 각 API의 요청 파라미터, 요청 본문, 응답 상태 코드 등을 살펴볼 수 있다. 또한 Try it out 기능을 통해 실제 API 요청을 실행할 수도 있다.
대표적인 활용 방법은 다음과 같다.
- API 경로와 HTTP 메서드 확인
- 요청 및 응답 스키마 확인
- 인증 토큰 입력
- API 요청 실행
- 서버의 실제 응답 확인
- 오류 응답과 상태 코드 검증
예를 들어 프론트엔드 개발자가 백엔드 구현 코드를 직접 확인하지 않아도 Swagger UI에서 API 계약을 파악하고 테스트할 수 있다.
다만 실제 요청 실행은 서버의 CORS 설정, 인증 정책, 네트워크 접근성 등에 영향을 받는다.
Swagger Editor
Swagger Editor는 OpenAPI 명세를 작성하고 검증하는 도구다.
YAML이나 JSON으로 작성한 명세의 문법 오류, 구조적 문제, 일부 규격 위반을 확인하는 데 활용할 수 있다.
예를 들어 다음과 같이 작성하면 YAML 문법이나 OpenAPI 구조에 문제가 발생할 수 있다.
paths:
/users:
post:
responses:
"201"
description: 성공
"201" 다음에 콜론이 빠져 있기 때문에 YAML 문법 오류가 발생한다.
Swagger Editor를 사용하면 이러한 문제를 조기에 발견할 수 있다.
정리하면 Swagger Editor는 명세 작성과 검증에, Swagger UI는 명세를 시각화하고 API를 테스트하는 데 중점을 둔다.
OpenAPI를 Node.js 서버에 적용하기
Node.js와 Express 기반 서버에서는 OpenAPI 명세를 작성하고 Swagger UI를 연결해 API 문서를 제공할 수 있다.
대표적인 라이브러리는 다음과 같다.
swagger-ui-express: Express 서버에서 Swagger UI 제공swagger-jsdoc: 코드 주석을 기반으로 OpenAPI 명세 생성@asteasolutions/zod-to-openapi: Zod 스키마를 OpenAPI 명세로 변환하는 데 활용할 수 있는 라이브러리
여기서는 별도의 YAML 파일을 작성하고 Swagger UI에 연결하는 방법을 살펴보자.
패키지 설치
npm install express swagger-ui-express yaml
npm install -D @types/express @types/swagger-ui-express @types/node typescript
TypeScript 환경에서 작업한다고 가정한다.
OpenAPI 명세 작성
openapi.yaml 파일을 생성한다.
openapi: 3.0.3
info:
title: Game Server API
version: 1.0.0
servers:
- url: http://localhost:3000
paths:
/users/{userId}:
get:
summary: 사용자 조회
operationId: getUser
parameters:
- name: userId
in: path
required: true
schema:
type: string
responses:
"200":
description: 조회 성공
content:
application/json:
schema:
$ref: "#/components/schemas/UserResponse"
"404":
$ref: "#/components/responses/NotFound"
components:
schemas:
UserResponse:
type: object
required:
- id
- nickname
properties:
id:
type: string
nickname:
type: string
ErrorResponse:
type: object
required:
- code
- message
properties:
code:
type: string
message:
type: string
responses:
NotFound:
description: 사용자를 찾을 수 없음
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
Express 서버에 Swagger UI 연결
import express from "express";
import swaggerUi from "swagger-ui-express";
import fs from "node:fs";
import YAML from "yaml";
const app = express();
app.use(express.json());
const openapiDocument = YAML.parse(
fs.readFileSync("./openapi.yaml", "utf8")
);
app.use(
"/api-docs",
swaggerUi.serve,
swaggerUi.setup(openapiDocument)
);
app.get("/users/:userId", (req, res) => {
const { userId } = req.params;
if (userId !== "user_123") {
return res.status(404).json({
code: "USER_NOT_FOUND",
message: "사용자를 찾을 수 없습니다.",
});
}
return res.status(200).json({
id: "user_123",
nickname: "Player01",
});
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});
서버를 실행하면 다음 주소에서 API 문서를 확인할 수 있다.
http://localhost:3000/api-docs
위 코드는 Swagger UI 연결을 보여주는 최소 예제다. 실제 프로젝트에서는 YAML 파일의 경로를 실행 환경에 맞게 관리하고, 라우트와 명세의 일치 여부를 검증하는 절차를 추가하는 것이 좋다.
특히 이 예제에서는 API 명세와 실제 Express 라우트가 별도로 작성되어 있다. 따라서 명세에 존재하는 API가 실제로 구현되지 않았거나, 실제 응답이 명세와 달라지는 문제가 발생할 수 있다.
Swagger UI가 정상적으로 표시된다는 사실만으로 서버 구현이 명세를 준수한다고 보장할 수는 없다.
OpenAPI Generator
OpenAPI Generator는 OpenAPI 명세를 기반으로 클라이언트 SDK, 서버 인터페이스, 모델 클래스 등의 코드를 생성하는 도구다.
예를 들어 백엔드에서 다음과 같은 API를 정의했다고 가정하자.
paths:
/users/{userId}:
get:
operationId: getUser
parameters:
- name: userId
in: path
required: true
schema:
type: string
responses:
"200":
description: 조회 성공
OpenAPI Generator를 사용하면 TypeScript, Java, C# 등 지원되는 언어와 생성기 설정에 따라 API 호출 코드나 데이터 모델을 생성할 수 있다.
Unity 클라이언트와 Node.js 백엔드가 함께 존재하는 게임 프로젝트라면 다음과 같은 구성을 고려할 수 있다.
- 백엔드: OpenAPI 명세 작성 및 관리
- 웹 클라이언트: TypeScript API 클라이언트 생성
- Unity 클라이언트: C# API 클라이언트 생성
- 공통 기준: 동일한 OpenAPI 명세
이 구조에서는 클라이언트가 서버의 API 형식을 수작업으로 중복 정의하는 부담을 줄일 수 있다.
CLI 설치 및 코드 생성
OpenAPI Generator CLI는 Java 실행 환경이 필요하다. 설치 방법은 운영체제와 프로젝트 환경에 따라 달라질 수 있다.
npm 패키지를 사용하는 경우 다음과 같이 설치할 수 있다.
npm install @openapitools/openapi-generator-cli --save-dev
TypeScript 클라이언트 생성 예시는 다음과 같다.
npx openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-fetch \
-o generated/api-client
주요 옵션은 다음과 같다.
-i: 입력 OpenAPI 명세 파일-g: 생성할 언어 또는 프레임워크의 생성기-o: 생성된 코드의 출력 경로
typescript-fetch는 Fetch API 기반의 TypeScript 클라이언트 생성기다. 생성기를 선택할 때는 대상 언어, 런타임, 사용 중인 프레임워크와 호환되는지 확인해야 한다.
생성된 코드는 프로젝트에서 직접 작성한 코드와 분리해 관리하는 것이 좋다. 명세를 변경해 코드를 다시 생성할 때 수작업 수정 내용이 사라지는 문제를 방지할 수 있기 때문이다.
코드 생성의 장점과 주의점
장점
- 클라이언트의 API 호출 코드 작성 부담 감소
- 요청 및 응답 모델의 일관성 향상
- API 변경 사항을 코드에 반영하기 쉬움
- 서로 다른 언어를 사용하는 클라이언트 간 계약 공유
- 반복적인 데이터 모델 작성 감소
주의점
- 명세가 부정확하면 잘못된 코드가 생성될 수 있음
- 생성된 코드의 구조와 의존성이 프로젝트에 적합한지 확인해야 함
- 서버의 실제 동작과 명세가 일치하는지 별도로 검증해야 함
- 생성 코드의 직접 수정은 재생성 시 유실될 수 있음
코드 생성은 정확한 API 계약을 대신하지 않는다. 정확한 명세가 먼저 존재해야 코드 생성의 효과도 얻을 수 있다.
OpenAPI 기반 API 개발 워크플로
OpenAPI를 프로젝트에 도입할 때는 다음과 같은 작업 흐름을 구성할 수 있다.
- API 계약 정의API 경로, HTTP 메서드, 요청 및 응답 스키마, 상태 코드, 인증 방식을 정의한다.
- 명세 검증Swagger Editor나 별도의 검증 도구로 문법과 명세 구조를 검사한다.
- API 구현Express 라우트, 서비스 로직, DTO 및 입력값 검증 코드를 구현한다.
- 문서 제공Swagger UI를 통해 API 문서를 제공한다.
- 클라이언트 코드 생성OpenAPI Generator로 필요한 SDK나 모델 코드를 생성한다.
- 계약 테스트실제 서버 응답이 OpenAPI 명세와 일치하는지 검증한다.
- 버전 관리 및 배포명세 변경을 코드 변경과 함께 관리하고, 하위 호환성을 확인한 뒤 배포한다.
이러한 워크플로를 적용하면 API 문서가 단순한 설명 자료에 머무르지 않고 개발과 테스트 과정에 직접 활용되는 계약으로 발전한다.
실무에서 고려할 설계 원칙
명세와 구현의 일치
가장 중요한 원칙은 명세와 실제 서버 구현이 일치해야 한다는 것이다.
명세에는 200 응답만 정의되어 있는데 실제 서버에서 404, 409, 500 등의 응답이 발생한다면 클라이언트 개발자는 예상하지 못한 결과를 처리해야 한다.
실제로 발생할 수 있는 상태 코드와 응답 형식을 명세에 포함하고, 자동화된 테스트로 일치 여부를 검증하는 것이 좋다.
DTO와 데이터베이스 모델 분리
데이터베이스 모델을 그대로 API 응답으로 사용하면 내부 데이터 구조가 외부에 노출되거나 데이터베이스 변경이 API 계약에 영향을 줄 수 있다.
따라서 API 전용 요청 및 응답 DTO를 정의하고 이를 OpenAPI 스키마로 표현하는 방식이 유용하다.
오류 응답의 일관성
모든 API가 일관된 오류 구조를 사용하면 클라이언트에서 공통 오류 처리기를 구현하기 쉽다.
예를 들어 HTTP 상태 코드와 함께 code, message를 반환하면 클라이언트는 상태 코드로 오류 범주를 판단하고 애플리케이션 오류 코드로 구체적인 원인을 처리할 수 있다.
하위 호환성 유지
이미 배포된 클라이언트가 존재하는 상태에서 요청 필수값을 추가하거나 응답 필드를 제거하면 기존 클라이언트가 정상적으로 동작하지 않을 수 있다.
API를 변경할 때는 기존 필드의 제거, 필수값 변경, 응답 형식 변경 등이 하위 호환성에 미치는 영향을 검토해야 한다.
인증과 권한 검증 분리
OpenAPI는 인증 방식을 문서화하지만, 사용자별 접근 권한까지 자동으로 보장하지는 않는다.
토큰 검증뿐 아니라 요청한 사용자가 해당 리소스에 접근할 수 있는지 서버에서 별도로 검사해야 한다.