Search
Duplicate

[오버뷰] 07.Pipes

Created
2021/07/02 02:54
Tags

Pipes

Pipe 는 @Injectable() 데코레이터가 달린 클레스 이다.
Pipe 는 PipeTransform interface 를 implement 해야 한다.
Pipe 는 두 가지 일반적인 사용 사례가 있습니다.

Transformation : 원하는 폼으로 인풋데이터를 바꾸는 것 (예를들어, string to integer)

Validation : 인풋 데이터를 검사하여 valid 하지 않은 경우 throw an exception, 아닐 경우 단순히 통과

두 경우 모두, 파이프는 컨트롤러 라우트 핸들러에 의해 처리되는 중인 arguments 위에서 작동합니다.
네스트는 (컨트롤러 라우트 핸들러) 메소드가 호출되기 전 파이프를 개입시킵니다. 그리고 파이프는 메소드로 향하는 아규먼트를 받아서 동작합니다.
모든 Transformation 이나 Validation 은 이 시점에 동작합니다.
그 후에 라우트 핸들러가 (잠재적으로) 변환된 아규먼트를 받아 호출됩니다.
네스트는 기본적으로 여러 빌트인 파이프를 지원합니다. 너는 개꿀로 사용할수 있습니다.
너는 너의 커스텀 파이프를 만들 수도 있습니다.
이 쳅터에서, 여기서는 빌트인 파이프를 소개하고, 어떻게 라우트핸들러에 바인드하는지 알아보겠습니다.
그런 후에 커스텀 파이프를 사용하는 방법을 보겠습니다.

HINT Pipe 들은 exceptions zone 에서 동작합니다. 이 뜻은, 파이프가 에러를 던지면 이것은 exception layer (글로벌 익셉션 필터, 그리고 현재 컨텍스트에 적용된 모든 인셉션 필터들) 에서 핸들링 된다는 것입니다. 위에서 말했다 시피, 파이프에서 발생된 예외는 명료합니다. 따라서 어떠한 컨트롤러 메소드도 그 후에 실행되지 않습니다. 이것은 개꿀입니다.

Built-in pipes

네스트는 당신이 개꿀로 사용할 수 있는 6개 빌트인 파이프를 지원합니다. (@nestjs/common 패키지에 있습니다)
ValidationPipe
ParseIntPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
DefaultValuePipe
ParseIntPipe 를 사용해보자.
이 예제는 transformaion 케이스 이다.
여기서 파이프는 메소드 핸들러 파라미터가 JS Integer 로 변환되게 합니다.(변환에 실패할 경우 에러를 띄웁니다)
이장 뒷부분에서 ParseIntPipe 를 시행하는 간단한 사용자 지정 파이프를 설명할 것입니다.

Binding pipes

파이프를 사용하기 위해, 적당한 컨텍스트에 파이프 클레스 인스턴스를 바인드 해야합니다.
ParseInPipe 예에서, 우리는 특정한 라우트 핸들러 메소드와 같이 동작하도록 했습니다.
그리고 이 라우트 핸들러 메소드가 실행되기 전에 파이프가 실행됩니다.
@Get(':id') async findOne(@Param('id', ParseIntPipe) id: number) { return this.catsService.findOne(id); }
Plain Text
복사
예제에서 아래와 같은 콜을 하면 그 아래 리턴을 볼수 있습니다.
GET localhost:3000/abc
Plain Text
복사
{ "statusCode": 400, "message": "Validation failed (numeric string is expected)", "error": "Bad Request" }
Plain Text
복사
이 예외는 findOne() 메소드가 실행되는 것을 방지한다.
우리는 위 예에서 ParseIntPipe 의 인스턴스화와 디펜던시 인잭션의 책임을 프레임워크에 넘기기 위해 ParseIntPipe 를 클레스로 전달하였다.
파이프, 가드와 마찬가지로, 여기에 인스턴스를 전달 할 수도있다.
인스턴스를 전달하는 것은, 파이프 내부의 행동을 커스터마이징 하기를 원할때 유용하다.
@Get(':id') async findOne( @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE })) id: number, ) { return this.catsService.findOne(id); }
Plain Text
복사
다른 Parse 로시작하는 모든 파이프들이 비슷하게 동작한다.
이 파이프들은 모두 라우트 파라미터, 쿼리스트링 파라미터, 요청 바디벨류 를 검사하는데 사용됩니다.
@Get() async findOne(@Query('id', ParseIntPipe) id: number) { return this.catsService.findOne(id); }
Plain Text
복사
ParseUUIDPipe 는 string parameter 를 받아서 UUID 로 만든다.
@Get(':uuid') async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) { return this.catsService.findOne(uuid); }
Plain Text
복사

