본문 바로가기

Swagger를 이용한 API 문서화

컨트롤러와 DTO 메타데이터를 OpenAPI 계약으로 만들고 Swagger UI에서 명세를 확인하고 요청을 실행합니다.

지난 절에서는 DTO와 유효성 검사를 통해 API의 견고함을 높이는 방법을 알아봤습니다.

이번 절에서는 RESTful API 활용도를 높이고 협업 효율을 개선하는 데 필수적인 API 문서화를 다룹니다.

특히 NestJS가 공식 지원하는 Swagger(OpenAPI) 기반 문서화 방법을 자세히 살펴보겠습니다.

API 문서는 프론트엔드 개발자, 다른 백엔드 팀, 그리고 미래의 나 자신에게 API 사용법을 명확히 전달하는 가이드입니다.

수동 문서화는 번거롭고, API 변경 시 문서 동기화가 어렵다는 단점이 있습니다.

Nest의 Swagger 모듈은 컨트롤러와 모델의 런타임 메타데이터를 읽어 OpenAPI 문서를 만들고, Swagger UI에서 그 계약을 탐색하거나 실제 HTTP 요청을 보낼 수 있게 합니다.

이 절은 5-1의 페이지 서비스와 CRUD에 문서 메타데이터를 추가하는 선택 예제입니다. UsersService.findAll(page, limit)의 { items, total }을 controller가 { items, page, limit, total }로 반환하는 계약을 이어갑니다. 5-2의 age는 입력 검증의 확장 설명이므로, 여기서는 저장·출력까지 제공된 name·email 계약을 문서화합니다.

Nest OpenAPI · 문서 생성

코드 옆 metadata를 문서로 모으고 실제 동작과 대조합니다

createDocument()가 전역 설정·라우트·모델의 runtime metadata를 탐색해 OpenAPI 문서를 만듭니다. 문서의 보안·검증·응답 선언을 실제 Guard·Pipe·직렬화 구현과 별도로 확인합니다.

Nest 메타데이터에서 OpenAPI 문서와 소비자로 이어지는 흐름 DocumentBuilder의 기본 문서 설정, Controller의 경로와 operation 메타데이터, DTO 및 응답 모델의 schema 메타데이터가 createDocument에 모인다. 만들어진 OpenAPI 문서는 Swagger UI와 JSON·YAML 기반 도구가 함께 사용한다. OPENAPI DocumentBuilder info · tags · securitySchemes Controller metadata route · operation · response DTO · response model schema · example · required createDocument() 애플리케이션 메타데이터 탐색 SwaggerModule OpenAPI Document serializable JSON · YAML Swagger UI read · authorize · try it out Clients · tools SDK · tests · gateways
문서 메타데이터 세 원천과 두 소비 경로 DocumentBuilder전역설정,Controller라우트/operation/response,DTO와응답모델schema가createDocument에따로모인다. 생성OpenAPI는사람이읽는SwaggerUI와도구입력인명세로분기한다. 선은문서정보의흐름이며실제Guard/Pipe를실행하는제어흐름이아니다. DocumentBuilder 문서 정보 · 보안 스키마 Controller metadata 경로 · operation · 응답 DTO 모델 metadata schema · 필드 예시 createDocument() 세 원천을 탐색 OpenAPI Document 직렬화 가능 JSON / YAML 사람 Swagger UI 실제 요청 도구 SDK · 테스트 명세 입력
  1. 1 · 메타데이터 원천

    DocumentBuilder는 문서 전역 정보와 보안 스키마를, Controller는 라우트와 operation을, DTO·응답 모델 class는 schema를 제공합니다.

  2. 2 · 문서 생성

    createDocument()가 세 원천을 탐색해 하나의 직렬화 가능한 OpenAPI Document를 만듭니다.

  3. 3 · 사람을 위한 UI

    Swagger UI는 operation과 schema를 읽고, 인증 값을 넣거나 실제 API로 Try it out 요청을 보냅니다.

  4. 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를 손쉽게 통합할 수 있도록 지원합니다.

단계 1: 필요한 패키지 설치
npm install @nestjs/swagger

@nestjs/swagger는 OpenAPI 문서 생성과 Swagger UI 설정을 제공하는 Nest 공식 패키지입니다. 현재 공식 설치 절차에서는 swagger-ui-express를 별도로 설치하지 않으며, Nest 애플리케이션은 Express와 Fastify 어댑터 중 하나를 사용할 수 있습니다.

단계 2: main.ts에 Swagger 설정 추가

애플리케이션의 진입점인 main.ts 파일에서 Swagger 문서를 초기화하고 설정합니다.

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe, VersioningType } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; // Swagger 관련 모듈 임포트

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 5-1의 실제 API 경로 /v1/users를 유지합니다.
  app.enableVersioning({
    type: VersioningType.URI,
    defaultVersion: '1',
  });

  // 전역 유효성 검사 파이프 설정 (이전 절에서 다룸)
  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는 문서 화면이고 실제 사용자 목록은 /v1/users입니다. DocumentBuilder.setVersion('1.0')도 OpenAPI 문서의 info.version이며 URI route version을 설정하지 않습니다.

