API 요청이 Controller에 도달했을 때, 그 데이터가 올바른 형태인지 어떻게 보장할 수 있을까. NestJS는 DTO(Data Transfer Object) 패턴과 class-validator를 결합해 입력 검증과 타입 안전성을 동시에 확보한다. DTO는 단순한 타입 정의가 아니라, 계층 간 데이터 계약(contract)을 명시하는 역할을 한다.
DTO는 한 계층에서 다른 계층으로 데이터를 전달하기 위해 정의하는 객체다. 비즈니스 로직을 갖지 않으며, 오직 데이터의 형태만 기술한다.
HTTP Request
│
▼
┌─────────────┐
│ Controller │ ← DTO로 요청 바디를 받음
└──────┬──────┘
│ DTO
▼
┌─────────────┐
│ Service │ ← DTO를 받아 도메인 로직 실행
└──────┬──────┘
│ Entity / ResponseDto
▼
┌─────────────┐
│ Repository │ ← DB와 직접 통신
└─────────────┘
세 가지 DTO는 각각 다른 책임을 가진다.
| DTO | 용도 | 특징 |
|---|---|---|
CreateUserDto | 생성 요청 바디 | 필수 필드 전체 포함 |
UpdateUserDto | 수정 요청 바디 | 모든 필드가 선택적 |
ResponseDto | 응답 직렬화 | 민감 필드 제외 |
// create-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}
UpdateUserDto는 PartialType을 활용해 중복 없이 선택적 필드를 만든다.
// update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
PartialType: @nestjs/mapped-types 패키지가 제공하는 유틸리티로, 부모 DTO의 모든 필드를 optional로 변환한다.
// response-user.dto.ts
import { Exclude, Expose } from 'class-transformer';
@Exclude()
export class ResponseUserDto {
@Expose()
id: number;
@Expose()
name: string;
@Expose()
email: string;
// password는 @Expose() 없으므로 직렬화에서 제외됨
}
class-validator는 데코레이터 기반으로 필드 유효성 규칙을 선언한다. 자주 쓰는 데코레이터는 아래와 같다.
| 데코레이터 | 검증 내용 |
|---|---|
@IsString | 문자열 여부 |
@IsEmail | 이메일 형식 |
@IsInt | 정수 여부 |
@MinLength(n) | 최소 문자 길이 |
@IsOptional | undefined 허용 |
@IsEnum(E) | 열거형 값 여부 |
DTO의 데코레이터는 ValidationPipe가 실행해야 효과가 있다. main.ts에서 전역으로 등록하는 것이 일반적이다.
// 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, // 불필요한 필드 포함 시 400 반환
transform: true, // 요청 바디를 DTO 인스턴스로 자동 변환
}),
);
await app.listen(3000);
}
bootstrap();
whitelist: true 옵션은 DTO에 정의되지 않은 키를 요청에서 자동으로 걷어낸다. 클라이언트가 의도치 않은 필드를 보내더라도 서비스 계층에 전달되지 않는다.
Controller에서는 @Body 데코레이터에 DTO 타입을 지정하면 된다.
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
transform: true를 활성화하면 @Body가 반환하는 값이 plain object가 아닌 CreateUserDto 클래스 인스턴스가 되어, 타입 안전성이 런타임까지 보장된다.
TypeScript의 타입은 컴파일 타임에만 존재한다. 런타임에 외부 데이터가 들어오는 순간 타입 정보는 사라진다. class-validator + ValidationPipe의 조합은 이 간극을 메운다. DTO 클래스가 런타임 검증의 기준이 되어, 타입 선언과 실제 동작이 일치하게 된다.