HINT : ParseUUIDPipe() 에서 당신이 특정 버전의 UUID 로 파싱하기를 원한다면 파이프에 옵션으로추가 할 수 있다.

Validation pipe 를 바인딩 하는 것은 조금 더 어렵다. 아래에서 알아보자

HINT : Also, see Validation techniques for extensive examples of validation pipes

Custom pipes

말했다 시피 우리의 자체 커스텀 파이프를 만들 수 있다.
비록 네스트가 멋진 ParseIntPipeValidationPipe 를 지원하지만, 커스텀 버전으로 만들어보자
우리는 단순한 ValidationPipe 를 만들 것이다.
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common'; @Injectable() export class ValidationPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { return value; } }
Plain Text
복사

HINT : PipeTransform<T, R> 은 모든 파이프에 의해 implemented 되는 일반적인 인터페이스 이다.

T : value type of the input, R : return type of transform() method.
모든 파이프는 반드시 PipeTransform 인터페이스를 완성하기 위해 transform() 메소드를 구현해야 한다.
이 메소드는 두개의 파라미터가 있다 (value, metadata)
value 파라미터는, 현재 처리중인 메소드에 들어가는 아규먼트이다(라우트 핸들러 메소드가 처리되기 전)
metadata 는 현재 처리중인 메소드 아규먼트의 메타데이터이다. 메타데이터 오브젝트의 프로퍼티들은 아래와 같다 :
export interface ArgumentMetadata { type: 'body' | 'query' | 'param' | 'custom'; metatype?: Type<unknown>; data?: string; }
Plain Text
복사
위 프로퍼티들은 현재 처리되는 아규먼트를 표현한다.
type : 아규먼트가 body @Body() , query @query() , param @Param() 이나 다른 커스텀 파라미터 어디에 있는지 가르쳐준다. metatype : 메타데이터의 타입을 제공해준다. 예를들어 String, 만약 라우트 핸들러 메소드에서 타입 정의를 생략하였거나, 바닐라 자바스크립트라면 이 값은 undefined 가 된다. data : 데코레이터로 전달되는 string, 예를들어 @Body('string') 데코레이터가 @Body() 이런상태 라면 이 값은 undefined 가 된다.

WARNING : 타입스크립트 인터페이스들은 transpilation 도중 사라진다. 그러므로 메소드 파라미터의 타입이 class 가 아니라 interface 로 선언 되어 있다면 metatype 의 value 는 Object 가 된다.

Schema based validation

우리의 validation pipe 를 더욱 활용성있게 만들어보자.
CatsControllercreate() 메소드가 해당 서비스를 실행하기 전에 body 가 valid 한지 검사하게 만들도록 해보자
@Post() async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); }
Plain Text
복사
createCatDto 에 바디파라미터를 맞춰보자. 이것은 아래와 같다
export class CreateCatDto { name: string; age: number; breed: string; }
Plain Text
복사
우리는 create method 로 들어오는 요청들이 valid 한 바디인지 확실시 하고 싶다.
따라서 우리는 createCatDto 오브젝트의 3 맴버를 검사해야 한다.
우리는 물론 라우트 핸들러 메소드 안에서 할 수도 있다.
하지만 이것은 single responsibility rule (하나의 클레스는 하나의 책임만 가진다) 는 SRP 위반이므로 이상적이지 않다!
다른 접근은, validator class 를 만들고 일을 거기에 위임하는 것이다.
하지만 이것은 우리가 꼭 이 라우터 핸들러 메소드 의 시작부분에 이 validator 함수를 집어넣는 것을 기억해야 한다는 디스어디벤티지가 있다.
그럼 validation 미들웨어를 만들면 어떨까?
This could work, 하지만 안타깝게도 어플리케이션 전체의 모든 컨텍스트에서 사용가능한 일반적인 미들웨어를 만드는 것은 불가능하다.
왜냐면 미들웨어는 미들웨어 이후에 called 될 라우트 핸들러 메소드와 모든 파라미터를 포함하는 execution context를 모르기 때문이다.
이것을 해결하기 위해 정확히 파이프가 디자인 되었다.
자 다시 우리의 validation pipe 를 정제해보자

Object schema validation

