본문으로 바로가기

DTO?

DTO가 무엇인지에 대해서는 다음 SO 게시물을 확인해봅시다.

 

stackoverflow.com/questions/1051182/what-is-data-transfer-object

 

What is Data Transfer Object?

What is a Data Transfer Object? In MVC are the model classes DTO, and if not what are the differences and do we need both?

stackoverflow.com

 

nest 공식 문서에서 언급된 바를 가져오자면 

 

(if you use TypeScript), we need to determine the DTO (Data Transfer Object) schema. A DTO is an object that defines how the data will be sent over the network. We could determine the DTO schema by using TypeScript interfaces, or by simple classes. Interestingly, we recommend using classes here. Why? Classes are part of the JavaScript ES6 standard, and therefore they are preserved as real entities in the compiled JavaScript. On the other hand, since TypeScript interfaces are removed during the transpilation, Nest can't refer to them at runtime. This is important because features such as Pipes enable additional possibilities when they have access to the metatype of the variable at runtime. - 출처

 

쉽게 말해서 정보를 교환하는 데 있어 타이핑을 체크하기 위한 스키마 정도로 이해하시면 될 것 같습니다.

DTO의 실사용례는 다음과 같습니다.

 

아래와 같은 controller 관련 코드가 작성되었다고 가정합시다.

import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
import { Movie } from './entities/movie.entity';
import { MoviesService } from './movies.service';

@Controller('movies')
export class MoviesController {
  constructor(private readonly moviesService: MoviesService) {}
  
  @Post()
  create(@Body() movieData) {
    return this.moviesService.create(movieData);
  }

  @Patch('/:id')
  patch(@Param('id') movieId: string, @Body() updateData) {
    console.log(updateData);
    console.log(movieId);
    return this.moviesService.update(movieId, updateData);
  }
}

 

그런데 여기에서 PATCH /:id 부분의 updateData와 POST / 부분의 moiveData는 controller 단을 거쳐 클라이언트로부터 받는 값입니다. 따라서 받는 값을 Validation 즉, 유효성 검사를 할 필요가 있습니다.

 

따라서 타입을 체크하기 위한 DTO를 만들어 준 후 (interface로 만들어도 되고 class로 만들어도 됩니다.)

export class CreateMovieDTO {
  readonly title: string;
  readonly year: number;
  readonly genres: string[];
}

 

타입 지정을 하면 됩니다. 타입이 필요한 Service 등의 부분에도 이렇게 넣으면됩니다.

여기까지야 일반 TS와 다를게 없습니다.

import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
import { CreateMovieDTO } from './dto/create-movie.dto';
import { Movie } from './entities/movie.entity';
import { MoviesService } from './movies.service';

@Controller('movies')
export class MoviesController {
  constructor(private readonly moviesService: MoviesService) {}

  ... (중략)
  
  @Post()
  create(@Body() movieData: CreateMovieDTO) {
    return this.moviesService.create(movieData);
  }

  @Patch('/:id')
  patch(@Param('id') movieId: string, @Body() updateData: CreateMovieDTO) {
    console.log(updateData);
    console.log(movieId);
    return this.moviesService.update(movieId, updateData);
  }
}

 

 

여기까지는 단순히 값을 검증하는 것에 불과하고, 잘못된 타입이나 값을 넣었을 경우 어떠한 제지도 하지 않습니다.

따라서 더 해야 할 것은, 값을 검증함과 동시에 적절한 처리를 해주는, 일종의 미들웨어를 설정하는 것입니다.

 

 

Pipe를 이용한 Class Validator

 

docs.nestjs.com/pipes

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

A pipe is a class annotated with the @Injectable() decorator. Pipes should implement the PipeTransform interface.

 

Pipes have two typical use cases:

  • transformation: transform input data to the desired form (e.g., from string to integer)
  • validation: evaluate input data and if valid, simply pass it through unchanged; otherwise, throw an exception when the data is incorrect

In both cases, pipes operate on the arguments being processed by a controller route handler. Nest interposes a pipe just before a method is invoked, and the pipe receives the arguments destined for the method and operates on them. Any transformation or validation operation takes place at that time, after which the route handler is invoked with any (potentially) transformed arguments.

 

Nest comes with a number of built-in pipes that you can use out-of-the-box. You can also build your own custom pipes.

 

 

아래 그림은, client 측에서 자체 filter로 validation을 하는 경우고, pipe는 nest가 해당 정보를 걸러내는 것을 표현한 것입니다.

 

 

 

Built-in pipes

 

nest가 기본적으로 가지고 있는 빌트인 파이프는 다음과 같은 것들이 있습니다.

 

Nest comes with six pipes available out-of-the-box:

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe

They're exported from the @nestjs/common package.

 

우선 pipe에 대한 자세한 내용은 추후에 다뤄보도록하고, 여기에서는 우선 pipe를 이용해 class validator를 만드는데 집중하겠습니다. 이 부분에 대해서는 아래 공식 문서에 정리가 되어 있습니다.

 

docs.nestjs.com/pipes#class-validator

github.com/typestack/class-validator

 

 

우선 검증을 위한 도구를 설치합니다. TS의 단짝친구인 class-validator와 class-transformer를 설치합니다.

