본문 바로가기

Swagger를 이용한 API 문서화

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

앞 글에서는 DTO와 ValidationPipe로 controller에 들어오는 입력을 런타임에 검사했습니다. 이 글에서는 그 DTO와 controller 정보를 모아 API 문서를 만드는 방법을 다룹니다. NestJS가 공식으로 지원하는 Swagger(OpenAPI) 기반 문서화를 사용합니다.

API 문서는 프런트엔드 개발자, 다른 백엔드 팀, 그리고 몇 달 뒤의 자신에게 API 사용법을 알려 주는 안내서입니다. 문서를 손으로 따로 쓰면 API가 바뀔 때마다 문서를 함께 고쳐야 하고, 둘이 어긋나기 쉽습니다. Nest의 Swagger 모듈은 controller와 모델 클래스의 런타임 메타데이터를 읽어 OpenAPI 문서를 만들고, Swagger UI에서 그 문서를 살펴보거나 실제 HTTP 요청을 보낼 수 있게 해 줍니다.

이 글은 5장 첫 글의 페이지 서비스와 CRUD에 문서 메타데이터를 더하는 선택 예제입니다. UsersService.findAll(page, limit)가 돌려준 { items, total }을 controller가 { items, page, limit, total }로 반환하는 구조를 그대로 이어 갑니다. 앞 글의 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가 실제 런타임에서 수행하며 문서와 계속 일치시켜야 합니다.

위 그림은 OpenAPI 문서가 만들어지는 세 가지 출처와 두 가지 사용처를 보여 줍니다. DocumentBuilder는 문서 전체의 정보와 보안 스키마를, controller는 경로와 operation 정보를, DTO와 응답 모델 클래스는 schema를 제공하고, createDocument()가 이 셋을 탐색해 하나의 문서로 묶습니다. 완성된 문서는 사람이 읽는 Swagger UI와, SDK 생성기나 테스트 도구가 읽는 JSON·YAML 명세로 나뉘어 쓰입니다. 그림의 선은 문서 정보가 흐르는 방향일 뿐이며, 실제 guard나 pipe가 실행되는 순서를 뜻하지는 않습니다.


Swagger와 OpenAPI

Swagger는 API를 설계·빌드·문서화하고 사용하는 데 쓰는 오픈소스 도구 모음을 가리킵니다. Swagger에서 시작한 명세 형식은 지금 OpenAPI Specification(OAS)이라는 이름으로 표준화되어 있으며, RESTful API를 프로그래밍 언어와 상관없이 기계가 읽을 수 있는(machine-readable) 형식으로 설명합니다.

Swagger UI는 사람이 읽는 화면이고, OpenAPI 명세는 도구가 읽는 문서라고 구분하면 이해하기 쉽습니다. Nest에서 Swagger 문서화는 API 목록을 보기 좋게 그리는 기능이 아니라, DTO와 controller의 메타데이터를 모아 OpenAPI 문서로 만드는 과정입니다.

Swagger를 쓰면 다음과 같은 이점이 있습니다.

  • 코드와 함께 관리하는 문서: Nest 라우트 메타데이터와 @nestjs/swagger 데코레이터를 OpenAPI 문서로 모읍니다. 다만 생성된 결과가 실제 동작과 일치하는지는 계속 확인해야 합니다.
  • 대화형 UI: Swagger UI가 엔드포인트, 요청·응답 schema, 매개변수를 화면에 보여 주고, 그 자리에서 실제 API를 호출해 볼 수도 있습니다.
  • 개발자 경험(DX) 향상: 프런트엔드 개발자가 백엔드 API를 쉽게 이해하고 사용할 수 있습니다.
  • 코드 생성: OpenAPI 명세를 입력으로 클라이언트 SDK나 서버 스텁 코드를 자동으로 만들 수 있습니다.
  • 표준화: OpenAPI는 널리 쓰이는 표준이므로 다른 도구나 플랫폼과 연동하기 쉽습니다.

NestJS에 Swagger 연결하기

NestJS는 @nestjs/swagger 패키지로 Swagger를 통합합니다. 패키지를 설치하고, main.ts에서 문서를 설정한 다음, DTO와 controller에 데코레이터를 붙이는 순서로 진행합니다.

패키지 설치

npm install @nestjs/swagger

@nestjs/swagger는 OpenAPI 문서 생성과 Swagger UI 설정을 함께 제공하는 Nest 공식 패키지이며, NestJS 12에 맞는 현재 버전은 12.1입니다. 공식 설치 절차에서는 swagger-ui-express를 따로 설치하지 않습니다. Express 대신 Fastify 어댑터를 쓴다면 @fastify/static을 추가로 설치합니다.

