NestJS + Swagger 사용 중 겪은 DTO 네이밍 충돌 이슈
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의 스키마가 문서에 나타나지 않게 됩니다.
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 값이 표시되는 것을 확인할 수 있습니다.
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를 함께 사용하면서 유사한 문제를 겪고 계신 분들께 이 글이 도움이 되었기를 바랍니다.