오브젝트 벨리데이션을 깨끗하고, DRY 하지 않게 만들기 위한 여러 방법이 있다.
Joi 라이브러리를 사용하면 스키마를 리더블한 API 로 잘 만들어 낼 수 있다.
Joi-based schemas 를 사용하여 우리의 발리데이션 파이프를 빌드해보자
$ npm install --save joi $ npm install --save-dev @types/joi
Plain Text
복사
아래 코드 ㅅ매플에서, 스키마를 생성자 인수로 사용하는 간단한 클래스를 만듭니다.
그런 다음 제공된 스키마에 들어오는 인수의 유효성을 검사하는 schema.validate() 메서드를 적용합니다.
위에서 말했다시피 validation pipe 는 변하지 않은 벨류를 리턴하거나, 예외를 일으킵니다.
다음 섹션에서, @UsePipe() 데코레이터를 사용하는, 주어진 컨트롤러 메소드에 올바른 스키마를 어떻게 적용시키는 지 볼수 있습니다.
이렇게 만드는 것은, 우리의 벨리데이션 파이프가 컨텍스트에 대해 re-usable 하게 만듭니다.
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; } }
Plain Text
복사

Binding validation pipes

이런 케이스에서, 라우트 핸들러 메소드 콜 레벨에서 파이프를 바인드 하기 원한다면, 우리 예제에서 처럼
우리는 JoiValidationPipe 를 사용하기 위해 아래의 행위를 해야 한다 :
1.
Create an instance of the JoiValidationPipe
2.
Pass the context-specific Joi schema in the JoiValidationPipe class constructor
3.
Bind the pipe to the method
우리는 위의 일을 @UsePipes() 데코레이터를 통해 할 수 있다.
@Post() @UsePipes(new JoiValidationPipe(createCatSchema)) async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); }
Plain Text
복사

Class validator

이 기술은 Typescript 로 이루어져있습니다. 바닐라 JS 를 사용한다면 이것을 사용 할 수 없습니다.
네스트는 class-validator 라이브러리와 잘 어울린다.
이 파워풀한 라이브러리는 데코레이터 베이스 벨리데이션을 사용 할 수 있도록 해준다.
데코레이터-베이스 벨리데이션은 네스트의 Pipe 능력과 결합되었을 때 진짜 개쩔게 강력하다
왜냐면 우리가 처리중인 metatype property 에 접근 할 수 있기 때문이다.
시작하기전 npm 을 하자
npm i --save class-validator class-transformer
Plain Text
복사
한번 인스톨하면, 우리는 CreateCatDto class 에 몇몇 데코레이터를 추가 할 수 있다.
여기에 중요한 기술적 어드벤티지가 존재한다! : CreateCatDto 클레스는 우리의 Post body object 를 위한 진실된 하나의 소스로 남는다. (별도의 유효성 검사 클래스를 만들 필요 없이)
import { IsString, IsInt } from 'class-validator'; export class CreateCatDto { @IsString() name: string; @IsInt() age: number; @IsString() breed: string; }
Plain Text
복사

HINT class-validator 데코레이터에 대해서는 여기 서 더 알아 볼 수 있다.

이 데코레이터를 사용하여 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); } }
Plain Text
복사

NOTICE class-transformer 라이브러리와 class-validator 라이브러리는 같은 제작자가 만들었다. 따라서 둘이 겁나 잘 동작한다.

코드로 들어가보자. 첫째로 transform() 메소드는 async 이다.
네스트가 비동기, 동기 두 버전의 pipe 를 지원하므로 가능하다
우리는 이 메소드를 async 로 만들어야 한다. 왜냐하면 class-validator 의 몇몇 메소드들이 비동기 이기 때문이다.
우리는 metatype field 를 추출하기 위해 비 구조화 할당을 하였다.
이것은 전체 ArgumentMetadata 를 가져오는 빠른 방법입니다.
그리고 그다음 metatype variable 에 할당된 추가적인 statement 가져오는 것이 있습니다.
다음 helper function 인 toValidate() 를 보세요.
이것은 처리중인 현재 아규먼트가 네이티브 자바스크립트 타입일 경우 유효성 검사를 바이패싱 하는 책임이 있습니다. (여기에는 validation decorators 를 붙일 수 없으므로, validation step 을 통해 실행할 이유가 없습니다.)
다음으로, 우리의 플레인 자바스크립트 아규먼트 오브젝트를 타입드오브젝트(우리가 validation 을 적용시킬 수 있는) 로 변환시키기 위해 우리는 class-transformer function 인 plainToClass() 을 사용했습니다. ( 말진짜 개 길고 어렵게 한다)
우리가 이렇게 해야 하는 이유는, network request 로 부터 역직렬화된 때 들어온 post body object 는 does not have any type information 이기 떄문입니다.
class-validator 는, validation 데코레이터(이전 DTO에서 정의한) 를 사용해야 되기 때문에, 우리는 들어온 바디를 플레인 바닐라 오브젝트가 아니라, 적당한 데코레이트된 오브젝트로 변경해야 합니다.
마지막 스탭은 ValidationPipe 를 바인드 하는 것입니다.
Pipes 는 파라미터스코프, 메소드스코프, 컨트롤스코프, 글로벌스코프 를 가질 수 있습니다.
메소드 레벨의 파이르 바인딩을 예제로 보겠습니다.
@Body() 데코레이터의 라우트 핸들러에 파이프 인스턴스를 바인드합니다.
따라서 우리의 pipe 는 Post 의 body 를 검사합니다.
파라미터 스코프 파이프는 유효성 검증 논리가 지정된 매개변수 하나만 관련될때 유용합니다.

