새소식

반응형
Back-end/NestJS

[NestJS] Validation과 Transformation (검증과 변환) (feat. Serialization-직렬화)

2024.02.12
  • -
반응형

1. Validation (검증)

웹 애플리케이션에서는 전달되는 모든 데이터의 정확성을 검증하는 것이 가장 좋은 시나리오일 것입니다.

 

그래서 들어오는 요청이 유효한지를 자동으로 검사하기 위해 Nest는 바로 사용할 수 있는 몇 가지의 내장 기본 pipes를 제공하고 있습니다.

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe

 

이러한 ValidationPipe는 강력한 class-validiator 패키지와 선언적인 validation 데코레이터를 사용합니다.

 

ValidationPipe는 모든 들어오는 클라이언트의 페이로드(payload)에 대한 검증 규칙을 강화하기 위해 간편한 접근을 제공합니다. 그 구체적인 규칙은 local 클래스 또는 DTO 선언 부분에 간단한 annotation(decorator)을 달아줌으로써 정해질 수 있습니다.

 

1-1. 개요

Pipes 포스팅에서 간단한 pipe를 만들어보고 그 pipe를 컨트롤러/메서드/전역 수준에서 앱에 바인딩하는 과정을 알아보면서 프로세스가 어떻게 동작하는지에 대해 살펴보았었습니다.

 

이번 포스팅을 잘 이해하려면 해당 포스팅을 반드시 보셔야 합니다. 여기서는 ValidationPipe의 다양한 실제 사용 사례에 초점을 맞춰, 고급 custom 기능 중 일부를 사용하는 방법에 대해서 알아볼 것입니다.

 

class-validator와 class-transform 라이브러리 기능들

 

validation과 transformation을 위해 Nest에서는 class-validatorclass-transformer라이브러리가 주로 사용되며 위와 같이 공식 깃헙 레파지토리에 방문하시면 원하시는 기능을 찾아 적재적소에 사용할 수 있습니다.

 

1-2. 기본 제공 ValidationPipe 사용하기

시작에 앞서 필요한 의존성을 설치합니다.

$ npm i --save class-validator class-transformer

 

이 pipe는 class-validator와 class-transfomer 라이브러리를 사용하기 때문에 많은 옵션을 기본으로 사용할 수 있습니다.

 

pipe에 전달되는 configuration 객체를 통해 여러 세팅을 진행할 수 있습니다. 다음은 기본 제공 옵션입니다.

export interface ValidationPipeOptions extends ValidatorOptions {
  transform?: boolean;
  disableErrorMessages?: boolean;
  exceptionFactory?: (errors: ValidationError[]) => any;
}

 

 

물론 추가적으로 ValidatorOptions 인터페이스로부터 상속된 class-validator의 모든 옵션에는 다음과 같은 것들이 있습니다.

 

  • enableDebugMessages<boolean>
    • true로 설정 시, validator가 무언가 잘못되었을 때 콘솔에 추가 경고를 출력합니다.
  • skipUndefinedProperties<boolean>
    • true로 설정 시, 검증하는 객체에서 undefined인 속성의 검증은 모두 생략합니다.
  • skipNullProperties<boolean>
    • true로 설정 시, 검증하는 객체에서 null인 속성의 검증은 모두 생략합니다.
  • skipMissingProperties<boolean>
    • true로 설정 시, 검증하는 객체에서 null 또는 undefined인 속성의 검증은 모두 생략합니다.  
  • whitelist<boolean>
    • true로 설정 시, 어떠한 검증 데코레이터도 사용하지 않는 속성은 whitelist에 등록되지 않은 것으로 간주하여 검증 대상으로부터 제거됩니다.
    • 만약 어떠한 데코레이터도 적합하지 않아 붙일 것이 없는 속성에는 @Allow() 라도 붙여주면 됩니다.
  • forbidNonWhitelisted<boolean>
    • true로 설정 시, whitelist에 없는 속성을 제거하는 대신 validator가 예외를 던집니다.
  • forbidUnknownValues<boolean>
    • true로 설정 시, 검증 에러가 클라이언트에게 반환되지 않습니다.
  • errorHttpStatusCode<number>
    • 이 설정은 어느 예외 타입이 에러 발생 시 사용될 것인지를 명시하도록 해줍니다. 기본값으로는 BadRequestException 예외를 발생시킵니다.
  • exceptionFactory<Function>
    • 검증 에러의 배열을 받아 발생될 exception 객체를 반환합니다.
  • groups<string[ ]>
    • 객체의 검증 동안에 사용될 그룹입니다.
  • always<boolean>
    • 데코레이터의 always 옵션을 기본 값으로 설정합니다. 데코레이터 옵션에서 default 값을 override 할 수 있습니다.
  • strictGroups<boolean>
    • groups 옵션이 주어지지 않았거나 빈 배열로 주어졌다면 하나 이상의 그룹이 있는 데코레이터를 무시합니다.
  • dismissDefaultMessages<boolean>
    • true로 설정 시, 검증이 default 메시지를 사용하지 않습니다. 명시적으로 설정해주지 않으면 에러 메세지는 항상 undefined입니다.
  • validationError.target<boolean>
    • 타겟을 ValidationError에 노출할지 여부를 나타냅니다.
  • validationError.value<boolean>
    • 검증된 값을 ValidationError에 노출할지 여부를 나타냅니다.
  • stopAtFirstError<boolean>
    • true로 설정하면, 주어진 속성의 검증이 첫 번째 에러를 마주친 이후 중단됩니다. 기본값은 false입니다.

 

