class-validator의 IsMongoId: MongoDB ObjectId 검증
NestJS로 API를 개발하다 보면 MongoDB ObjectId 형식의 파라미터를 검증해야 하는 경우가 자주 생깁니다. 잘못된 형식의 ID가 들어오면 데이터베이스 쿼리 오류가 발생하고, 사용자에게는 불친절한 에러 메시지가 전달되죠.
class-validator의 @IsMongoId() 데코레이터를 사용하면 이 문제를 간단하게 해결할 수 있습니다. DTO에 데코레이터 하나만 추가하면 MongoDB ObjectId 검증이 자동으로 이루어지고, 잘못된 요청은 컨트롤러에 도달하기 전에 차단됩니다.
이 글에서는 @IsMongoId()의 기본 사용법부터 내부 구현, 실전 활용법, 그리고 커스텀 검증까지 모두 다뤄볼게요.
Class-Validator와 NestJS의 만남
NestJS는 TypeScript 기반의 강력한 서버 프레임워크입니다. 그중에서도 class-validator는 NestJS 공식 문서에서 권장하는 검증 라이브러리로, 데코레이터 기반의 직관적인 문법으로 입력 데이터를 검증할 수 있게 해줍니다.
class-validator를 도입하면 서비스 레이어에서 일일이 검증 로직을 작성할 필요가 없어집니다. DTO에 데코레이터를 추가하는 것만으로 요청 데이터의 구조를 제어할 수 있죠. 실제로 많은 개발자들이 class-validator 도입 후 제품의 안정성과 개발 편의성이 크게 향상되었다고 이야기합니다.
@IsMongoId()란 무엇인가?
정의와 목적
@IsMongoId()는 class-validator에서 제공하는 내장 데코레이터로, 문자열이 MongoDB ObjectId 형식(24자 16진수)인지 검증합니다.
MongoDB에서 각 문서는 고유한 _id 필드를 가지며, 이는 12바이트 BSON 타입의 ObjectId입니다. 문자열로 표현하면 24자리 16진수 형태가 되는데, @IsMongoId()는 바로 이 형식을 검증하는 역할을 합니다.
기본 사용 예제
import { IsMongoId, IsNotEmpty } from 'class-validator';
export class FindUserDto {
@IsNotEmpty()
@IsMongoId()
userId: string;
}
위 예제에서 userId는 반드시 비어있지 않은 MongoDB ObjectId 형식이어야 합니다. 만약 잘못된 형식의 ID가 전달되면 다음과 같은 에러 응답이 반환됩니다:
{
"statusCode": 400,
"message": ["userId must be a mongodb id"],
"error": "Bad Request"
}
validator.js 활용
@IsMongoId()의 내부 구현을 살펴보면 흥미로운 점을 발견할 수 있습니다:
import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';
import isMongoIdValidator from 'validator/lib/isMongoId';
export const IS_MONGO_ID = 'isMongoId';
export function isMongoId(value: unknown): boolean {
return typeof value === 'string' && isMongoIdValidator(value);
}
export function IsMongoId(validationOptions?: ValidationOptions): PropertyDecorator {
return ValidateBy(
{
name: IS_MONGO_ID,
validator: {
validate: (value): boolean => isMongoId(value),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + '$property must be a mongodb id',
validationOptions
),
},
},
validationOptions
);
}
핵심 포인트는 다음과 같습니다:
- validator.js 의존:
validator/lib/isMongoId를 사용해 실제 검증을 수행합니다. - 타입 제한:
string타입만 허용합니다. - 정규식 기반: 24자 16진수 형식을 정규식으로 검증합니다.
- 커스터마이징 가능:
validationOptions를 통해 에러 메시지를 변경할 수 있습니다.
NestJS 프로젝트에 Class-Validator 설정하기
설치
먼저 필요한 패키지를 설치합니다:
npm i --save class-validator class-transformer
class-transformer는 plain object를 class instance로 변환하는 데 필요한 라이브러리입니다.
ValidationPipe 적용
NestJS에서 class-validator의 데코레이터를 인식하려면 ValidationPipe를 사용해야 합니다. 전역으로 적용하거나 특정 컨트롤러/라우트에만 적용할 수 있습니다.
전역 적용 (main.ts):
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
})
);
await app.listen(3000);
}
bootstrap();
컨트롤러 레벨 적용:
import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
@Controller('users')
export class UserController {
@Post('find')
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
async findUser(@Body() dto: FindUserDto) {
return this.userService.findUserById(dto.userId);
}
}
주요 옵션:
whitelist: DTO에 정의되지 않은 속성을 자동으로 제거합니다.forbidNonWhitelisted: DTO에 없는 속성이 포함되면 요청을 거부합니다.transform: plain object를 DTO 클래스 인스턴스로 자동 변환합니다.
DTO에서 @IsMongoId() 활용하기
실전 예제: 사용자 조회 API
// user.dto.ts
import { IsMongoId, IsNotEmpty } from 'class-validator';
export class GetUserByIdDto {
@IsNotEmpty({ message: 'userId는 필수입니다.' })
@IsMongoId({ message: 'userId는 유효한 MongoDB ObjectId여야 합니다.' })
userId: string;
}
// user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('find')
async findUser(@Body() dto: GetUserByIdDto) {
console.log(dto.userId);
return this.userService.findUserById(dto.userId);
}
}
유효한 요청:
{
"userId": "507f191e810c19729de860ea"
}
무효한 요청:
{
"userId": "12345"
}
응답:
{
"statusCode": 400,
"message": ["userId는 유효한 MongoDB ObjectId여야 합니다."],
"error": "Bad Request"
}
요청 파라미터 검증
쿼리 파라미터나 URL 파라미터에서도 동일하게 사용할 수 있습니다:
import { Controller, Get, Param } from '@nestjs/common';
import { IsMongoId } from 'class-validator';
export class UserIdParam {
@IsMongoId()
id: string;
}
@Controller('users')
export class UserController {
@Get(':id')
async getUser(@Param() params: UserIdParam) {
return this.userService.findById(params.id);
}
}
흔한 문제와 해결책
빈 문자열 처리
@IsMongoId()는 빈 문자열을 검증하지 않습니다. 빈 문자열도 거부하려면 @IsNotEmpty()를 함께 사용하세요:
export class CreateNewsDto {
@IsNotEmpty()
@IsString()
title: string;
@IsNotEmpty()
@IsString()
content: string;
@IsNotEmpty()
@IsString()
@IsMongoId()
pageId: string;
@IsOptional()
@IsString()
@IsMongoId()
ownerId: string;
}
@IsOptional()을 사용하면 해당 필드가 없거나 undefined일 때는 검증을 건너뜁니다.
커스텀 에러 메시지
기본 에러 메시지가 마음에 들지 않는다면 커스터마이징할 수 있습니다:
export class FindUserDto {
@IsMongoId({ message: '올바른 사용자 ID 형식이 아닙니다.' })
userId: string;
}
다국어 지원이 필요하다면 i18n 라이브러리와 결합해 사용할 수도 있습니다.
고급 활용법과 커스터마이징
다른 검증 데코레이터와 결합
여러 데코레이터를 조합해 더 정교한 검증 규칙을 만들 수 있습니다:
import { IsMongoId, IsOptional, IsArray, ArrayMinSize } from 'class-validator';
export class AssignTasksDto {
@IsArray()
@ArrayMinSize(1)
@IsMongoId({ each: true, message: '각 항목은 유효한 MongoDB ObjectId여야 합니다.' })
userIds: string[];
@IsMongoId()
projectId: string;
}
{ each: true } 옵션을 사용하면 배열의 각 요소를 개별적으로 검증합니다.
커스텀 검증기: DB 존재 여부 확인
@IsMongoId()는 형식만 검증할 뿐, 해당 ID가 실제로 데이터베이스에 존재하는지는 확인하지 않습니다. 이를 위한 커스텀 검증기를 만들어볼까요?
import { registerDecorator, ValidationOptions, ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
@ValidatorConstraint({ name: 'IsExistingMongoId', async: true })
@Injectable()
export class IsExistingMongoIdConstraint implements ValidatorConstraintInterface {
constructor(
@InjectModel('User') private readonly userModel: Model<any>,
) {}
async validate(value: string, args: ValidationArguments): Promise<boolean> {
const modelName = args.constraints[0];
if (modelName === 'User') {
const user = await this.userModel.findById(value).exec();
return !!user;
}
return false;
}
defaultMessage(args: ValidationArguments): string {
return `${args.property}에 해당하는 문서가 존재하지 않습니다.`;
}
}
export function IsExistingMongoId(
modelName: string,
validationOptions?: ValidationOptions,
): PropertyDecorator {
return (object, propertyName) => {
registerDecorator({
name: 'IsExistingMongoId',
target: object.constructor,
propertyName,
constraints: [modelName],
options: validationOptions,
validator: IsExistingMongoIdConstraint,
});
};
}
사용법:
export class UpdateUserDto {
@IsExistingMongoId('User', { message: '존재하지 않는 사용자입니다.' })
userId: string;
}
커스텀 검증기는 비동기 작업을 지원하므로 데이터베이스 쿼리를 통한 검증이 가능합니다. 다만 성능에 영향을 줄 수 있으니 필요한 경우에만 사용하세요.
@IsMongoId()로 더 안전한 API 만들기
@IsMongoId()는 작지만 강력한 도구입니다. 이 데코레이터 하나로 다음과 같은 이점을 얻을 수 있습니다:
- 코드 간소화: 서비스 레이어에서 반복되는 검증 로직을 제거할 수 있습니다.
- 일관성: 모든 MongoDB ObjectId 검증이 동일한 방식으로 이루어집니다.
- 에러 핸들링: 잘못된 요청을 조기에 차단해 불필요한 데이터베이스 쿼리를 방지합니다.
- 유지보수성: DTO를 보는 것만으로 어떤 검증이 이루어지는지 명확하게 알 수 있습니다.
NestJS 프로젝트에서 MongoDB를 사용한다면 @IsMongoId()는 필수 도구입니다. 기본 사용법부터 커스텀 검증기까지 다양한 활용법을 익혀두면 더 견고하고 안전한 API를 만들 수 있을 거예요.
지금 바로 여러분의 프로젝트에 class-validator를 설치하고 @IsMongoId()를 적용해보세요. 작은 변화가 큰 차이를 만들어낼 겁니다!