DTO와 유효성 검사
안녕하세요! 지난 절에서는 RESTful API의 설계 원칙과 NestJS에서 이를 구현하는 기본적인 방법에 대해 살펴보았습니다. 이번 절에서는 REST API 개발의 핵심적인 부분인 DTO(Data Transfer Object)와 유효성 검사(Validation) 에 대해 심층적으로 알아보겠습니다.
클라이언트가 서버로 데이터를 보낼 때, 이 데이터가 서버의 비즈니스 로직에 맞게 올바른 형식과 내용을 가지고 있는지 확인하는 것은 매우 중요합니다. 잘못된 데이터는 애플리케이션 오류, 데이터베이스 손상, 심지어 보안 취약점으로 이어질 수 있기 때문입니다. NestJS는 이를 위해 DTO를 활용한 타입 안전성과 강력한 유효성 검사 기능을 제공합니다.
DTO란 무엇인가?
DTO(Data Transfer Object) 는 클라이언트와 서버 간에 데이터를 전송하기 위해 사용되는 객체 형태의 데이터 구조입니다. 즉, 네트워크를 통해 전송될 데이터의 형식(Shape) 을 정의하는 데 사용됩니다.
DTO 사용의 주요 이점
- 타입 안전성(Type Safety): TypeScript와 함께 사용될 때, DTO는 API 요청 및 응답 데이터의 타입을 명확하게 정의하여 컴파일 시점에 오류를 방지하고 코드의 안정성을 높입니다.
- 유효성 검사(Validation): DTO 클래스에
@class-validator
라이브러리의 데코레이터를 사용하여 데이터의 유효성 규칙을 선언적으로 정의할 수 있습니다. - 코드 가독성 및 유지보수성: API의 입력 및 출력 데이터 구조를 한눈에 파악할 수 있게 하여 코드의 가독성을 높이고, 변경 사항이 발생했을 때 영향을 받는 부분을 쉽게 파악할 수 있게 합니다.
- 문서화: DTO는 API의 입력 및 출력 데이터에 대한 훌륭한 자체 문서 역할을 합니다. Swagger와 같은 API 문서화 도구와 연동될 때 더욱 빛을 발합니다.
- 강력한 자동 완성: IDE에서 DTO 속성에 대한 자동 완성을 제공하여 개발 생산성을 향상시킵니다.
예시 (User 생성 DTO)
// src/users/dto/create-user.dto.ts
export class CreateUserDto {
name: string;
email: string;
age: number;
}
위 DTO는 사용자를 생성할 때 name
(문자열), email
(문자열), age
(숫자) 필드가 필요함을 명시합니다.
NestJS의 유효성 검사
NestJS는 DTO를 이용한 유효성 검사를 위해 두 가지 강력한 라이브러리를 사용합니다. class-validator
와 class-transformer
입니다. 이 두 라이브러리는 NestJS에서 DTO를 정의하고 유효성 검사를 수행하는 데 필수적인 역할을 합니다.
class-validator
: 클래스 속성에 데코레이터를 붙여 유효성 규칙을 정의하는 데 사용됩니다.class-transformer
: 일반 객체를 클래스 인스턴스로 변환하거나, 클래스 인스턴스를 일반 객체로 변환하는 데 사용됩니다. 특히 요청 본문(plain JavaScript object)을 DTO 클래스의 인스턴스로 자동 변환하여@class-validator
가 작동할 수 있도록 해줍니다.
단계 1: 필요한 패키지 설치
npm install class-validator class-transformer
단계 2: DTO에 유효성 검사 데코레이터 적용
지난 절에서 만들었던 CreateUserDto
와 UpdateUserDto
에 유효성 검사 규칙을 추가해 보겠습니다.
// src/users/dto/create-user.dto.ts
import { IsString, IsEmail, IsNotEmpty, IsInt, Min, Max } from 'class-validator';
export class CreateUserDto {
@IsString({ message: '이름은 문자열이어야 합니다.' })
@IsNotEmpty({ message: '이름은 필수 항목입니다.' })
name: string;
@IsEmail({}, { message: '유효한 이메일 형식이 아닙니다.' })
@IsNotEmpty({ message: '이메일은 필수 항목입니다.' })
email: string;
@IsInt({ message: '나이는 정수여야 합니다.' })
@Min(0, { message: '나이는 0보다 커야 합니다.' })
@Max(150, { message: '나이는 150보다 작거나 같아야 합니다.' })
age: number; // 새로운 필드 추가
}
@IsString()
: 해당 필드가 문자열인지 확인합니다.@IsEmail()
: 해당 필드가 유효한 이메일 형식인지 확인합니다.@IsNotEmpty()
: 해당 필드가 비어있지 않은지 (null, undefined, 빈 문자열) 확인합니다.@IsInt()
,@Min()
,@Max()
: 숫자 필드에 대한 유효성 검사 규칙입니다.{ message: '...' }
: 유효성 검사 실패 시 반환될 사용자 지정 오류 메시지를 설정할 수 있습니다.
부분 업데이트를 위한 UpdateUserDto
// src/users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types'; // npm install @nestjs/mapped-types
import { CreateUserDto } from './create-user.dto';
// PartialType은 CreateUserDto의 모든 필드를 선택적(optional)으로 만듭니다.
// 즉, name?: string; email?: string; age?: number; 와 동일합니다.
export class UpdateUserDto extends PartialType(CreateUserDto) {}
단계 3: 전역 ValidationPipe
설정
main.ts
파일에서 NestJS 애플리케이션 전체에 ValidationPipe
를 전역적으로 적용하는 것이 가장 효율적인 방법입니다. 이 파이프는 들어오는 모든 요청 본문을 자동으로 유효성 검사하고 DTO 인스턴스로 변환합니다.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common'; // ValidationPipe 임포트
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 전역 유효성 검사 파이프 설정
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // DTO에 정의되지 않은 속성은 요청 본문에서 자동으로 제거합니다.
// 클라이언트가 불필요하거나 악의적인 속성을 보낼 때 유용합니다.
forbidNonWhitelisted: true, // DTO에 정의되지 않은 속성이 존재하면 요청을 거부하고 400 Bad Request 에러를 발생시킵니다.
transform: true, // 요청 본문을 해당 DTO 클래스의 인스턴스로 자동 변환합니다.
// (예: 문자열 '123'을 숫자 123으로, "true"를 boolean true로)
// 이 옵션이 있어야 class-transformer가 작동하고, @class-validator가 올바른 타입에서 유효성 검사를 수행합니다.
disableErrorMessages: process.env.NODE_ENV === 'production', // 프로덕션 환경에서 상세 에러 메시지 비활성화 (보안)
}));
await app.listen(3000);
}
bootstrap();
ValidationPipe
옵션 설명
whitelist: true
: DTO에 정의되지 않은 속성(필드)이 요청 본문에 포함되어 있으면, 해당 속성을 자동으로 제거합니다. 이는 예상치 못한 데이터나 잠재적으로 악의적인 데이터가 데이터베이스에 저장되는 것을 방지하는 데 유용합니다.forbidNonWhitelisted: true
:whitelist
옵션과 함께 사용될 때, DTO에 정의되지 않은 속성이 요청 본문에 포함되어 있으면 아예 요청을 거부하고400 Bad Request
응답을 반환합니다. 이는 API의 입력 스키마를 엄격하게 강제하고자 할 때 유용합니다.transform: true
: 수신된 요청 본문(일반 JavaScript 객체)을 해당 DTO 클래스의 인스턴스로 자동 변환합니다. 예를 들어,CreateUserDto
의age: number
필드에 클라이언트가"25"
라는 문자열을 보내도,ValidationPipe
는 이를25
라는 숫자로 변환하여 유효성 검사를 수행하고 컨트롤러에 전달합니다. 이 기능은@class-transformer
에 의해 제공됩니다.disableErrorMessages
: 프로덕션 환경에서는 상세한 에러 메시지가 잠재적인 공격자에게 정보를 노출할 수 있으므로, 이를 비활성화하여 일반적인 "Bad Request" 메시지만 반환하도록 설정할 수 있습니다.
단계 4: 컨트롤러에서 DTO 사용
컨트롤러 메서드의 @Body()
데코레이터와 함께 DTO 클래스를 타입 힌트로 사용합니다.
// src/users/users.controller.ts (업데이트)
import { Controller, Get, Post, Body, Param, Patch, Delete, HttpCode, HttpStatus, NotFoundException, UsePipes } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
// import { ValidationPipe } from '@nestjs/common'; // 개별 파이프 적용 시 임포트
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
// ... (findAll, findOne 등 기존 메서드는 동일)
@Post()
@HttpCode(HttpStatus.CREATED)
// @UsePipes(new ValidationPipe()) // 전역 파이프를 사용하므로 이 부분은 필요 없습니다.
create(@Body() createUserDto: CreateUserDto) {
// ValidationPipe가 자동으로 createUserDto의 유효성을 검사하고,
// 유효하면 CreateUserDto 인스턴스로 변환하여 여기에 주입합니다.
// 유효하지 않으면 400 Bad Request 응답을 자동으로 보냅니다.
return this.usersService.create(createUserDto);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
const updatedUser = this.usersService.update(+id, updateUserDto);
if (!updatedUser) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return updatedUser;
}
// ... (remove 등 기존 메서드는 동일)
}
컨트롤러 코드 자체는 DTO를 타입 힌트로 사용하는 것 외에는 크게 변하지 않습니다. ValidationPipe
가 모든 마법을 수행하여 요청이 컨트롤러에 도달하기 전에 유효성 검사를 처리합니다.
유효성 검사 테스트 및 에러 응답 확인
애플리케이션을 실행하고(npm run start:dev
), Postman 등으로 테스트해봅니다.
새로운 사용자 생성 (유효한 데이터)
POST
http://localhost:3000/users
- Body:
{"name": "David", "email": "david@example.com", "age": 30}
- 응답:
201 Created
와 생성된 사용자 정보. - (정상 동작)
새로운 사용자 생성 (유효하지 않은 데이터 - name
누락, email
형식 오류, age
범위 오류):
POST
http://localhost:3000/users
- Body:
{"email": "invalid-email", "age": 200}
- 응답:
400 Bad Request
와 함께 다음과 유사한 에러 메시지 배열을 받게 됩니다.{ "statusCode": 400, "message": [ "이름은 문자열이어야 합니다.", "이름은 필수 항목입니다.", "유효한 이메일 형식이 아닙니다.", "나이는 150보다 작거나 같아야 합니다." ], "error": "Bad Request" }
- (각 필드에 정의된 에러 메시지가 반환됨을 확인)
새로운 사용자 생성 (DTO에 정의되지 않은 필드 포함)
POST
http://localhost:3000/users
- Body:
{"name": "Eve", "email": "eve@example.com", "age": 25, "extraField": "malicious data"}
forbidNonWhitelisted: true
일 경우:400 Bad Request
응답 (정의되지 않은 필드extraField
때문에 요청 거부).forbidNonWhitelisted: false
&whitelist: true
일 경우:201 Created
응답과 함께extraField
가 자동으로 제거된 사용자 정보가 생성됨.
DTO와 유효성 검사는 REST API의 견고성과 신뢰성을 크게 향상시키는 필수적인 요소입니다. NestJS는 class-validator
와 class-transformer
를 통한 강력한 통합으로 개발자가 최소한의 노력으로 최대의 유효성 검사 효과를 얻을 수 있도록 돕습니다. 이는 API의 안정성을 보장하고, 클라이언트와 서버 간의 데이터 계약을 명확하게 정의하는 데 기여합니다.
다음 절에서는 API 문서화를 위한 Swagger 통합에 대해 알아보겠습니다.