1-2. Auto-validation (자동 검증)

애플리케이션 level에서 ValidationPipe를 바인딩하여 모든 엔드포인트가 올바르지 않은 데이터를 받지 않도록 보장하도록 하려면 어떻게 해야 할까요? 다음 코드와 같이 useGlobalPipes를 사용할 수 있습니다.

 

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

 

이 pipe를 테스트하기 위해 기본 엔드포인트를 만들어줍시다.

@Post()
create(@Body() createUserDto: CreateUserDto) {
  return 'This action adds a new user';
}

 

TypeScript는 Generic이나 interface에 대한 메타데이터를 저장하지 않기 때문에, DTO에서 generic이나 interface를 사용할 경우 ValidationPipe가 요청과 함께 보내진 데이터의 유효성을 제대로 검증하지 못할 수 있습니다. 그렇기 때문에 DTO는 명시적인 클래스를 사용하는 것이 좋습니다.

 

DTO를 import 할 때는, 런타임 시에 타입이 지워지기 때문에 type-only import를 사용할 수 없습니다.
즉, import type { CreateUserDto } 대신 import { CreateUserDto } 와 같이 사용해야 합니다.

 

이제 CreateUserDto에 몇 가지 유효성 검사 규칙을 추가할 수 있습니다. 여기에 자세히 설명된 class-validator 패키지에서 제공하는 데코레이터를 사용하여 해당 작업을 수행합니다. 이렇게 하면 CreateUserDto를 사용하는 모든 라우트에서 이러한 유효성 검사 규칙이 자동으로 적용됩니다.

 

import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsNotEmpty()
  password: string;
}

 

이러한 규칙을 적용한 후, request body에 잘못된 이메일 속성이 포함된 요청이 엔드포엔트에 도달하면 애플리케이션이 자동으로 400 Bad Request 요청 코드와 함께 아래와 같은 response body로 응답합니다.

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": ["email must be an email"]
}

 

물론 request body에 대한 유효성 검사 이외에도, 다른 요청 객체 속성에 대해 ValidationPipe를 사용할 수 있습니다. 엔드포인트 경로에 :id 를 허용하고 싶다고 가정해 보겠습니다. 이 경우 해당 요청 매개변수에 숫자만 허용되도록 하려면 다음 구문을 사용합니다.

@Get(':id')
findOne(@Param() params: FindOneParams) {
  return 'This action returns a user';
}

 

FindOneParams는 DTO와 같이 class-validator를 사용하여 검증 규칙을 정의한 단순한 클래스입니다.

 

이는 다음과 같은 모습을 갖고 있습니다.

import { IsNumberString } from 'class-validator';

export class FindOneParams {
  @IsNumberString()
  id: number;
}

 

1-3. Disable detailed erros (세부적인 에러 표시하지 않기)

