Nodejs

NestJS + Swagger 사용 중 겪은 DTO 네이밍 충돌 이슈

Seongmin 2025. 5. 29. 22:13
반응형

API를 개발하다 보면, 프론트엔드나 외부 서비스가 연동할 수 있도록 요청과 응답의 형식이 명확하게 정의된 API 문서를 제공해야 합니다. 이때 사용되는 대표적인 도구가 바로 Swagger입니다.

NestJS 프레임워크는 이를 위해 공식적으로 Swagger 모듈(@nestjs/swagger)을 제공하고 있으며, 간단한 데코레이터만 추가하면 DTO와 라우터 정보를 바탕으로 자동으로 API 문서를 생성해줍니다. 덕분에 문서화에 따로 시간을 들이지 않고도, 실시간으로 최신 API 문서를 유지할 수 있어 매우 편리합니다.

저도 그래서 Swagger 모듈을 사용하여 API 문서를 제공하고 있습니다. 그런데 최근에 서로 다른 모듈에서 동일한 이름의 DTO를 사용하는 상황에서 Swagger 문서에 한쪽 정보만 표시되는 문제를 겪었습니다. 이번 글에서는 이 문제를 어떻게 발견했고, 어떤 방식으로 해결했는지를 정리해보았습니다.

 

1. @nestjs/swagger 모듈

NestJS에서는 @nestjs/swagger 모듈을 사용해 Swagger(OpenAPI) 문서를 손쉽게 생성할 수 있습니다. 이 모듈은 라우팅 정보, DTO 클래스, 데코레이터를 분석해서 OpenAPI 스펙에 맞는 문서를 만들어줍니다. 설치 및 기본 연동 방법은 NestJS 공식 문서에 잘 정리되어 있으므로, 이 글에서는 생략하겠습니다.

 

2. DTO 문서화

DTO는 주로 @Body(), @Query() 등의 데코레이터를 통해 요청 파라미터 타입으로 사용되며, Swagger는 이때 지정된 DTO의 타입 정보를 기반으로 문서를 생성합니다. 응답 타입은 @ApiOkResponse()나 @ApiCreatedResponse(), @ApiResponse() 등 @nestjs/swagger 모듈에서 제공하는 데코레이터를 사용하면 명시할 수 있습니다.

import { Controller, Post, Body } from '@nestjs/common';
import { ApiCreatedResponse } from '@nestjs/swagger';

import { CreateUserDto } from './create-user.dto';

@Controller()
export class AppController {
  @ApiCreatedResponse({
    type: UserDto
  })
  @Post()
  async createUser(@Body() createUserDto: CreateUserDto) {
    ...
  }
}

 

DTO 속성은 @ApiProperty(), @ApiPropertyOptional() 등 @nestjs/swagger 모듈에서 제공하는 데코레이터를 사용하면 명시할 수 있습니다.

export class CreateUserDto {
  @ApiProperty({
    type: 'string',
    title: '아이디',
    example: 'id1234'
  })
  userId: string;
	
  @ApiProperty({
    type: 'integer',
    format: 'int32',
    title: '나이',
    example: 34
  });
  age: number;
}

 

3. 발생한 문제: DTO 이름이 겹칠 때 문서에 누락되는 현상

앞서 본 것처럼, @nestjs/swagger 모듈을 사용하면 API 문서를 쉽게 생성할 수 있습니다. 하지만 실제로 프로젝트 규모가 커지고, 여러 모듈에서 각자의 DTO를 정의하게 되면 문제가 발생할 수 있습니다. 바로 DTO 클래스 이름이 중복될 경우 Swagger 문서에서 일부가 누락되거나 덮어쓰기되는 현상입니다.

 

예를 들어 CreateUserDto라는 DTO를 users 모듈과 admins 모듈에서 각각 정의했다고 가정해보겠습니다.

// users/dto/create-user.dto.ts
export class CreateUserDto {
  @ApiProperty({
    type: 'string',
    title: '아이디',
    example: 'id1234',
  })
  userId: string;

  @ApiProperty({
    type: 'string',
    title: '사용자 이름',
    example: 'bar',
  })
  username: string;
}

// admins/dto/create-user.dto.ts
export class CreateUserDto {
  @ApiProperty({
    type: 'string',
    title: '아이디',
    example: 'id1234',
  })
  userId: string;

  @ApiProperty({
    type: 'string',
    title: '닉네임',
    example: 'foo',
  })
  nickname: string;
}

 

