Swagger를 이용한 API 문서화
컨트롤러와 DTO 메타데이터를 OpenAPI 계약으로 만들고 Swagger UI에서 명세를 확인하고 요청을 실행합니다.
지난 절에서는 DTO와 유효성 검사를 통해 API 견고함을 높이는 방법을 알아봤습니다.
이번 절에서는 RESTful API 활용도를 높이고 협업 효율을 개선하는 데 필수적인 API 문서화를 다룹니다.
특히 NestJS가 공식 지원하는 Swagger(OpenAPI) 기반 문서화 방법을 자세히 살펴보겠습니다.
API 문서는 프론트엔드 개발자, 다른 백엔드 팀, 그리고 미래의 나 자신에게 API 사용법을 명확히 전달하는 가이드입니다.
수동 문서화는 번거롭고, API 변경 시 문서 동기화가 어렵다는 단점이 있습니다.
Nest의 Swagger 모듈은 컨트롤러와 모델의 런타임 메타데이터를 읽어 OpenAPI 문서를 만들고, Swagger UI에서 그 계약을 탐색하거나 실제 HTTP 요청을 보낼 수 있게 합니다.
Nest · OpenAPI Contract
코드 옆 메타데이터를 하나의 API 계약으로 모은다
SwaggerModule.createDocument()는 애플리케이션 설정, 라우트, 모델의 런타임 메타데이터를 탐색해 직렬화 가능한 OpenAPI 문서를 만듭니다.
-
1 · 메타데이터 원천
DocumentBuilder는 문서 전역 정보와 보안 스키마를, Controller는 라우트와 operation을, DTO·응답 모델 class는 schema를 제공합니다. -
2 · 문서 생성
createDocument()가 세 원천을 탐색해 하나의 직렬화 가능한 OpenAPI Document를 만듭니다. -
3 · 사람을 위한 UI
Swagger UI는 operation과 schema를 읽고, 인증 값을 넣거나 실제 API로 Try it out 요청을 보냅니다.
-
4 · 도구를 위한 명세
JSON·YAML 계약은 SDK 생성, 계약 테스트, API gateway 같은 자동화 도구의 입력이 됩니다.
OpenAPI 메타데이터는 동작을 설명합니다. 입력 검증은 Pipe, 접근 제어는 Guard, 응답 변환은 serializer와 exception layer가 실제 런타임에서 수행하며 문서와 계속 일치시켜야 합니다.
Swagger(OpenAPI)란 무엇인가?
Swagger API 문서화는 단순히 API 목록을 예쁘게 보여주는 기능이 아니라, DTO 메타데이터와 컨트롤러 메타데이터를 모아 OpenAPI 계약으로 만드는 과정입니다.
Swagger UI는 사람이 읽는 화면이고, OpenAPI 명세는 도구가 읽는 계약이라고 이해하면 됩니다.
Swagger는 API를 설계, 빌드, 문서화, 사용하는 데 도움이 되는 오픈소스 도구들의 생태계를 말합니다.
현재는 OpenAPI Specification(OAS)이라는 이름으로 표준화되어 있으며, 이는 RESTful API를 언어 독립적으로 설명하기 위한 기계 판독 가능한(machine-readable) 인터페이스 파일 형식을 정의합니다.
Swagger 사용의 주요 이점- 코드와 함께 관리하는 문서: Nest 라우트 메타데이터와
@nestjs/swagger데코레이터를 OpenAPI 문서로 모읍니다. 자동 생성 결과가 실제 동작과 일치하는지는 계속 검수해야 합니다. - 대화형 UI: 웹 기반의 사용자 인터페이스(Swagger UI)를 제공하여 API 엔드포인트, 요청/응답 스키마, 파라미터 등을 시각적으로 보여주고, 실제 API 호출을 테스트해볼 수도 있습니다.
- 개발자 경험(DX) 향상: 프론트엔드 개발자가 백엔드 API를 쉽게 이해하고 사용할 수 있도록 돕습니다.
- 코드 생성: OpenAPI 명세를 기반으로 클라이언트 SDK나 서버 스텁 코드를 자동으로 생성할 수 있습니다.
- 표준화: OpenAPI는 널리 인정받는 표준이므로, 다른 도구나 플랫폼과의 연동이 용이합니다.
NestJS에 Swagger 통합하기
NestJS는 @nestjs/swagger 패키지를 통해 Swagger를 손쉽게 통합할 수 있도록 지원합니다.
npm install @nestjs/swagger@nestjs/swagger는 OpenAPI 문서 생성과 Swagger UI 설정을 제공하는 Nest 공식 패키지입니다. 현재 공식 설치 절차에서는 swagger-ui-express를 별도로 설치하지 않으며, Nest 애플리케이션은 Express와 Fastify 어댑터 중 하나를 사용할 수 있습니다.
main.ts에 Swagger 설정 추가
애플리케이션의 진입점인 main.ts 파일에서 Swagger 문서를 초기화하고 설정합니다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; // Swagger 관련 모듈 임포트
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 전역 유효성 검사 파이프 설정 (이전 절에서 다룸)
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
disableErrorMessages: process.env.NODE_ENV === 'production',
}));
// Swagger 설정 시작
const config = new DocumentBuilder()
.setTitle('Users API Example') // API 문서의 제목
.setDescription('The Users API description with CRUD operations.') // API 문서 설명
.setVersion('1.0') // API 버전
.addTag('users', 'User related endpoints') // API에 태그 추가 (컨트롤러 그룹화에 사용)
.addBearerAuth( // OpenAPI Bearer 보안 스키마 정의
{ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
'access-token' // @ApiBearerAuth()에서 참조할 보안 스키마 이름
)
.build();
// 문서가 요청될 때 직렬화 가능한 OpenAPI 문서 객체 생성
const documentFactory = () => SwaggerModule.createDocument(app, config);
// /api 경로에 Swagger UI를 서빙하도록 설정
// 이 경로로 접속하면 API 문서를 웹에서 볼 수 있습니다.
SwaggerModule.setup('api', app, documentFactory);
// Swagger 설정 끝
await app.listen(3000);
}
bootstrap();DocumentBuilder(): API 문서의 기본 정보(제목, 설명, 버전 등)를 설정하는 빌더 클래스입니다.addTag(): API 엔드포인트를 논리적인 그룹으로 묶는 데 사용되는 태그를 추가합니다. 컨트롤러에@ApiTags()데코레이터와 함께 사용됩니다.addBearerAuth(): Bearer 인증 방식을 OpenAPI의securitySchemes에 정의합니다. 이 정의만으로 엔드포인트가 보호되거나 문서에 인증 요구가 적용되지는 않습니다.SwaggerModule.createDocument(app, config): Nest 애플리케이션의 라우트·모델 메타데이터와DocumentBuilder설정을 모아 직렬화 가능한 OpenAPI 문서 객체를 생성합니다.SwaggerModule.setup('api', app, documentFactory):/api에 Swagger UI를, 기본적으로/api-json과/api-yaml에 기계 판독 가능한 명세를 노출합니다.
API의 구조와 동작을 Swagger 문서에 상세하게 표현하기 위해 DTO 클래스와 컨트롤러 메서드에 @nestjs/swagger 데코레이터를 적용합니다.
import { IsString, IsEmail, IsNotEmpty, IsInt, Min, Max } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; // ApiProperty 임포트
export class CreateUserDto {
@ApiProperty({ description: '사용자 이름', example: '홍길동' }) // Swagger UI에 표시될 속성 정보
@IsString({ message: '이름은 문자열이어야 합니다.' })
@IsNotEmpty({ message: '이름은 필수 항목입니다.' })
name: string;
@ApiProperty({ description: '사용자 이메일 (고유)', example: 'hong.gd@example.com' })
@IsEmail({}, { message: '유효한 이메일 형식이 아닙니다.' })
@IsNotEmpty({ message: '이메일은 필수 항목입니다.' })
email: string;
@ApiProperty({ description: '사용자 나이', example: 30, minimum: 0, maximum: 150 })
@IsInt({ message: '나이는 정수여야 합니다.' })
@Min(0, { message: '나이는 0 이상이어야 합니다.' })
@Max(150, { message: '나이는 150보다 작거나 같아야 합니다.' })
age: number;
}import { PartialType } from '@nestjs/swagger';
import { CreateUserDto } from './create-user.dto';
// PartialType은 CreateUserDto의 모든 필드를 선택적(optional)으로 만들고,
// 해당 필드에 대한 @ApiProperty() 정의도 자동으로 상속합니다.
export class UpdateUserDto extends PartialType(CreateUserDto) {}@ApiProperty(): DTO 속성의 설명, 예시, OpenAPI schema 제약을 문서에 남깁니다. 이는class-validator와ValidationPipe가 수행하는 런타임 검증과 별개의 역할입니다. Swagger CLI 플러그인을 사용하면 일부 메타데이터를 자동으로 보완할 수 있습니다.
import { Controller, Get, Post, Body, Param, Patch, Delete, HttpCode, HttpStatus, NotFoundException } from '@nestjs/common';
import { UsersService, User } from './users.service'; // 런타임 응답 모델 class 임포트
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import {
ApiTags, // 컨트롤러에 태그를 지정
ApiOperation, // API 작업에 대한 설명
ApiResponse, // 특정 HTTP 상태 코드에 대한 응답 설명
ApiParam, // 경로 파라미터 설명
ApiBody, // 요청 본문 설명
} from '@nestjs/swagger';
@ApiTags('users') // 이 컨트롤러의 모든 API를 'users' 태그로 그룹화합니다.
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
@ApiOperation({ summary: '모든 사용자 조회', description: '등록된 모든 사용자 목록을 반환합니다.' })
@ApiResponse({ status: 200, description: '성공적으로 모든 사용자를 반환합니다.', type: [User] }) // User[] 타입을 명시하여 응답 스키마 표시
findAll(): User[] { // 반환 타입 명시
return this.usersService.findAll();
}
@Get(':id')
@ApiOperation({ summary: '특정 사용자 조회', description: 'ID를 사용하여 특정 사용자를 조회합니다.' })
@ApiParam({ name: 'id', description: '조회할 사용자의 고유 ID', example: 1 })
@ApiResponse({ status: 200, description: '성공적으로 사용자를 반환합니다.', type: User })
@ApiResponse({ status: 404, description: '사용자를 찾을 수 없습니다.' })
findOne(@Param('id') id: string): User {
const user = this.usersService.findOne(+id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
@Post()
@ApiOperation({ summary: '새 사용자 생성', description: '새로운 사용자 계정을 생성합니다.' })
@ApiBody({ type: CreateUserDto, description: '생성할 사용자 정보' }) // 요청 본문의 타입을 명시
@ApiResponse({ status: 201, description: '성공적으로 사용자 생성.', type: User })
@ApiResponse({ status: 400, description: '유효하지 않은 요청 본문입니다.' })
@HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto): User {
return this.usersService.create(createUserDto);
}
@Patch(':id')
@ApiOperation({ summary: '사용자 정보 부분 수정', description: 'ID를 사용하여 특정 사용자의 정보를 부분적으로 수정합니다.' })
@ApiParam({ name: 'id', description: '수정할 사용자의 고유 ID' })
@ApiBody({ type: UpdateUserDto, description: '수정할 사용자 정보 (일부만 포함 가능)' })
@ApiResponse({ status: 200, description: '성공적으로 사용자 정보 수정.', type: User })
@ApiResponse({ status: 404, description: '사용자를 찾을 수 없습니다.' })
@ApiResponse({ status: 400, description: '유효하지 않은 요청 본문입니다.' })
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto): User {
const updatedUser = this.usersService.update(+id, updateUserDto);
if (!updatedUser) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return updatedUser;
}
@Delete(':id')
@ApiOperation({ summary: '사용자 삭제', description: 'ID를 사용하여 특정 사용자를 삭제합니다.' })
@ApiParam({ name: 'id', description: '삭제할 사용자의 고유 ID' })
@ApiResponse({ status: 204, description: '성공적으로 사용자 삭제 (응답 본문 없음).' })
@ApiResponse({ status: 404, description: '사용자를 찾을 수 없습니다.' })
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string): void { // void로 반환 타입을 명시
const removed = this.usersService.remove(+id);
if (!removed) {
throw new NotFoundException(`User with ID ${id} not found`);
}
}
}@ApiTags('users'): 이 컨트롤러의 모든 엔드포인트를 Swagger UI에서 'users'라는 태그(그룹) 아래에 표시합니다.@ApiOperation({ summary: '...', description: '...' }): API 작업의 간략한 요약과 자세한 설명을 제공합니다.@ApiResponse({ status: ..., description: '...', type: ... }): 특정 HTTP 상태 코드(예: 200, 201, 400, 404)에 대한 예상 응답을 문서화합니다.type속성에 DTO나 엔티티 클래스를 지정하면 해당 응답의 스키마를 Swagger UI에 자동으로 표시합니다.@ApiParam({ name: 'id', description: '...', example: ... }): 경로 파라미터(예::id)에 대한 설명과 예시 값을 제공합니다.@ApiBody({ type: CreateUserDto, description: '...' }):POST나PATCH요청의 본문(Body)에 대한 설명을 제공하고, 어떤 DTO 타입을 사용하는지 명시하여 해당 DTO의 스키마를 Swagger UI에 표시합니다.@ApiBearerAuth('access-token'): 보호된 컨트롤러나 메서드의 OpenAPI operation에 Bearer 인증 요구를 붙입니다. 문자열은addBearerAuth(..., 'access-token')의 보안 스키마 이름과 정확히 같아야 합니다. 이 데코레이터는 문서 메타데이터만 추가하므로 실제 접근 제어에는 Guard가 필요합니다.
아래 표는 Swagger 문서 품질을 높이기 위해 어떤 위치에 어떤 데코레이터를 붙일지 빠르게 판단하는 기준입니다.
Nest · Metadata Map
붙인 위치와 OpenAPI 산출물을 함께 추적한다
데코레이터는 계약을 기록하지만 Pipe, Guard, serializer의 런타임 동작을 대신하지 않습니다. 문서 필드와 실제 책임을 나란히 검수하세요.
| 코드 위치 | 선언 | OpenAPI에 남는 결과 | 런타임에서 별도로 확인할 것 |
|---|---|---|---|
| 문서 전역 | DocumentBuilder |
info, tags, securitySchemes |
서버 동작이나 인증을 적용하지 않음 |
| DTO · 응답 모델 | @ApiProperty() 또는 CLI plugin |
components.schemas의 property와 제약 |
요청 DTO class-validator · Pipe응답 · serializer 대조 |
| Handler 입력 | @Body(), @Param(), 필요 시 @ApiBody()·@ApiParam() |
requestBody와 parameters |
Pipe가 파싱·변환·검증을 수행 |
| Controller · Handler | @ApiTags(), @ApiOperation() |
tag, summary, description | 라우트 처리 결과를 바꾸지 않음 |
| 응답 계약 | @ApiResponse() |
상태 코드별 response와 schema | 실제 예외·직렬화 결과와 대조 |
| 보호된 operation | addBearerAuth()@ApiBearerAuth(name) |
보안 스키마와 operation의 security requirement | Guard가 실제 접근을 허용하거나 차단 |
- 문서 전역 ·
DocumentBuilder info, tags,securitySchemes를 만듭니다.- 서버 동작이나 인증을 적용하지 않습니다.
- 모델 ·
@ApiProperty() components.schemas의 property와 제약을 만듭니다.- 요청 DTO는
class-validator·Pipe, 응답은 serializer 결과와 대조합니다. - Handler 입력 · body와 param metadata
requestBody와parameters를 만듭니다.- Pipe가 실제 파싱·변환·검증을 수행합니다.
- Operation · tags와 설명
@ApiTags()와@ApiOperation()이 tag, summary, description을 만듭니다.- 라우트 처리 결과를 바꾸지는 않습니다.
- 응답 ·
@ApiResponse() - 상태 코드별 response와 schema를 만듭니다.
- 실제 예외와 직렬화 결과가 문서와 같은지 대조합니다.
- 보안 · 같은 scheme 이름
addBearerAuth()로 보안 정의를 만들고@ApiBearerAuth('access-token')로 operation 요구를 연결합니다.- 문서 표시에 그치며 실제 접근 제어는 Guard가 수행합니다.
검수 순서는 단순합니다. 실제 route와 DTO를 기준으로 OpenAPI 문서를 비교하고, UI의 Try it out 또는 계약 테스트로 요청·응답·인증 조건이 일치하는지 확인합니다.
users.service.ts의 User 응답 모델을 Export
Swagger의 type 옵션은 런타임에 참조할 수 있는 클래스가 필요합니다.
따라서 User를 인터페이스가 아니라 응답 모델 클래스로 export해야 @ApiResponse({ type: User })가 스키마를 만들 수 있습니다.
import { Injectable } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';
export class User {
@ApiProperty({ example: 1 })
id!: number;
@ApiProperty({ example: '홍길동' })
name!: string;
@ApiProperty({ example: 'hong.gd@example.com' })
email!: string;
@ApiProperty({ example: 30 })
age!: number;
}
@Injectable()
export class UsersService {
// ... 기존 코드
}Swagger UI 확인
애플리케이션을 실행합니다.
npm run start:dev
웹 브라우저를 열고 http://localhost:3000/api로 접속합니다.
이제 Swagger UI에서 정리된 API 문서 페이지를 확인할 수 있습니다.
- API 엔드포인트 목록:
users태그 아래에 모든 사용자 관련 API가 그룹화되어 있습니다. - 각 엔드포인트의 상세 정보:
summary,description,parameters,request body,responses등이 상세하게 표시됩니다. - 모델 스키마:
Schemas섹션에서CreateUserDto,UpdateUserDto,User등의 DTO/엔티티 구조를 시각적으로 확인할 수 있습니다. - Try it out: 각 엔드포인트 옆의 Try it out 버튼을 클릭하면 문서가 아니라 실행 중인 API 서버로 실제 HTTP 요청을 보냅니다.
- Authorize:
addBearerAuth()로 스키마를 정의하고 operation에 같은 이름의@ApiBearerAuth()를 적용했다면 JWT를 입력해 해당 요청의Authorization헤더에 사용할 수 있습니다. - 기계 판독 명세: 기본 설정에서는
/api-json과/api-yaml에서도 같은 OpenAPI 계약을 확인할 수 있으며, SDK 생성기나 계약 테스트 도구가 이를 입력으로 사용할 수 있습니다.
Swagger 문서의 가치는 데코레이터를 붙이는 데서 끝나지 않습니다.
요청 DTO, 응답 모델, 인증 조건, 오류 예시가 실제 파이프·가드·직렬화·예외 처리 결과와 계속 맞아야 협업 계약으로 쓸 수 있습니다. OpenAPI 데코레이터는 런타임 동작을 강제하지 않습니다.
다음 절에서는 이미 배포된 API 계약을 깨지 않도록 버전 관리와 진화 전략을 다룹니다.