에러 메시지는 요청에 무엇이 올바르지 않은지 설명하는 데 유용합니다. 하지만 일부 production 환경에서는 이렇게 세부적인 에러가 표시되지 않기를 선호합니다. 이를 위해 ValidationPipe에 options 객체를 전달함으로써 설정값을 조정할 수 있습니다.

 

app.useGlobalPipes(
  new ValidationPipe({
    disableErrorMessages: true,
  }),
);

 

disableErrorMessages 속성값을 true로 조정하여, 더 이상 자세한 에러 메세지가 response body에 표시되지 않을 것입니다.

 

1-4. Stripping properties (속성 제거)

ValidationPipe는 메서드 핸들러로 전달되면 안 되는 속성을 필터링하는 역할도 할 수 있습니다. 이러한 경우에 허용할 속성에 대한 whitelist를 적용하여, 이 whitelist에 포함되지 않는 속성은 자동으로 결과 객체에서 제거되도록 합니다.

 

예를 들어, 어떤 핸들러에서 email과 password 속성만 받을 수 있도록 되어 있는데, 요청에 age 속성이 같이 포함되어 들어있다면, 해당 age 속성이 자동으로 최종 DTO에서 제거됩니다.

 

이러한 기능을 사용하려면 whitelist 속성값을 true로 설정해주면 됩니다.

app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
  }),
);

 

true로 설정 시, non-whitelisted 속성, 즉 화이트리스트에 올라가지 않은 속성을 제거한다는 의미인데 화이트리스트에 올라가지 않은 속성은 validation 클래스가 제공하는 어떠한 데코레이터도 붙지 않은 속성을 의미합니다.

 

또한 whitelist에 없는 속성이 요청에 존재하는 경우, 해당 요청을 중단하고 사용자에게 오류 reponse를 반환하고 싶다면, whitelist를 true로 설정하는 것과 더불어 forbidNonWhitelisted 속성값 또한 true로 설정해주면 됩니다.

 

1-5. Transform palyoad objects (변환 페이로드 객체)

일반적으로 네트워크를 통해 넘어오는 페이로드(payload)값은 plain JavaScript 객체입니다. 이때 ValidationPipe는 자동으로 페이로드의 value를 우리 코드의 DTO 클래스에 맞는 타입의 객체로 변환시켜줄 수 있습니다.

 

이러한 변환이 자동으로 이루어지도록 하려면, transform 옵션값을 true로 설정하면 되고, 이는 메서드 level에서 설정될 수 있습니다:

@Post()
@UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

 

이 변환을 전역적(globally)으로 하고 싶다면 앞서 봤던 것처럼 해당 옵션을 global pipe에 주면 됩니다.

app.useGlobalPipes(
  new ValidationPipe({
    transform: true,
  }),
);

 

auto-transformation 옵션을 활성화하면 ValidationPipe는 원시 타입(primitive type)의 변환도 수행해줍니다.

 

다음 예제에서 findOne() 메서드는 추출된 하나의 id path parameter 인자를 받습니다.

@Get(':id')
findOne(@Param('id') id: number) {
  console.log(typeof id === 'number'); // true
  return 'This action returns a user';
}

 

아시겠지만 기본적으로 모든 path parameter와 query parameter는 네트워크를 거쳐 문자열로 전달됩니다. 위의 예제에서는 메서드 시그니처에 id 타입을 숫자로 지정해 준 것을 볼 수 있는데, 이 때문에 전역으로 설정된 ValidationPipe가 이것을 보고 string 식별자를 number로 자동 변환 시켜주게 됩니다.

 

1-6. Explicit conversion (명시적 변환)

위에서 ValidationPipe가 속성의 예상되는 타입에 따라 query 및 path 매개변수를 내재적으로 변환해주는 것을 보았습니다. 하지만 이러한 기능을 사용하기 위해선 반드시 auto-transformation 설정을 활성화 해주어야 했습니다.

 

그렇지 않고서는 ParseIntPipe나 ParseBoolPipe와 같은 validation 클래스를 통해 명시적으로 값을 캐스팅해야 할 것입니다.

  • 참고로 앞서 말했듯 모든 path 매개변수와 query 매개변수는 기본적으로 네트워크를 통해 string 타입으로 넘어오기 때문에 ParseStringPipe와 같은 클래스는 필요없습니다.

 

