07 [NestJS] Pipe(class-validator, class-transformer)
DTO 포스팅에서ValidationPipe를 처음 접했다. DTO 클래스에 데코레이터 몇개 붙이는 것만으로 유효성 검사가 자동으로 처리되다니.
Express에서if문으로 가득 찬 유효성 검사 코드를 짜던 때와는 다른 경험이었다.이번엔 파이프(Pipe) 와, 그 파이프를 더욱 강력하게 만들어주는class-validator와class-transformer
에 대해 포스팅한다.
파이프(Pipe)의 두 가지 핵심
NestJS 공식 문서에 따르면, 파이프는 @Injectable() 데코레이터가 붙은 클래스다. 파이프의 주된 임무는 두가지로 나뉜다.
- 유효성 검사(Validation): 입력 데이터가 우리가 정한 규칙에 맞는지 검사한다. 규칙에 어긋나면, 요청이 컨트롤러에 도달하기 전에 예외(Exception)를 던져 실행을 중단시킨다.
- 데이터 변환(Transformation): 입력 데이터를 우리가 원하는 타입이나 형태로 변환한다. 예를 들어, URL 파라미터로 들어온 문자열 '123'을 숫자
123으로 바꿔주는 식이다.
NestJS에는 ValidationPipe, ParseIntPipe, ParseUUIDPipe등 여러 내장 파이프가 있지만, 그중에서도ValidationPipe는 DTO와 결합하여 가장 널리, 그리고 가장 강력하게 사용된다.
class-validator: DTO에 규칙을 부여하다
ValidationPipe가 일을할 수 있는건, class-validator 라이브러리 덕분이다. 이 라이브러리는 우리가 DTO 클래스의 프로퍼티 위에 붙이는 수많은 유혀성 검사 데코레이터를 제공한다.
import {
IsString,
IsEmail,
IsNotEmpty,
IsNumber,
Min,
Max,
IsOptional,
IsEnum,
} from 'class-validator';
// 여러 유저 역할을 정의하는 Enum
enum UserRole {
ADMIN = 'ADMIN',
USER = 'USER',
}
export class CreateUserDto {
@IsString()
@IsNotEmpty() // 빈 문자열을 허용하지 않음
readonly name: string;
@IsEmail() // 이메일 형식 검사
readonly email: string;
@IsNumber()
@Min(19) // 최소값 검사
@Max(100) // 최대값 검사
readonly age: number;
@IsEnum(UserRole) // role 프로퍼티가 UserRole Enum에 속하는 값인지 검사
@IsOptional() // 이 필드는 선택 사항 (없어도 통과)
readonly role?: UserRole; // ?를 붙여 optional 프로퍼티로 선언
}
이렇게 DTO 클래스 자체에 유효성 규칙이 명시적으로 선언되니, 이 DTO가 어떤 데이터를 기대하는지 한눈에 파악할 수 있어 정말 좋았다. 유효성 검사 로직이 컨트롤러나 서비스에 흩어지지 않고, 데이터의 '모양'을
정의하는 DTO에 응집되니 코드의 전체적인 구조가 풜씬 깔끔해졌다.
class-transformer: 데이터의 타입을 바꾸다
네트워크를 통해 전달되는 데이터는 기본적으로 평범한 텍스트(plain text)에 가깝다. 예를 들어 GET/users/123이라는 요청에서 id 파라미터인 123은 숫자 123이 아니라 문자열"123"이다.
이떄 calss-transformer를 사용한다. 이 라이브러리는 들어온 데이터를 우리가 DTO 클래스에 정의한 실제 타입(number, boolean, Data, ...)으로 자동변환을 해주는 역할을
한다.
이 기능은 ValidationPipe의 transform 옵션을 true로 설정하면 된다.
// src/main.ts
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 이 옵션이 class-transformer를 활성화한다!
}),
);
이제 컨트롤러에서 id를 받을 떄, NestJS의 또다른 내장 파이프인 ParseIntPipe를 사용하면 이 변환 과정을 더 명시적으로 처리할 수 있다.
// src/users/users.controller.ts
@Get(':id')
// ParseIntPipe가 id 파라미터를 숫자로 변환하고, 실패하면 예외를 던진다.
findOne(@Param('id', ParseIntPipe)
id: number
)
{
// 이제 'id'는 문자열 "123"이 아닌 숫자 123이다.
console.log(typeof id); // "number"
return this.usersService.findOne(id);
}
ValidationPipe의 transform: true 옵션과 ParseIntPipe같은 변환 파이프 덕분에, 서비스나 컨트롤러에서는 더이상 parseInt()같은 번거로운 타입 변환 코드를 신경 쓸
필요가 없다.
ValidationPipe옵션 제대로 활용하기
ValidationPipe는 몇 가지 유용한 옵션을 추가로 제공한다. 이 옵션들은 애플리케이션의 안정성과 보안을 위해 설정해두는 것이 좋다.
// src/main.ts
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
whitelist: true: DTO에 정의되지 않은 속성이 요ㅇ에 포함되어 있다면, 그 속성을 자동으로 제거하고 DTO를 생성한다. 예를 들어 클라이언트가 악의적으로role: 'ADMIN'같은 데이터를
보내더라도, DTO에role이 정의되어 있지 않다면 이 속성은 무시된다.forbidNonWhitelisted: true:whitelist에서 한 걸음 더 나아간다. DTO에 정의되지 않은 속성이 감지되면, 요청 자체를 차단하고400 Bad Request에러를 리턴한다.
훨씬 엄격한 규칙이다.transform: true: 위에서 설명한class-transformer의 자동 타입 변환 기능을 활성화한다.
마무리
- 파이프는 컨트롤러로 향하는 데이터의 문지기.
class-validator는 그 문지기가 들고 있는 규칙 목록.class-transformer는 방문자의 신분(타입)을 정확하게 바꿔주는 변환기.