Global scoped pipes

아래와 같이 적용 할 수 있다.
async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); } bootstrap();
Plain Text
복사
글로벌 파이프는 모든 어플리케이션(모든 컨트롤러, 모든 라우트핸들러) 에서 사용됩니다.
의존성 주입 관점에서, 어떤 외부 모듈로부터 등록된 글로벌 파이프(with useGlobalPipes() as in the example above) 는 의존성을 주입 할 수 없다. 왜냐하면 어떤 모듈의 컨텍스트 밖에서 바인딩 되었기 때문입니다.
이러한 이슈를 해결 하기 위해, 아래 construction 을 사용하여 글로벌 파이프를 directly from any module 로 만들 수 있습니다.
import { Module } from '@nestjs/common'; import { APP_PIPE } from '@nestjs/core'; @Module({ providers: [ { provide: APP_PIPE, useClass: ValidationPipe, }, ], }) export class AppModule {}
Plain Text
복사

The built-in ValidationPipe

네스트에 의해서 기본적으로 제공되는 ValidationPipe 가 있다.
빌트인 ValidationPipe 는 우리가 만들어본 간단한 얘보다 더 많은 옵션을 제공한다.
모든 디테일을 보려면 여기

Transformation use case

Validation 은 유일한 커스텀 파이프를 사용하는 케이스가 아니다.
쳅터 초반부에 설명했던 것처럼, 파이프는 인풋데이터를 원하는 포맷으로 transform 할 수 있다.
이것은 가능하다 왜냐하면 transform 함수가 리턴하는 벨류는 이전 아규먼트들의 벨류를 완벽하게 오버라이드한 것이기 때문이다.
언제 이걸 쓰냐고?
클라이언트로부터 온 데이터가 어떤 변화를 겪게 하고 싶을때 고려해볼 수있다. ( 말진짜 ㅈ나 어렵게한다 진짜 그냥 클라이언트로 부터 온 데이터를 변경시키고 싶을때 라고 하면 되잖아 엠병진짜)
예를들어 string 에서 integer 로 바꾸고 싶을때
라우트 핸들러 메소드에 의해 적절하게 핸들링 될 수 있습니다.
더 나아가서, 어떤 요구되는 데이터 필드가 없을때, 우리는 디폴트 벨류를 적용할 수 있습니다.
Transformation pipes 는 클라이언트 요청과 핸들러 사이에 처리기능을 삽입하여 이러한 기능을 수행 할 수 있습니다.
여기 string 을 integer value 로 바꾸는 책임을 가지는 ParseIntPipe 예제가 있습니다.
빌트인 PraseIntPipe 는 굉장히 복잡하기 때문에 간단하게 커스텀 파이프를 만들어 썼습니다.
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; } }
Plain Text
복사
아래와 같이 바인딩 할 수 있다.
@Get(':id') async findOne(@Param('id', new ParseIntPipe()) id) { return this.catsService.findOne(id); }
Plain Text
복사
또 다른 유용한 transformation 케이스는 existing user 엔티티를 베이터베이스로 부터 가져올때 이다.
@Get(':id') findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) { return userEntity; }
Plain Text
복사
위 파이프 구현은 독자에게 맡기지만 다른 변환 파이프와 마찬가지로 입력값(id) 를 받고, 출력값 (UserEntity 개체) 를 반환합니다. 이렇게 하면 핸들러에서 공통파이로 상용구 코드를 추상화하여 코드를 더 선언적으로 DRY 로 만들 수 있습니다.

Providing defaults

Parse* 파이프들은 파라미터들이 디파인되어 있음을 가정합니다. 파라미터가 null 이거나 undefined value 일때는 예외를 던집니다.
우리는 엔드포인트가 빠진 쿼리스트링 파라미터 벨류를 처리할수 있도록 디폴트 벨류를 Parse* 파이프를 부르기 전에 집어넣을 수 있습니다.
DefaultValuePipe 는 이러한 작업을 해줍니다.
아래와 같이 @Query() 데코레이터안에 Parse* 파이프를 부르기 전에 인스턴스화 하여 적어줄 수 있습니다.
@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 }); }
Plain Text
복사