단계 3: DTO 및 컨트롤러에 Swagger 데코레이터 적용

API의 구조와 동작을 Swagger 문서에 상세하게 표현하기 위해 DTO 클래스와 컨트롤러 메서드에 @nestjs/swagger 데코레이터를 적용합니다.

5-1의 입력 변환·검증 규칙을 유지하면서 ApiProperty를 붙입니다. 아래 두 DTO 파일은 같은 경로의 기존 파일을 갱신합니다. Swagger용 PartialType으로 import를 바꾸어도 필드 생략은 허용하고 null은 거부하는 PATCH 정책은 유지합니다.

src/users/dto/create-user.dto.ts (업데이트)
import { Transform } from 'class-transformer';
import { IsEmail, IsString, Length, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
  @ApiProperty({ description: '사용자 이름', example: '홍길동', maxLength: 80 })
  @Transform(({ value }) =>
    typeof value === 'string' ? value.trim() : value,
  )
  @IsString()
  @Length(1, 80)
  name!: string;

  @ApiProperty({ description: '사용자 이메일', example: '[email protected]', maxLength: 254 })
  @Transform(({ value }) =>
    typeof value === 'string' ? value.trim() : value,
  )
  @IsEmail()
  @MaxLength(254)
  email!: string;
}
src/users/dto/update-user.dto.ts (업데이트)
import { PartialType } from '@nestjs/swagger';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto, {
  skipNullProperties: false,
}) {}
  • @ApiProperty(): DTO 속성의 설명, 예시, OpenAPI schema 제약을 문서에 남깁니다. 이는 class-validator와 ValidationPipe가 수행하는 런타임 검증과 별개의 역할입니다. Swagger CLI 플러그인을 사용하면 일부 메타데이터를 자동으로 보완할 수 있습니다.

응답 schema에도 런타임 class가 필요합니다. 다음 문서용 모델을 별도 DTO 파일에 추가합니다. service의 UserRecord interface를 Swagger class로 바꾸거나 service를 다시 작성할 필요는 없습니다.

src/users/dto/users-response.model.ts
import { ApiProperty } from '@nestjs/swagger';

export class UserResponseModel {
  @ApiProperty({ example: 1 })
  id!: number;

  @ApiProperty({ example: 'Alice' })
  name!: string;

  @ApiProperty({ example: '[email protected]' })
  email!: string;
}

export class UsersPageResponseModel {
  @ApiProperty({ type: [UserResponseModel] })
  items!: UserResponseModel[];

  @ApiProperty({ example: 1, minimum: 1 })
  page!: number;

  @ApiProperty({ example: 20, minimum: 1, maximum: 100 })
  limit!: number;

  @ApiProperty({ example: 2, minimum: 0 })
  total!: number;
}

UserResponseModel과 UsersPageResponseModel은 문서의 schema 참조입니다. 실제 응답 값은 계속 UserResponseDto.from()으로 허용 필드만 복사합니다. 문서 class를 선언한다고 응답에 age가 생기거나 직렬화·저장이 자동으로 바뀌지는 않습니다.

기존 UsersController에 아래 import와 class 메타데이터를 합치고 기존 목록 메서드 한 개를 갱신합니다. 같은 @Get()을 별도로 추가하지 않습니다. 다른 CRUD 메서드와 필요한 기존 import는 그대로 둡니다.

src/users/users.controller.ts (목록 메서드 발췌)
import { Controller, Get, Query } from '@nestjs/common';
import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ListUsersQueryDto } from './dto/list-users-query.dto';
import {
  UserResponseDto,
  type UsersPageResponse,
} from './dto/user-response.dto';
import { UsersPageResponseModel } from './dto/users-response.model';
import { UsersService } from './users.service';

@ApiTags('users')
@Controller({ path: 'users', version: '1' })
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  @ApiOperation({ summary: '사용자 목록 조회', description: '요청한 페이지와 전체 항목 수를 반환합니다.' })
  @ApiQuery({ name: 'page', required: false, schema: { type: 'integer', minimum: 1, default: 1 } })
  @ApiQuery({ name: 'limit', required: false, schema: { type: 'integer', minimum: 1, maximum: 100, default: 20 } })
  @ApiResponse({ status: 200, description: '빈 목록도 같은 페이지 구조입니다.', type: UsersPageResponseModel })
  @ApiResponse({ status: 400, description: 'page 또는 limit 검증 실패' })
  findAll(@Query() query: ListUsersQueryDto): UsersPageResponse {
    const result = this.usersService.findAll(query.page, query.limit);

    return {
      items: result.items.map(UserResponseDto.from),
      page: query.page,
      limit: query.limit,
      total: result.total,
    };
  }

  // 단일 조회·생성·PATCH·삭제와 rethrowConflict는 5-1 구현을 유지합니다.
}

단일 조회·생성·PATCH의 성공 응답에는 UserResponseModel을 import하여 각각 @ApiResponse({ status: 200, type: UserResponseModel }) 또는 생성의 status: 201을 붙입니다. 삭제에는 @ApiResponse({ status: 204, description: '응답 본문 없음' })을 사용합니다. 문서화할 때도 기존 ParseIntPipe, 404·409 변환, 201과 Location, 본문 없는 204, 예상하지 못한 예외의 재전파를 유지합니다. 메타데이터는 이 동작을 대신 구현하지 않습니다.