명시적으로 값을 캐스팅하는 모습은 아래와 같이 될 것입니다.

@Get(':id')
findOne(
  @Param('id', ParseIntPipe) id: number,
  @Query('sort', ParseBoolPipe) sort: boolean,
) {
  console.log(typeof id === 'number'); // true
  console.log(typeof sort === 'boolean'); // true
  return 'This action returns a user';
}

 

1-7. Mapped types

CRUD(Create/Read/Update/Delete)와 같은 기능을 만들 때, 기본 Entity 타입에 transformation을 구성하는 것이 유용할 때가 많습니다. Nest는 타입 변환을 수행하는 여러 utility 함수를 제공하기 때문에 이 작업을 더 편리하게 할 수 있습니다.

 

WARNING
애플리케이션에서 @nestjs/swagger 패키지를 사용하는 경우 이 장에서 Mapped Types에 대한 자세한 내용을 참조하셔야 합니다. 마찬가지로 @nestjs/graphql 패키지를 사용하는 경우 이 장을 참고하시면 됩니다. 두 패키지 모두 타입에 크게 의존하기 때문에 이것들을 사용하려면 추가적인 import가 필요합니다. 따라서 @nestjs/swagger나 @nestjs/grahql과 같은 앱 유형에 따라 적절한 패키지가 아니라 @nestjs/mapped-types를 사용했다면, 공식문서에 없는 다양한 에러를 마주할 수도 있습니다.

 

입력 유효성 검사(Input validation)에 대한 타입들(DTO라고도 함)을 작성할 때는, 동일한 타입에 대한 create나 update 변형을 하는 것이 유용한 경우가 많습니다.

 

예를 들어, create 변형은 모든 필드를 필수로 설정하고, update 변형은 모든 필드를 선택 사항(optional)으로 지정합니다.

 

Nest는 이 작업을 더 쉽게 하고 bolierplate 코드를 최소화하기 위해 PartialType() 유틸리티 함수를 제공합니다. PartialType() 함수는 입력 유형의 모든 속성이 선택 사항으로 설정된 타입(클래스)을 반환합니다.

 

예를 들어, 다음과 같은 create 유형이 있다고 가정해 보겠습니다.

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

 

기본적으로 아무것도 명시하지 않으면, 이러한 필드들은 모두 필수로 지정되어 있는 것입니다. 그런데 만약 여기서 각 필드는 동일하지만 선택 사항이 되도록하는 또 다른 새로운 타입을 만들고 싶다면, 위 클래스에 대한 참조를 인자로 전달하는 PartialType()을 사용합니다. 

export class UpdateCatDto extends PartialType(CreateCatDto) {}

 

PickType() 함수는 입력 유형에서 속성 집합을 직접 골라서 새 유형을 구성하는 함수입니다. 

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

 

우리는 위 클래스로부터 PickType() 유틸리티 함수를 사용하여 원하는 속성 집합을 고를 수 있습니다.

export class UpdateCatAgeDto extends PickType(CreateCatDto, ['age'] as const) {}

 


OmitType() 함수는 입력 타입에서 모든 속성을 선택하고, 특정 키 집합을 제거하여 새 유형을 구성합니다.

 

예를 들어, 다음과 같은 DTO 타입이 있다고 가정해 봅시다.

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

 

이때 아래와 같이 'name'을 제외한 모든 속성을 포함하는 이전 타입에서 파생된 새 타입을 생성할 수 있습니다. 이 구조에서 OmitType의 두 번째 인자는 제외 시킬 속성 이름을 원소로 갖는 배열입니다.

export class UpdateCatDto extends OmitType(CreateCatDto, ['name'] as const) {}

 


IntersectionType() 함수는 두 개의 타입을 하나의 새로운 타입(클래스)로 결합합니다.

 

예를 들어 아래처럼 두 가지 타입이 있다고 가정해 보겠습니다.

export class CreateCatDto {
  name: string;
  breed: string;
}

export class AdditionalCatInfo {
  color: string;
}

 

아래는 IntersectionType의 사용 예시입니다.

