
API를 설계할 때 가장 경계해야 할 것은 '클라이언트의 입력값'을 맹신하는 것입니다. 프론트엔드에서 폼 유효성 검사를 했더라도, 악의적인 사용자는 언제든 Postman 등을 통해 이상한 데이터를 백엔드로 보낼 수 있습니다.
// 검증이 없는 위험한 컨트롤러 @Post('movie') createMovie(@Body() movieData: any) { // movieData에 예상치 못한 필드가 있거나, // 필수 값이 없어도 그대로 DB에 저장되거나 런타임 에러를 유발합니다. return this.movieService.create(movieData); }
과거에는 이런 값을 검증하기 위해 컨트롤러 내부에 if (!movieData.title) throw new Error(...) 같은 보일러플레이트 코드를 수십 줄씩 작성해야 했습니다. 이는 코드를 매우 지저분하게 만듭니다.
JavaScript 생태계에서는 Joi, Yup, Zod 등 다양한 유효성 검사 라이브러리가 있습니다. (실제로 Joi를 활용해 스키마 기반 검증을 하는 경우도 많습니다.)
하지만 NestJS는 TypeScript의 데코레이터와 리플렉션 기능을 적극적으로 사용하는 프레임워크입니다. 따라서 NestJS와 가장 찰떡궁합인 솔루션은 데코레이터 기반으로 동작하는 class-validator와 class-transformer의 조합입니다.
이들을 활용하면 전역 파이프(Global Pipe) 하나만 설정해 두어도 애플리케이션 전체의 타입 안정성과 요청 검증을 자동으로 수행할 수 있습니다.
class를 사용하여 정의합니다.class-validator 데코레이터)에 맞는지 검사합니다.plainToInstance)해줍니다.패키지를 설치하고 전역 파이프를 설정해 보겠습니다.
npm install class-validator class-transformer
main.ts 파일에 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, // DTO에 없는 속성은 무시(제거)하고 받음 forbidNonWhitelisted: true, // DTO에 없는 속성을 보내면 에러를 발생시킴 transform: true, // 클라이언트 페이로드를 DTO 클래스의 인스턴스로 자동 변환함 (문자열 파라미터를 숫자로 변환하는 등) }) ); await app.listen(3000); } bootstrap();
이제 Movie 생성을 위한 DTO를 작성합니다.
// create-movie.dto.ts import { IsString, IsNumber, IsOptional, IsArray } from 'class-validator'; export class CreateMovieDto { @IsString() readonly title: string; @IsNumber() readonly year: number; @IsOptional() @IsArray() @IsString({ each: true }) // 배열 안의 각 요소도 문자열인지 검사 readonly genres?: string[]; }
이제 컨트롤러에서는 DTO 클래스 타입만 지정해주면 끝납니다.
// movie.controller.ts @Post() createMovie(@Body() movieData: CreateMovieDto) { // 여기까지 도달했다면 movieData는 100% 안전하고 검증된 상태입니다. return this.movieService.create(movieData); }
ValidationPipe와 DTO를 분리한 결과, 컨트롤러에는 순수하게 "요청을 서비스로 위임"하는 본연의 역할만 남게 되었습니다. 클라이언트가 title을 빼놓고 요청을 보내면, 우리의 코드에 도달하기도 전에 NestJS가 알아서 400 Bad Request 에러와 함께 "title must be a string"이라는 친절한 에러 메시지를 반환해 줍니다.
Trade-off:
class-transformer의 transform: true 옵션은 매우 편리하지만(예: 쿼리 파라미터 ?id=1을 자동으로 number 1로 변환), 이 변환 과정에서 약간의 성능 오버헤드가 발생합니다. 초당 수만 건의 요청을 처리하는 극단적인 마이크로서비스 환경에서는 리플렉션과 형변환 비용을 고려하여 Joi나 Zod 같은 함수형 스키마 검증 도구를 대안으로 고려하기도 합니다.
NestJS에서 class-validator와 class-transformer는 거의 필수적인 표준 조합입니다. 특히 파이프의 whitelist와 forbidNonWhitelisted 옵션은 불필요한 해킹 시도나 페이로드 오염을 원천 차단하는 매우 강력한 방어막이 되어줍니다. 꼭 프로젝트 설정 시 기본으로 포함하시길 바랍니다!