새로운 데코레이터 설명
  • @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 Swagger · 선언과 런타임 책임

선언 위치, OpenAPI 결과, 실제 실행 책임을 함께 확인합니다

각 선언은 API 계약을 기록합니다. Guard·Pipe·serializer가 실제 동작을 수행하므로 같은 schema와 상태 코드를 만드는지 대조합니다. Try it out은 실제 API 요청을 보냅니다.

문서 전역

선언: DocumentBuilder
OpenAPI에 남는 결과: info, tags, securitySchemes
런타임에서 별도로 확인할 것: 서버 동작이나 인증을 적용하지 않음

DTO · 응답 모델

선언: @ApiProperty() 또는 CLI plugin
OpenAPI에 남는 결과: components.schemas의 property와 제약
런타임에서 별도로 확인할 것:
요청 DTO
class-validator · Pipe
응답 · serializer 대조

Handler 입력

선언: @Body(), @Param(), 필요 시 @ApiBody()·@ApiParam()
OpenAPI에 남는 결과: requestBody와 parameters
런타임에서 별도로 확인할 것: Pipe가 파싱·변환·검증을 수행

Controller · Handler

선언: @ApiTags(), @ApiOperation()
OpenAPI에 남는 결과: tag, summary, description
런타임에서 별도로 확인할 것: 라우트 처리 결과를 바꾸지 않음

응답 계약

선언: @ApiResponse()
OpenAPI에 남는 결과: 상태 코드별 response와 schema
런타임에서 별도로 확인할 것: 실제 예외·직렬화 결과와 대조

보호된 operation

선언:
addBearerAuth()
@ApiBearerAuth(name)

DocumentBuilder와 operation에 같은 scheme 이름을 사용합니다.

OpenAPI에 남는 결과: 보안 스키마와 operation의 security requirement
런타임에서 별도로 확인할 것: Guard가 실제 접근을 허용하거나 차단

검수 순서는 단순합니다. 실제 route와 DTO를 기준으로 OpenAPI 문서를 비교하고, UI의 Try it out 또는 계약 테스트로 요청·응답·인증 조건이 일치하는지 확인합니다.


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, UserResponseModel, UsersPageResponseModel의 문서 구조를 시각적으로 확인할 수 있습니다.
  • Try it out: 각 엔드포인트 옆의 Try it out 버튼을 클릭하면 문서가 아니라 실행 중인 API 서버로 실제 HTTP 요청을 보냅니다. 목록의 요청 경로 /v1/users?page=1&limit=20과 응답의 items·page·limit·total을 대조하세요. 문서가 배열 응답을 나타내거나 실제 값에 없는 age를 필수 응답 필드로 표시한다면 schema와 구현의 경계를 다시 확인합니다.
  • Authorize: addBearerAuth()로 스키마를 정의하고 operation에 같은 이름의 @ApiBearerAuth()를 적용했다면 JWT를 입력해 해당 요청의 Authorization 헤더에 사용할 수 있습니다.
  • 기계 판독 명세: 기본 설정에서는 /api-json과 /api-yaml에서도 같은 OpenAPI 계약을 확인할 수 있으며, SDK 생성기나 계약 테스트 도구가 이를 입력으로 사용할 수 있습니다.

Swagger 문서의 가치는 데코레이터를 붙이는 데서 끝나지 않습니다.

요청 DTO, 응답 모델, 인증 조건, 오류 예시가 실제 파이프·가드·직렬화·예외 처리 결과와 계속 맞아야 협업 계약으로 쓸 수 있습니다. OpenAPI 데코레이터는 런타임 동작을 강제하지 않습니다.

문서 수정으로 바뀌지 않는 동작 찾기

@ApiProperty({ maxLength: 80 })를 40으로 고쳤지만 @Length(1, 80)는 유지했습니다. 또 @ApiBearerAuth('access-token')만 붙이고 guard는 연결하지 않았습니다. 60글자 이름 요청과 토큰 없는 요청은 문서 표시만으로 거부될까요? 실제 계약을 40글자·인증 필수로 바꾸려면 어떤 경계를 함께 수정해야 할까요?

OpenAPI와 실행 경계로 풀이하기

문서의 길이 표기는 런타임 validator를 바꾸지 않으므로, 다른 입력 조건을 만족하는 60글자 이름은 기존 길이 규칙을 통과합니다. 인증 메타데이터도 실제 가드를 실행하지 않으므로 토큰 없는 요청을 자동으로 막지 않습니다. 요청 validator와 guard 정책을 의도한 계약으로 바꾸고, 실제 요청 결과와 생성된 명세를 함께 확인해야 합니다. 기존 클라이언트의 허용 입력을 80에서 40으로 줄이는 영향은 다음 절의 호환 변경 판단에도 포함됩니다.

다음 절에서는 이미 배포된 API 계약을 깨지 않도록 버전 관리와 진화 전략을 다룹니다.