export class UpdateCatDto extends IntersectionType(
  CreateCatDto,
  AdditionalCatInfo,
) {}

 

이렇게 하면 두 가지 타입의 모든 속성을 결합한 하나의 새로운 타입을 생성하게 됩니다.

 

 

위와 같은 타입 매핑(Type Mapping) 유틸리티 함수들은 같이 결합해서 사용하는 것도 가능합니다.

 

예를 들어, 다음 예시처럼 이름을 제외한 CreatecatDTO 유형의 모든 속성을 가진 타입(클래스)를 생성한 다음, 해당 속성들이 선택 사항으로 설정되도록 할 수 있습니다.

export class UpdateCatDto extends PartialType(
  OmitType(CreateCatDto, ['name'] as const),
) {}

 

1-8. Parsing and validating arrays (구문 분석 및 검증 배열)

TypeScript는 제네릭이나 인터페이스에 대한 메타데이터를 저장하지 않기 때문에 DTO에 제네릭이나 인터페이스를 사용할 경우 ValidationPipe가 들어오는 데이터의 유효성을 제대로 검증하지 못하게 됩니다. 다음 코드는 createUserDtos의 유효성 검사가 제대로 이루어지지 않는 예입니다.

@Post()
createBulk(@Body() createUserDtos: CreateUserDto[]) {
  return 'This action adds new users';
}

 

배열로 된 입력을 검증하기 위해선 해당 배열을 감싸는(wrap) 속성을 포함하는 전용 클래스를 생성하거나 ParseArrayPipe를 사용하는 방법이 있습니다.

@Post()
createBulk(
  @Body(new ParseArrayPipe({ items: CreateUserDto }))
  createUserDtos: CreateUserDto[],
) {
  return 'This action adds new users';
}

 

또한 ParseArrayPipe는 query 매개변수를 파싱할 때 유용하게 사용될 수 있습니다. query 매개변수로 전달된 id를 기반으로 user를 반환하는 findByIds() 메서드를 고려해봅시다.

@Get()
findByIds(
  @Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
  ids: number[],
) {
  return 'This action returns users by ids';
}

 

이 구조는 다음과 같이 HTTP GET 요청에서 들어오는 query 매개변수의 유효성을 검사할 수 있습니다.

GET /?ids=1,2,3

 

'1,2,3' 이라는 데이터는 그냥 넘어오게 되면 아무런 의미도 없는 문자열이지만 separator를 콤마(,)로 지정하여 ['1', '2', '3']의 배열 형태로 변환하고 각각의 items에 대하여 타입을 number로 지정해주었기 때문에 결과적으로 [1, 2, 3] 의 데이터로 변환이 되게 됩니다.

 

2. class-validator와 Global Pipe는 잘 어울린다.

Nest의 Global Pipe는 데이터 검증과 변환 로직을 애플리케이션 전역에 걸쳐 적용하는 솔루션을 우아하게 제공합니다. pipe를 전역 범위에서 구현함으로써 모든 라우트 핸들러에 검증 코드를 복제하지 않고 데이터 무결성을 보장하여 들어오는 요청의 데이터가 같은 검증 규칙을 수행하도록 해줍니다.

 

2-1. Global Pipe 구현

Global pipe를 구현하려면 main.ts 파일에 아래와 같은 코드를 작성해주면 됩니다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));

  await app.listen(3000);
}
bootstrap();

 

app.useGlobalPipes() 함수를 사용하여 ValidationPipe가 전역적으로 적용되도록 구성하여 데이터가 애플리케이션으로 들어갈 때 변환되고 검증되도록 합니다.

 

2-2. class-validator의 역할

class-validator는 데코레이터를 활용하여 DTO에 대한 유효성 검사 규칙을 정의하는 프로세스를 굉장히 간소화시킵니다. @IsNotEmpty()와 @IsString()과 같은 데코레이터를 사용하면 DTO 클래스 내에서 직접 유효성 검사 규칙을 선언할 수 있게됩니다.

 

예를 들어, 회원가입 기능을 만들어야 한다고 합시다. 여기서 class-validator를 사용하려면 먼저 CreateUserDto 클래스를 만들고 아래와 같이 적절한 유효성 검사 데코레이터로 해당 속성들을 데코레이팅 할 수 있습니다.