이처럼 클래스의 정의는 다르지만 이름이 동일한 경우, Swagger 문서를 생성할 때 내부적으로클래스 이름을 기준으로 스키마를 등록하게 됩니다. 이 과정에서 한쪽 DTO의 스키마가 문서에 나타나지 않게 됩니다.

Swagger 문서

 

4. 해결 방법: ApiSchema 데코레이터 사용하기

참고: @ApiSchema() 데코레이터는 NestJS v10 이상부터 지원됩니다. 기존 버전(v9 이하)을 사용하고 있다면 NestJS를 업그레이드해야 이 기능을 사용할 수 있습니다.

 

이 문제를 해결하기 위해서는 @nestjs/swagger 모듈에서 제공하는 @ApiSchema() 데코레이터를 사용할 수 있습니다. 이 데코레이터를 사용하면 Swagger 스키마에 등록될 이름을 직접 지정할 수 있기 때문에, 클래스 이름이 겹치더라도 충돌을 방지할 수 있습니다.

 

아래와 같이 Dto에 @ApiSchema() 데코레이터를 추가해서 이름이 겹치지 않도록 구분해줍니다.

// users/dto/create-user.dto.ts
@ApiSchema({
  name: 'User_CreateUserDto',
})
export class CreateUserDto {
  @ApiProperty({
    type: 'string',
    title: '아이디',
    example: 'id1234',
  })
  userId: string;

  @ApiProperty({
    type: 'string',
    title: '사용자 이름',
    example: 'bar',
  })
  username: string;
}

// admins/dto/create-user.dto.ts
@ApiSchema({
  name: 'Admin_CreateUserDto',
})
export class CreateUserDto {
  @ApiProperty({
    type: 'string',
    title: '아이디',
    example: 'id1234',
  })
  userId: string;

  @ApiProperty({
    type: 'string',
    title: '닉네임',
    example: 'foo',
  })
  nickname: string;
}

 

다시 Swagger 문서를 확인해보면 @ApiSchema() 데코레이터에 명시한 name 값이 표시되는 것을 확인할 수 있습니다.

Swagger 문서

 

5. 반복되는 이름 지정이 번거롭다면: @ApiNamedSchema() 커스텀 데코레이터로 개선하기

@ApiSchema() 데코레이터를 사용하면 DTO 이름 충돌 문제는 해결할 수 있지만, 매번 고유한 이름을 수동으로 지정해야 한다는 번거로움이 남습니다.

@ApiSchema({
  name: 'User_CreateUserDto',
})
export class CreateUserDto {
  ...
}

 

이런 반복적인 작업을 줄이기 위해, 저는 prefix만 지정하면 자동으로 스키마 이름을 만들어주는 커스텀 데코레이터 @ApiNamedSchema()를 만들어 사용하고 있습니다.

export const ApiNamedSchema = (domain: string): ClassDecorator => {
  return (target) => {
    const className = target.name;
    const name = `${domain}_${className}`;
    return ApiSchema({ name })(target);
  };
};

 

@ApiNamedSchema() 는 클래스 이름을 그대로 사용하면서, 입력한 값을 prefix로 추가합니다. (단, prefix와 클래스 이름이 같을 경우에는 기존과 동일한 충돌 문제가 발생할 수 있으므로 주의해야 합니다.)

// users/dto/create-user.dto.ts
@ApiNamedSchema('user')
export class CreateUserDto {
  @ApiProperty({
    type: 'string',
    title: '아이디',
    example: 'id1234',
  })
  userId: string;

  @ApiProperty({
    type: 'string',
    title: '사용자 이름',
    example: 'bar',
  })
  username: string;
}

// admins/dto/create-user.dto.ts
@ApiNamedSchema('admin')
export class CreateUserDto {
  @ApiProperty({
    type: 'string',
    title: '아이디',
    example: 'id1234',
  })
  userId: string;

  @ApiProperty({
    type: 'string',
    title: '닉네임',
    example: 'foo',
  })
  nickname: string;
}

 

6. 마치며

Swagger를 통해 API 문서를 자동화하는 것은 정말 유용한 기능이지만, 모듈마다 유사한 이름의 DTO가 많은 프로젝트에서는 이로 인해 의도하지 않은 결과가 발생할 수 있습니다. 이번 글에서 소개한 방식처럼 @ApiSchema()와 커스텀 데코레이터를 활용하면, 문서화 과정에서 발생할 수 있는 문제를 미리 방지하고 더 안정적으로 API를 관리할 수 있습니다. 저처럼 NestJS와 Swagger를 함께 사용하면서 유사한 문제를 겪고 계신 분들께 이 글이 도움이 되었기를 바랍니다.

반응형