파이프는 @Injectable()
데코레이터로 주석이 달린 클래스이다.
파이프는 PipeTransform
인터페이스를 implement해야 한다.
파이프는 다음과 같이 두가지 경우에 사용한다.
두가지 경우 파이프는 controller route handler가 처리하는 arguments에서 작동한다.
Nest는 메서드 호출 직전에 파이프를 삽입하고, 파이프는 메서드로 향하는 인수를 수신하고 이에 대해 동작한다.
변환, 유효성 검사는 해당 시점에서 적용되며, 그 후 route handler가 변환된 인수와 함께 호출된다.
Nest에는 기본적으로 사용할 수 있는 여러 내장된 파이프가 있고, Custom하게 파이프를 만들 수도 있다.
Nest에는 즉시 사용할 수 있는 6개의 파이프가 제공된다.
ValidationPipe
ParseIntPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
DefaultValuePipe
@nestjs/common
패키지에서 export 된다.
ParseIntPipe
는 메서드 핸들러 매개변수가 자바스크립트 정수로 변환되도록 한다. 변환에 실패하면 예외처리를 수행한다.
파이프를 사용하기 위해서는 파이프 클래스의 인스턴스를 적절한 컨텍스트에 바인딩해야 한다.
다음 ParseIntPipe
예제에서는 파이프를 특정 라우트 핸들러 메서드와 바인딩하고, 메서드가 호출되기 전에 실행되는지 확인한다.
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
위와 같이 메서드에 바인딩 하고, 아래와 같은 요청을 보내면 다음과 같은 결과가 나온다
GET localhost:3000/abc
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
findOne()
메서드는 실행되지 않는다.
위의 예에서는 인스턴스가 아닌 클래스를 전달하였다.
아래의 예에서는 내부 인스턴스를 전달해보도록 하겠다. 내부 인스턴스를 전달하는 것은 옵션을 전달해 내장 파이프의 동작을 커스텀하려는 경우 유용하다.
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE })) //pipe 적용 시 에러가 발생할 경우 이처럼 에러코드 세팅
id: number,
) {
return this.catsService.findOne(id);
}
다음은 ParseUUIDPipe
를 사용해 문자열 매개변수를 구문분석하고 UUID인지 확인하는 예제이다.
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
Nest는 내장 ParseIntPipe
및 ValidationPipe
를 제공하지만, 커스텀한 파이프를 구성할 수도 있다.
각각 간단한 Custome Version을 만들어 Custom Pipes가 어떻게 구성되는지 보겠다.
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
모든 파이프는 PipeTransform
인터페이스를 implements 해야하며, transform()
메서드를 구현해야 한다.
이 메서드에는 두개의 매개변수가 있다.
value
metadata
value
매개변수는 현재 처리된 메서드의 매개변수이고, metadata
는 현재 처리된 메서드 매개변수의 메타데이터이다.
메타데이터 객체에는 다음과 같은 속성이 있다.
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
각 속성들의 의미는 다음과 같다.
type | 인수가 본문 @Body() , 쿼리 @Query() , param @Param() 또는 커스텀 매개변수인지 여부를 나타낸다 |
---|---|
metatype | 인수의 메타타입을 제공: 라우트 핸들러 메소드 signiture에서 타입 선언을 생략하거나 바닐라 자바스크립트를 사용하면 값이 정의되지 않음 |
data | 데코레이터에 전달된 문자열(예: @Body('string') ). 데코레이터 괄호를 비워두면 정의되지 않음 |
※ TypeScript 인터페이스는 변환중에 사라진다. 따라서 메서드 매개변수의 타입이 클래스가 아닌 인터페이스로 선언되면 metatype
값은 object
가 된다.
CatsController
의 create()
메서드 실행 전 매개변수 객체가 유효한지 확인해보고 싶을 것이다.
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
createCatDto
본문 매개변수의 타입은 CreateCatDto
이다.
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
create()
메서드의 객체가 유효한지 확인하기 위해 createCatDto
객체의 세 멤버를 검증해야 한다.
메서드내에서 이를 수행하는 것은 SRP(단일 책임 규칙)을 위반하므로 적절치 않다.
그래서 validator class를 만들고 이곳에 작업을 위임하거나, validation middleware를 만들어야 한다.
전자의 경우에는 메서드의 시작 부분에서 validator class를 호출해야 한다는 단점이 있고, 후자는 전체 어플리케이션의 모든 컨텍스트에서 사용할 수 있는 일반 미들웨어로 만드는 것은 불가능하다.(미들웨어가 호출될 handler 및 매개변수를 포함해 실행 컨텍스트를 인식하지 못하기 때문이다.)
효율적으로 객체 유효성 검사를 수행하는 몇가지 방법이 있다.
스키마 기반 유효성 검사
Joi 라이브러리를 사용해 읽기 쉬운 API를 사용하여 간단한 방식으로 스키마를 만들 수 있다.
npm install --save joi
npm install --save-dev @types/joi
아래 코드 샘플은 스키마를 constructor
매개변수로 사용하는 클래스이다.
제공된 스키마에 대해 들어오는 인수의 유효성을 검사하는 schema.validate()
메서드를 적용한다.
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ObjectSchema } from 'joi';
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private schema: ObjectSchema) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = this.schema.validate(value);
if (error) {
throw new BadRequestException('Validation failed');
}
return value;
}
}
변환 파이프를 바인딩하는 것은 위에서 살펴보았다.
ValidationPipe
를 바인딩 하는 것도 매우 간단하다.
JoiValidationPipe
를 메서드 수준에서 바인딩 하려면 다음을 수행해야 한다.
JoiValidationPipe
의 인스턴스를 만든다.아래와 같이 @UsePipes()
데코레이터를 사용한다.
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
Class-Validator 라이브러리를 사용해 데코레이터 기반 유효성 검사를 할 수 있다.
데코레이터 기반 유효성 검사는 처리된 속성의 metatype
에 엑세스 할 수 있으므로, Nest의 파이프 기능과 결합할 때 매우 유용하다
npm i --save class-validator class-transformer
설치를 수행하면 CreateCatDto
클래스에 데코레이터를 몇개 추가할 수 있다.
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
이제, 이 데코레이터들을 사용하는 ValidationPipe
클래스를 아래와 같이 만들 수 있다.
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
코드를 보면 transform()
메서드 앞에 async
키워드가 붙어있다. 이는 일부 class-validator
에서 유효성 검사 결과가 Promise
로 반환될 수 있기 때문에 그렇다.
다음으로, 매개변수에서 ArgumentMetadata
에서 metatype
만 추출한다.
다음으로, Helper Function toValidate()
를 확인한다. 처리중인 현재 매개변수가 네이티브 자바스크립트 타입이면, 유효성 검사 단계를 건너뛴다.(유효성 검사 데코레이터를 연결할 수 없기 때문이다.)
다음으로, class-transformer function plainToClass()
를 사용해 일반 자바스크립트 인수 객체를 타입이 지정된 객체로 변환해 유효성 검사를 적용할 수 있다. 이 작업의 수행 이유는 네트워크 요청에서 역직렬화될 때 들어오는 POST 본문 객체가 아무 타입 정보도 가지고 있지 않기 때문이다. class-transformer는 이전에 DTO에 대해 정의한 유효성 검사 데코레이터를 사용해야 하므로 들어오는 본문을 단순한 바닐라 객체가 아닌 decorated 객체로 처리하기 위해 이 변환을 수행해야 한다.
마지막으로는 값을 그대로 반환하거나 예외를 던진다.
마지막 단계는 ValidationPipe
를 바인딩 하는 것이다.
파이프는 매개변수 범위, 메서드 범위, 컨트롤러 범위, 전역 범위로 바인딩 된다.
아래 예제에서는 파이프 인스턴스를 route handler @Body()
데코레이터에 바인딩하여 파이프가 POST 본문의 유효성을 검사하도록 호출한다.
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
매개변수 범위 파이프는 유효성 검증 로직이 지정된 매개변수 하나에만 관련될 때 유용하다.
ValidationPipe
는 Nest에서 제공하므로, 유효성 검사 파이프를 직접 빌드할 필요는 없다.
ValidationPipe
는 일반적으로 생성되었으므로, 전체 어플리케이션의 모든 route handler에 적용되도록 전역 범위 파이프로 설정할 수 있다.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
하이브리드 어플리케이션의 경우
uesGlobalPipe()
메서드는 파이프를 전역으로 마운트하지 않는다.
Excpetion Filter와 마찬가지로 전역 범위 파이프로 설정할 때 모듈 외부에서 등록했으므로, 종속성을 주입할 수 없다.
종속성 주입을 가능하게 하기 위해 모든 모듈에서 직접 전역 파이프를 설정하면 된다.
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
파이프를 통해 전달된 데이터의 변환은 다음과 같은 경우에 유용하다.
클라이언트에서 전달된 데이터가 route handler 메서드에 의해 처리되기 전에 string -> int로 변환할 때,
일부 필수 데이터 필드가 누락되었을 때, 기본값을 적용하려는 경우
다음은 문자열을 정수로 변환하는 ParseIntPipe
이다.
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
해당 파이프를 아래와 같이 선택한 매개변수에 바인딩 한다.
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return this.catsService.findOne(id);
}
또 다른 사례는 전달된 ID를 통해 DB에서 사용자를 선택하는 것이다.
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
변환 파이프에 매개변수로 null
이나 undefined
값을 받게되면 예외가 발생한다.
엔드포인트가 누락된 쿼리 문자열 매개변수 값을 처리할 수 있게 하려면, DefaultValuePipe
를 이용해 중간 과정을 거치게 하면 된다.
해당 파이프 앞에 @Query()
데코레이터에서 DefaultValuePipe
를 인스턴스화하면 된다.
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}