import { IsNotEmpty, IsString } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  @IsString()
  name: string;

  @IsNotEmpty()
  @IsString()
  email: string;

  // Additional validation rules can be added here
}

 

데이터가 애플리케이션으로 들어오면 이와 같이 DTO에 설정된 규칙에 따라 유효성 검사가 이루어지게 됩니다.

 

이때 Global Pipes는 모든 라우트에 대해서 일관된 유효성 검사 및 변환이 이루어지도록 하며, class-validator의 데코레이터를 사용하면 유효성 검사 규칙을 굉장히 쉽게 정의할 수 있습니다.

 

2-3. Performance Optimization (성능 최적화)

그러나 모든 라우트가 똑같은 수준의 유효성 검증이 필요한 것은 아니기 때문에 모든 수신 요청에 대해 전역 파이프를 적용하게 되면 모든 요청마다 매번 같은 유효성 검사와 변환 로직이 수행되어 간단한 유효성 검사만 필요한 라우트나 유효성 검사가 아예 불필요한 라우트에게는 잠재적인 오버헤드를 유발되고 이는 성능에 까지 영향을 끼치게 되는 요소가 됩니다.

 

따라서 class-validator는 유효성 검사 규칙을 정의하는 편리한 방법을 제공하지만, 모든 경로에 복잡한 유효성 검사 로직을 과도하게 사용하면 응답 시간에 직접적인 영향을 끼칠 수 있기 때문에 개발자는 유효성 검사 규칙이 그 검증이 반드시 필요한 라우트에 대해서만 적용하도록 신중히 고려해야 하고 특정 use-case에 대한 집중적인 유효성 검사 전략을 따로 구현하는 것이 권장됩니다.

 

2-4. Custom Validaion (사용자 정의 유효성 검사)

특정 경로에 고유한 유효성 검사 요구 사항이 있는 경우, 글로벌 파이프를 사용하는 것보다 custom validation을 사용하는 것이 좋습니다. 그렇기 때문에 전역 유효성 검증과 각각의 use-cases에 대한 custom validation의 균형을 맞추는 것은 매우 중요한 과제라고 할 수 있습니다.

 

2-5. 어떻게 균형을 맞추지?

애플리케이션의 특성과 여러 trade-off를 고려한 장단점을 생각해 봐야합니다.

 

중요한 라우트나 복잡한 유효성 검증이 필요한 라우트의 경우에는 전역 파이프와 class-validator를 사용하는 것이 매우 유용하지만, 덜 중요한 라우트나 특정 라우트만의 고유한 유효성 검증 로직이 필요한 라우트의 경우, 보다 더 localized(custom)된 접근 방식이 적합할 것입니다.

 

3. Serialization (직렬화)

직렬화(serialization)는 네트워크 응답에서 객체가 반환되기 전에 발생하는 프로세스입니다.

 

클라이언트에게 반환할 데이터를 변환하고 필터링하기 위한 규칙을 제공하는 데 사용되는 기법입니다.

  • 예를 들어 비밀번호와 같은 민감한 데이터는 항상 응답에서 제외되어야 할 것입니다.
  • 또는 Entity 속성의 하위 집합(DTO)만 전송한다거나 특정 속성을 추가로 변환할 수도 있습니다.

 

만약 이러한 변환을 개발자가 수동으로 진행한다면 지루하기도 하고, 아무래도 사람이 직접 하다보면 오류가 발생하기 쉬우며, 모든 경우를 각각 잘 처리했는지 확인하기도 쉽지 않습니다.

 

3-1. 개요

Nest는 이러한 작업을 간단한 방법으로 수행할 수 있도록 지원하는 내장 기능을 제공합니다. 그것은 바로 강력한 class-transformer 패키지를 사용하여 선언적이고 확장 가능한 방식으로 객체를 변환하는 ClassSerializerInterceptor 인터셉터입니다.

 

이 인터셉터가 수행하는 기본 작업은 메서드 핸들러가 반환한 값을 가져와 class-transformer의 instanceToPlain() 함수를 적용하는 것입니다.

 