main.ts에 Swagger 설정 추가

애플리케이션의 진입점인 main.ts에서 Swagger 문서를 초기화합니다. 앞부분의 URI 버전 설정과 ValidationPipe는 5장 첫 글의 설정을 이어 쓰며, 운영 환경에서 검증 오류의 상세 메시지를 숨기는 disableErrorMessages만 더했습니다. 새로 볼 곳은 // Swagger 설정 시작과 // Swagger 설정 끝 사이입니다. 그 구간은 문서의 기본 정보를 만드는 DocumentBuilder, 문서 객체를 만드는 함수, Swagger UI를 경로에 연결하는 SwaggerModule.setup()의 세 단계로 이루어집니다.

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(process.env.PORT ?? 3000);
}
void bootstrap();

코드의 각 부분은 다음 일을 합니다.

  • DocumentBuilder(): API 문서의 제목, 설명, 버전 같은 기본 정보를 설정하는 빌더 클래스입니다.
  • addTag(): 엔드포인트를 논리적인 그룹으로 묶는 태그를 추가합니다. controller의 @ApiTags()와 함께 씁니다.
  • addBearerAuth(): Bearer 인증 방식을 OpenAPI의 securitySchemes에 정의합니다. 이 정의만으로 엔드포인트가 보호되거나, 각 operation에 인증 요구가 표시되지는 않습니다.
  • SwaggerModule.createDocument(app, config): 애플리케이션의 라우트·모델 메타데이터와 DocumentBuilder 설정을 모아 직렬화할 수 있는 OpenAPI 문서 객체를 만듭니다.
  • SwaggerModule.setup('api', app, documentFactory): /api에 Swagger UI를 띄우고, 기본적으로 /api-json과 /api-yaml에서 기계가 읽을 명세를 제공합니다.

createDocument()를 바로 호출하지 않고 documentFactory 함수로 감싼 데는 이유가 있습니다. 공식 문서에 따르면 이렇게 하면 문서가 처음 요청될 때 만들어지므로 초기화 시간이 줄어듭니다. 또한 문서를 즉시 만드는 방식이라면 app.enableVersioning()이 먼저 실행되어야 경로에 버전 접두사가 붙는데, factory 방식은 이 순서의 영향을 받지 않습니다.

이 설정에서 /api는 문서 화면이고, 실제 사용자 목록은 /v1/users입니다. DocumentBuilder.setVersion('1.0')도 OpenAPI 문서의 info.version 값일 뿐이며, URI 라우트 버전을 설정하지 않습니다.

DTO와 controller에 데코레이터 붙이기

API의 구조와 동작을 문서에 자세히 남기려면 DTO 클래스와 controller 메서드에 @nestjs/swagger 데코레이터를 붙입니다.

5장 첫 글의 입력 변환·검증 규칙은 그대로 두고 ApiProperty만 더합니다. 아래 두 DTO 파일은 같은 경로의 기존 파일을 갱신하는 코드입니다. PartialType의 import를 Swagger 패키지로 바꾸어도, 필드 생략은 허용하고 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를 문서에 그리려면 응답 쪽에도 런타임 클래스가 필요합니다. 다음 문서용 모델을 별도 DTO 파일에 추가합니다. service의 UserRecord interface를 Swagger 클래스로 바꾸거나 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()이 허용한 필드만 복사해서 만듭니다. 문서용 클래스를 선언한다고 응답에 age가 생기거나 직렬화와 저장 방식이 바뀌지는 않습니다.

기존 UsersController에 아래 import와 클래스 메타데이터를 합치고, 기존 목록 메서드 하나만 갱신합니다. 같은 @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을 가져와 각각 @ApiResponse({ status: 200, type: UserResponseModel })을 붙이고, 생성에는 status: 201을 씁니다. 삭제에는 @ApiResponse({ status: 204, description: '응답 본문 없음' })을 사용합니다. 문서 데코레이터를 붙인 뒤에도 ParseIntPipe, 404·409 변환, 201과 Location, 본문 없는 204, 예상하지 못한 예외의 재전파는 그대로 유지합니다. 메타데이터는 이런 동작을 대신 구현하지 않습니다.