npm i --save class-validator class-transformer

 

DTO에 대해서 해당 타입 유효성 검사를 위해 class-validator를 다음과 같이 이용합니다.

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

export class CreateMovieDTO {
  @IsString()
  readonly title: string;

  @IsNumber()
  readonly year: number;

  @IsString({ each: true })
  readonly genres: string[];
}

 

 

그 다음으로 해당 백엔드의 전역에서 유효성 검사가 이루어지도록 전역 범위 파이프를 설정합니다. 모든 코드를 총괄하는 main.ts에 다음과 같이 설정하면 됩니다.

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 전역 범위 파이프
  app.useGlobalPipes(new ValidationPipe({
  	whitelist: true, // validation을 위한 decorator가 붙어있지 않은 속성들은 제거
    forbidNonWhitelisted: true, // whitelist 설정을 켜서 걸러질 속성이 있다면 아예 요청 자체를 막도록 (400 에러)
    transform: true, // 요청에서 넘어온 자료들의 형변환
  }));
  
  await app.listen(5000);
}
bootstrap();

 

그리고 검증하기 위한 type을 controller 단에 붙여주면, 유저로부터 받는 요청을 검증할 수 있습니다.

@Post()
create(@Body() movieData: CreateMovieDTO) {
  return this.movieService.create(movieData);
}

@Patch('/:id')
update(@Param('id') movieId: number, @Body() updateData: UpdateMovieDTO) {
  return this.movieService.update(movieId, updateData);
}

 

 

mapped-types

 

 

추가적으로 mapped-types를 이용하여 다음과 같은 DTO를 간단히 조작할 수 있습니다.

예를 들어 위에서 작성한 CreateMovieDTO를 조금 수정해서 UpdateMovieDTO를 만든다고 가정한다면 조금 낭비적인 부분을 볼 수 있습니다. 단순히 모든 속성에 ?를 붙인 것이기 때문입니다.

export class CreateMovieDTO {
  @IsString()
  readonly title: string;

  @IsNumber()
  readonly year: number;

  @IsString({ each: true })
  readonly genres: string[];
}

// property에 ?만 붙임
export class UpdateMovieDTO {
  @IsString()
  readonly title?: string;

  @IsNumber()
  readonly year?: number;

  @IsString({ each: true })
  readonly genres?: string[];
}

 

이럴때는 mapped-types에서 제공하는 PartialType으로 덧씌워주면 됩니다.

mapped-types에 대해서는 다음 github를 참고합시다. github.com/nestjs/mapped-types

npm i @nestjs/mapped-types

 

 

이용 전

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

export class UpdateMovieDTO {
  @IsString()
  readonly title?: string;

  @IsNumber()
  readonly year?: number;

  @IsString({ each: true })
  readonly genres?: string[];
}

 

이용 후 간단히

import { IsString, IsNumber } from 'class-validator';
import { PartialType } from '@nestjs/mapped-types';
import { CreateMovieDTO } from './create-movie.dto';

export class UpdateMovieDTO extends PartialType(CreateMovieDTO) {}

 

자동으로 extends한 DTO의 속성을 모두 선택적으로 만들 수 있습니다. nest가 이래서 편합니다.

 

 

이후 추가적인 Validation 속성들

 

추가적으로 Validation에 있어 부가적인 기능을 사용할 수 있습니다. 아래 공식 문서의 techniques 부분을 살펴보면 더욱 자세하게 나옵니다.

 

 

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

 

다음과 같이 사용할 수 있습니다.

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // decorator(@)가 없는 속성이 들어오면 해당 속성은 제거하고 받아들입니다.
      forbidNonWhitelisted: true, // DTO에 정의되지 않은 값이 넘어오면 request 자체를 막습니다.
      transform: true, // 클라이언트에서 값을 받자마자 타입을 정의한대로 자동 형변환을 합니다.
    }),
  );
  await app.listen(5000);
}
bootstrap();

 

정의되지 않은 property를 전송하려고 하면 다음과 같이 400 에러가 뜹니다.

{
    "statusCode": 400,
    "message": [
        "property hack should not exist",
        "property this should not exist"
    ],
    "error": "Bad Request"
}

 

 

추가적인 속성으로 다음과 같은 것들이 있습니다.

 

skipMissingProperties boolean If set to true, validator will skip validation of all properties that are missing in the validating object.
whitelist boolean If set to true, validator will strip validated (returned) object of any properties that do not use any validation decorators.
forbidNonWhitelisted boolean If set to true, instead of stripping non-whitelisted properties validator will throw an exception.
forbidUnknownValues boolean If set to true, attempts to validate unknown objects fail immediately.
disableErrorMessages boolean If set to true, validation errors will not be returned to the client.
errorHttpStatusCode number This setting allows you to specify which exception type will be used in case of an error. By default it throws BadRequestException.
exceptionFactory Function Takes an array of the validation errors and returns an exception object to be thrown.
groups string[] Groups to be used during validation of the object.
dismissDefaultMessages boolean If set to true, the validation will not use default messages. Error message always will be undefined if its not explicitly set.
validationError.target boolean Indicates if target should be exposed in ValidationError
validationError.value boolean Indicates if validated value should be exposed in ValidationError.

darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체