이는 아래 설명된 대로 entity 혹은 DTO 클래스에 class-transformer의 데코레이터로 표현된 규칙을 적용할 수 있습니다.

 

3-2. Exclude 속성 (제외 속성)

User entity에서 password 속성을 자동으로 제외하고 싶다고 가정해 보겠습니다. 그러면 entity 클래스에서 해당 속성에 다음과 같은 @Exclude() 데코레이터를 추가합니다:

import { Exclude } from 'class-transformer';

export class UserEntity {
  id: number;
  firstName: string;
  lastName: string;

  @Exclude()
  password: string;

  constructor(partial: Partial<UserEntity>) {
    Object.assign(this, partial);
  }
}

 

이제 이 클래스의 인스턴스를 반환하는 메서드 핸들러를 가진 컨트롤러를 생각해 봅시다.

 

@UseInterceptors(ClassSerializerInterceptor)
@Get()
findOne(): UserEntity {
  return new UserEntity({
    id: 1,
    firstName: 'Kamil',
    lastName: 'Mysliwiec',
    password: 'password', // 비밀번호를 포함하여 응답
  });
}

 

WARNING
이때 클래스의 인스턴스를 반환해야 한다는 점에 유의하세요. 예를 들어 { user: new UserEntity() }와 같이 일반 JavaScript 객체를 반환하게 되면 객체가 제대로 직렬화되지 않습니다.

 

이 엔드포인트로의 요청에 대해 클라이언트는 다음과 같은 응답을 받게될 것입니다:

{
  "id": 1,
  "firstName": "Kamil",
  "lastName": "Mysliwiec"
}

 

분명 컨트롤러에서 우리는 password를 포함시켜 응답을 했음에도 클라이언트가 받는 respnose에는 해당 속성은 빠져있는 것을 확인할 수 있습니다.

 

pipe와 마찬가지로 interceptor역시 애플리케이션 전역에서 적용될 수도 있습니다. 전역으로 설정함으로써 interceptor와 entity 클래스 선언의 조합으로 UserEntity를 반환하는 모든 메서드로부터 비밀번호 속성을 제거하는 작업이 자동화할 수 있습니다. 이는 이러한 비지니스 규칙들을 중앙 집중식으로 시행할 수 있게 해줍니다.

 

3-3. Expose 속성 (노출 속성)

@Expose() 데코레이터를 사용하여 별칭(alias) 이름을 제공하거나, 아래와 같이 속성의 특정 속성 값을 계산하는 함수(getter 함수와 유사)를 실행한 값을 노출시킬 수 있습니다.

@Expose()
get fullName(): string {
  return `${this.firstName} ${this.lastName}`;
}

 

 

3-4. Transform (변환)

@Transform() 데코레이터를 사용하여 추가적인 데이터 변환을 수행할 수 있습니다. 예를 들어, 다음 role 속성은 RoleEntity 객체 전체를 반환하는 대신 RoleEntity에서 name 속성만을 반환합니다.

 

@Transform(({ value }) => value.name)
role: RoleEntity;

 

3-5. Pass options

변환 함수의 default 동작을 개발자가 원하는대로 수정하고 싶은 경우에는 어떻게 할까요?

 

default 설정을 재정의하려면 @SerializeOptions() 데코레이터를 사용하여 옵션 객체에 원하는 설정을 전달합니다.

@SerializeOptions({
  excludePrefixes: ['_'],
})
@Get()
findOne(): UserEntity {
  return new UserEntity();
}

 

SerializeOptions()를 통해 전달된 옵션은 기본 instanceToPlain() 함수의 두 번째 인자로 전달됩니다. 이 예제에서는 _ 접두사로 시작하는 모든 프로퍼티를 자동으로 제외시켜주는 기능을 합니다.

 


 

이렇게 Validation과 Transfomation을 어떻게 할 수 있는지에 대해서 알아보았습니다. Validation은 Pipe를 통해 기능을 구현할 수 있고 Transformation은 Interceptor를 통해 구현할 수 있었습니다. 물론 이 이면에는 class-validator와 class-transformer 같은 강력한 라이브러리가 있었기 때문에 더욱 편하게 검증과 변환을 할 수 있었습니다.

 

감사합니다.

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.