이 글에서 새로 쓴 데코레이터는 다음과 같습니다.

  • @ApiTags('users'): 이 controller의 모든 엔드포인트를 Swagger UI의 users 태그 아래에 묶어 보여 줍니다.
  • @ApiOperation({ summary, description }): 작업의 짧은 요약과 자세한 설명을 붙입니다.
  • @ApiResponse({ status, description, type }): 상태 코드별 예상 응답을 문서화합니다. type에 클래스를 지정하면 그 응답의 schema가 함께 표시됩니다.
  • @ApiParam({ name: 'id', description, example }): :id 같은 경로 매개변수에 설명과 예시 값을 붙입니다.
  • @ApiBody({ type: CreateUserDto, description }): POST나 PATCH 요청 본문에 어떤 DTO를 쓰는지 밝히고 그 schema를 보여 줍니다.
  • @ApiBearerAuth('access-token'): 보호된 controller나 메서드의 operation에 Bearer 인증 요구를 붙입니다. 문자열은 addBearerAuth(..., 'access-token')에 준 보안 스키마 이름과 정확히 같아야 합니다. 이 데코레이터도 문서 메타데이터만 추가하므로, 실제 접근 제어에는 guard가 필요합니다.

아래 표는 어떤 위치에 어떤 데코레이터를 붙일지 빠르게 판단할 때 참고할 수 있습니다.

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 또는 계약 테스트로 요청·응답·인증 조건이 일치하는지 확인합니다.

위 표는 선언 위치마다 OpenAPI 문서에 남는 결과와, 런타임에서 따로 확인해야 할 동작을 나란히 적은 것입니다. 예를 들어 @ApiProperty()는 components.schemas의 속성과 제약으로 남지만, 실제 검증은 class-validator와 pipe가 합니다. @ApiBearerAuth(name)은 operation의 보안 요구로 남지만, 요청을 실제로 막는 것은 guard입니다. 표 아래 문장처럼, 실제 route와 DTO를 기준으로 생성된 문서를 비교하고 Try it out이나 자동 테스트로 요청·응답·인증 조건이 일치하는지 확인하는 순서로 검수하면 됩니다.

더 깊이: CLI 플러그인은 컴파일 단계에서 무엇을 채울까

@ApiProperty()를 필드마다 붙여야 하는 까닭은 TypeScript의 메타데이터 반영 기능에 한계가 있기 때문입니다. CLI 플러그인 문서의 설명처럼, 런타임 메타데이터만으로는 클래스에 어떤 속성이 있는지, 그 속성이 선택인지 필수인지 알 수 없습니다. 그래서 nest-cli.json의 compilerOptions.plugins에 @nestjs/swagger를 넣으면, Nest CLI가 컴파일할 때 소스의 구조를 읽어 DTO 속성마다 @ApiProperty()에 해당하는 메타데이터를 채웁니다. name?: string의 물음표로 required: false를, 타입으로 type이나 enum을 정하고 배열도 알아보며, 초기값은 default로 남깁니다.

이 플러그인이 분석하는 파일은 이름이 .dto.ts나 .entity.ts로 끝나는 것뿐입니다. 이 글의 users-response.model.ts는 어느 쪽에도 맞지 않으므로, 플러그인을 켜도 dtoFileNameSuffix 옵션을 바꾸지 않으면 그 파일은 지금처럼 데코레이터를 직접 붙여야 합니다. 또 플러그인은 Nest CLI의 컴파일 과정에 끼어드는 것이라서, 소스를 따로 컴파일하는 도구에는 적용되지 않습니다. 예를 들어 ts-jest는 소스를 메모리에서 바로 컴파일하므로 Jest 설정에 별도의 변환기를 연결해야 하고, SWC 빌더는 --type-check를 켜야 플러그인을 쓸 수 있습니다.


Swagger UI 확인

npm run start:dev로 애플리케이션을 실행하고, 브라우저에서 http://localhost:3000/api에 접속하면 Swagger UI 문서 페이지가 열립니다. 화면에서는 다음 항목을 확인할 수 있습니다.

  • 엔드포인트 목록: users 태그 아래에 사용자 관련 API가 묶여 있습니다.
  • 엔드포인트 상세 정보: summary, description, 매개변수, 요청 본문, 응답이 함께 표시됩니다.
  • 모델 schema: Schemas 섹션에서 CreateUserDto, UpdateUserDto, UserResponseModel, UsersPageResponseModel의 구조를 확인할 수 있습니다.
  • 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 생성기나 API 테스트 도구가 이 파일을 입력으로 사용합니다.

Swagger 문서의 가치는 데코레이터를 붙이는 데서 끝나지 않습니다. 요청 DTO, 응답 모델, 인증 조건, 오류 예시가 실제 pipe·guard·직렬화·예외 처리 결과와 계속 맞아야 팀이 믿고 쓰는 문서가 됩니다. OpenAPI 데코레이터는 런타임 동작을 바꾸지 않는다는 점을 기억해 두세요.


정리

  • @nestjs/swagger는 DocumentBuilder 설정, controller 메타데이터, DTO·응답 모델의 schema를 모아 OpenAPI 문서를 만들고, /api에 Swagger UI를, /api-json과 /api-yaml에 명세를 제공합니다.
  • createDocument()를 factory 함수로 감싸면 문서가 처음 요청될 때 만들어지고, 버전 설정 순서의 영향도 받지 않습니다.
  • @ApiProperty(), @ApiResponse(), @ApiBearerAuth() 같은 데코레이터는 문서만 바꾸며, 검증은 pipe가, 접근 제어는 guard가 실제로 수행합니다.
  • 응답 schema를 문서에 그리려면 런타임 클래스가 필요하므로, interface 대신 문서용 모델 클래스를 따로 둘 수 있습니다.
  • 생성된 문서는 Try it out이나 자동 테스트로 실제 요청·응답과 비교해 어긋나지 않게 관리합니다.

연습 문제

1. 문서만 고치면 바뀌는 것

@ApiProperty({ maxLength: 80 })을 40으로 고쳤지만 @Length(1, 80)은 그대로 두었습니다. 또 @ApiBearerAuth('access-token')만 붙이고 guard는 연결하지 않았습니다. 60글자 이름을 담은 요청과 토큰이 없는 요청은 문서 표시만으로 거부될까요? 실제로 40글자 제한과 인증 필수를 적용하려면 무엇을 함께 고쳐야 할까요?

해설 보기

문서의 길이 표기는 런타임 validator를 바꾸지 않으므로, 다른 입력 조건을 만족하는 60글자 이름은 기존 @Length(1, 80) 규칙을 통과합니다. 인증 메타데이터도 guard를 실행하지 않으므로 토큰 없는 요청을 막지 못합니다.

의도한 규칙을 적용하려면 @Length(1, 40)처럼 validator를 바꾸고 guard를 연결한 뒤, 실제 요청 결과와 생성된 명세를 함께 확인해야 합니다. 기존 클라이언트가 보내던 41자에서 80자 사이의 이름이 갑자기 거부되는 영향은 다음 글의 호환성 판단에도 포함됩니다.

2. 문서를 미리 만들면

documentFactory 대신 const document = SwaggerModule.createDocument(app, config);를 app.enableVersioning()보다 앞줄에서 호출하고, SwaggerModule.setup('api', app, document)로 넘기도록 바꾸었습니다. Swagger UI에서 목록 조회의 Try it out을 누르면 어떤 일이 생길까요?

해설 보기

공식 문서의 안내대로, 문서를 즉시 만들 때 버전 설정이 아직 적용되지 않았다면 생성된 paths에 버전 접두사가 빠집니다. 문서에는 /users가 표시되지만 실제 라우트는 /v1/users이므로, Try it out으로 보낸 요청은 404 Not Found가 됩니다.

app.enableVersioning()을 먼저 호출하거나, 이 글처럼 factory 함수로 감싸 문서가 처음 요청될 때 만들어지게 하면 해결됩니다.

3. 보안 스키마 이름이 다르면

main.ts에서는 addBearerAuth({ ... }, 'access-token')으로 스키마를 정의했는데, controller에는 인자 없이 @ApiBearerAuth()만 붙였습니다. Authorize 화면에 JWT를 입력한 뒤 보호된 operation을 실행하면 Authorization 헤더가 붙을까요?

해설 보기

@ApiBearerAuth()의 이름 기본값은 bearer이므로, operation은 문서에 정의되지 않은 bearer 스키마를 요구하게 됩니다. Authorize 화면에서 입력한 값은 access-token 스키마에 저장되므로 이 operation의 요청에는 Authorization 헤더가 붙지 않고, guard가 연결되어 있다면 401 Unauthorized가 돌아옵니다.

두 곳에 같은 이름을 쓰거나, 둘 다 이름을 생략해 기본값 bearer로 맞추면 됩니다.

다음 글 버전 관리와 API 진화 전략에서는 이미 배포된 API를 쓰는 클라이언트를 깨뜨리지 않도록 버전을 나누고 API를 바꾸어 가는 방법을 다룹니다.