개발이 취미인 사람

[Nest.js] Nest.js API 만들기 (3) - DTO(Data Transfer Object) & 유효성 검사(Validation Check) & Pipes 본문

백앤드(Back-End)/Nest.js

[Nest.js] Nest.js API 만들기 (3) - DTO(Data Transfer Object) & 유효성 검사(Validation Check) & Pipes

RyanSin 2021. 11. 10. 21:17
반응형

- 개요

안녕하세요. 이번 시간에는 DTO와 유효성 검사에 대해 알아보겠습니다.

 

지난 시간에 Nest.js Handler에 대해 알아봤습니다. 혹시 놓치고 오신 분들은 아래 링크를 통해 학습하고 오시는 걸 추천드리겠습니다.

[Nest.js] Nest.js API 만들기 (2) - Handler(@Get, @Post ...)

 

[Nest.js] Nest.js API 만들기 (2) - Handler(@Get, @Post ...)

- 개요 안녕하세요. 이번 시간에는 지난 시간에 이어서 RESTFul API를 만들어보는 시간을 가져보겠습니다. 지난 시간에는 Controller, Service, Provider에 대해 학습하고 실제 HTTP 요청을 통해 브라우저에

any-ting.tistory.com

- 개념

1. DTO (Data Transfer Object)

DTO(Data Transfer Object)란 무엇일까요? 단어 그대로 "데이터 옮기다 객체로"라고 단어 하나하나씩 해석할 수 있습니다.

 

즉, 클라이언트에서 서버로 데이터를 전송할 때 사용되며(Cliant -> Server), 계층(@Controller -> @Service) 간에 데이터를 전송할 때도 사용됩니다.

 

또한 제일 중요한 클라이언트로부터 전송받은 데이터를 객체로 변환할 때도 사용됩니다.

 

왜 굳이... 객체로 변환해야 할까요??... 하나씩 받아도 되고 크게 문제가 되지 않을 수 있다고 생각을 할 수도 있습니다.

 

클라이언트에서 전송하는 객체는 기본적으로 타입을 보장하지 않습니다. (Nest.js에서 네트워크 통신을 통해 받은 값은 JavaScript 객체)

 

그렇기 때문에 우리는 @Controller에서 데이터를 받기 전에 타입 검사와 유효성 검사를 통해 문제를 예방하는 것이 좋습니다.

 

예전 Express를 사용해서 개발을 하셨던 분들은 특정 라이브러리 또는 유효성 검사와 타입을 체크하는 로직을 실제로 구현하셨을 꺼라 생각이 듭니다.

 

하지만 Nest.js 프레임워크는 이러한 기능을 지원하기 때문에 개발 시 좀 더 간편하게 대응할 수 있습니다.

 

2. 유효성 검사 (Validation Check)

유효성 검사(Validation Check)는 크게 설명할 부분이 없다고 생각합니다. (개발자라면... 고된 일이기 때문이죠...)

 

- 기본 설정

@Controller에서 클라이언트로 객체를 전송받을 때 우리는 @Body 데이코레이터를 통해 값을 전달받았습니다.

 

하지만 어떠한 값이 어떤 타입을 가지고 전달받는지 우리는 알 수 없습니다.

 

그렇기 때문에 만약 DTO를 사용하지 않는다면 개발자가 @Body 데코레이터를 하나씩 모두 선언하는 방법을 선택해서 개발하게 됩니다.(노동에 소리가 느껴지네요...)

 

DTO 파일 생성

user 폴더 하위에 dto 폴더를 생성합니다. user.dto.ts 파일을 통해 우리가 사용할 DTO를 만들어줍니다.

 

DTO 파일

 

 user.dto.ts
/**
 * @description SRP를 위반하는 구조이지만 테스트용으로 한 파일에 두 클래스를 선언했다.
 *
 * SRP란: 한 클래스는 하나의 책임만 가져야한다. (단일 책임의 원칙)
 */
export class CreateUserDto {
  id: number; // 유저 고유 아이디
  name: string; // 유저 이름
}

export class UpdateUserDto {}

DTO는 우리가 전송받을 클래스 객체를 선언합니다. 유저를 생성하는 부분과 수정하는 부분으로 나눠서 클래스를 만들었습니다.

 

위와 같이 DTO를 선언해서 사용하면 전송 데이터 형식을 알 수 있습니다. (다른 개발자분들과 협업을 할 때 코드를 확인)

 

validation 모듈 설치
#npm
npm i class-validator class-transformer

#yarn
yarn add class-validator class-transformer

두 모듈을 통해 우리는 DTO 유효성 검사를 진행할 수 있습니다.

 

user.dto.ts 파일 수정
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { PartialType } from '@nestjs/mapped-types';

/**
 * @description SRP를 위반하는 구조이지만 테스트용으로 한 파일에 두 클래스를 선언했다.
 *
 * SRP란: 한 클래스는 하나의 책임만 가져야한다. (단일 책임의 원칙)
 */
export class CreateUserDto {
  @IsNumber()
  @IsNotEmpty()
  id: number; // 유저 고유 아이디

  @IsString()
  @IsNotEmpty()
  name: string; // 유저 이름
}

export class UpdateUserDto extends PartialType(CreateUserDto) {}

class-validator 모듈을 통해 클라이언트로부터 전송받은 객체 속성을 검사합니다.

 

- @IsNumber() -> 자료형은 정수형

- @IsNotEmpty() -> 전송된 데이터 값이 null 또는 undefined 라면 에러 발생

 

UpdateUserDto 속성은 CreateUserDto 클래스와 다르지 않기 때문에 해당 속성을 물려받습니다. (물려받기 위해 PartialType() 사용)

 

- Pipes 적용하기

위와 같이 기본 설정을 완료했다면 Nset.js에서 지원하는 Pipes를 사용해서 유효성 검사를 진행해보겠습니다.

 

Pipes란 무엇일까요? Express에서 미들웨어를 사용해 보신 분들이라면 이해가 금방 가실 겁니다.

 

Pipes 구조

클라이언트 요청이 @Controller에게 바로 전달되지 않고 Pipes를 한번 거친 다고 생각하시면 좋습니다.

 

Pipes 사용법

사용방법은 3가지 방법이 있습니다.

  1. Global-Level
  2. Handler-Level
  3. Parameter-Level

내장 파이프 #

Nest는 즉시 사용할 수 있는 8개의 파이프와 함께 제공됩니다.

  • ValidationPipe
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe

@nestjs/common패키지에서 내 보냅니다. (출처 : 공식 홈페이지)

 

1. Global-Level Pipe

Global Pipe는 애플리케이션 전체에 적용되는 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({
      /**
       * whitelist: DTO에 없은 속성은 무조건 거른다.
       * forbidNonWhitelisted: 전달하는 요청 값 중에 정의 되지 않은 값이 있으면 Error를 발생합니다.
       * transform: 네트워크를 통해 들어오는 데이터는 일반 JavaScript 객체입니다.
       *            객체를 자동으로 DTO로 변환을 원하면 transform 값을 true로 설정한다.
       * disableErrorMessages: Error가 발생 했을 때 Error Message를 표시 여부 설정(true: 표시하지 않음, false: 표시함)
       *                       배포 환경에서는 true로 설정하는 걸 추천합니다.
       */
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      disableErrorMessages: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();

 

옵션

공식홈페이지 : https://docs.nestjs.com/techniques/validation

Nest.js는 여러 가지 옵션을 지원합니다. 저는 제가 사용했던 옵션만 포스팅하겠습니다. 

- whitelist 옵션

해당 옵션은 사용자가 전송한 데이터 중에 DTO 속성으로 정의되지 않은 속성은 제외시키는 옵션입니다.

(해당 옵션 값만 true로 설정 후 테스트 진행)

 

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({
      /**
       * whitelist: DTO에 없은 속성은 무조건 거른다.
       * forbidNonWhitelisted: 전달하는 요청 값 중에 정의 되지 않은 값이 있으면 Error를 발생합니다.
       * transform: 네트워크를 통해 들어오는 데이터는 일반 JavaScript 객체입니다.
       *            객체를 자동으로 DTO로 변환을 원하면 transform 값을 true로 설정한다.
       * disableErrorMessages: Error가 발생 했을 때 Error Message를 표시 여부 설정(true: 표시하지 않음, false: 표시함)
       *                       배포 환경에서는 true로 설정하는 걸 추천합니다.
       */
      whitelist: true,
      // forbidNonWhitelisted: true,
      // transform: true,
      // disableErrorMessages: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();

 

포스트맨 요청

 

요청 결과

 

실제 콘솔 로그를 통해 데이터를 확인하면 age 속성 값이 제외된 걸 알 수 있습니다.

 

- forbidNonWhitelisted 옵션

클라이언트가 전달하는 요청 값 중에 정의되지 않은 속성 값이 있다면 Error를 발생합니다.

 

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({
      /**
       * whitelist: DTO에 없은 속성은 무조건 거른다.
       * forbidNonWhitelisted: 전달하는 요청 값 중에 정의 되지 않은 값이 있으면 Error를 발생합니다.
       * transform: 네트워크를 통해 들어오는 데이터는 일반 JavaScript 객체입니다.
       *            객체를 자동으로 DTO로 변환을 원하면 transform 값을 true로 설정한다.
       * disableErrorMessages: Error가 발생 했을 때 Error Message를 표시 여부 설정(true: 표시하지 않음, false: 표시함)
       *                       배포 환경에서는 true로 설정하는 걸 추천합니다.
       */
      whitelist: true,
      forbidNonWhitelisted: true,
      // transform: true,
      // disableErrorMessages: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();

 

포스트맨 요청

age 속성은 정의되어 있지 않아 Error가 발생하는 걸 확인할 수 있습니다.

 

- transfrom 옵션

클라이언트가 전달하는 데이터는 기본적으로 JavaScript 객체입니다. 객체를 자동으로 DTO 객체로 변환할 때 사용합니다.

 

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({
      /**
       * whitelist: DTO에 없은 속성은 무조건 거른다.
       * forbidNonWhitelisted: 전달하는 요청 값 중에 정의 되지 않은 값이 있으면 Error를 발생합니다.
       * transform: 네트워크를 통해 들어오는 데이터는 일반 JavaScript 객체입니다.
       *            객체를 자동으로 DTO로 변환을 원하면 transform 값을 true로 설정한다.
       * disableErrorMessages: Error가 발생 했을 때 Error Message를 표시 여부 설정(true: 표시하지 않음, false: 표시함)
       *                       배포 환경에서는 true로 설정하는 걸 추천합니다.
       */
      whitelist: true,
      // forbidNonWhitelisted: true,
      transform: true,
      // disableErrorMessages: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();

 

포스트맨 요청

- transform 옵션 false

false 설정

- transform 옵션 true

true 설정

옵션을 활성화했기 때문에 전달받은 데이터가 DTO로 변환된 걸 확인할 수 있습니다.

 

- disableErrorMessages 옵션

서버에서 Error가 발생했을 때 Error 메시지를 노출한다는 건 공격자에게 많은 정보를 제공할 수 있습니다.

그렇기 때문에 배포 단계에는 해당 옵션을 false로 설정해서 배포 환경에서는 노출하지 않는 걸 추천합니다.

 

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({
      /**
       * whitelist: DTO에 없은 속성은 무조건 거른다.
       * forbidNonWhitelisted: 전달하는 요청 값 중에 정의 되지 않은 값이 있으면 Error를 발생합니다.
       * transform: 네트워크를 통해 들어오는 데이터는 일반 JavaScript 객체입니다.
       *            객체를 자동으로 DTO로 변환을 원하면 transform 값을 true로 설정한다.
       * disableErrorMessages: Error가 발생 했을 때 Error Message를 표시 여부 설정(true: 표시하지 않음, false: 표시함)
       *                       배포 환경에서는 true로 설정하는 걸 추천합니다.
       */
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      disableErrorMessages: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();

 

포스트맨 요청

2. Handler-level Pipes

핸들러 레벨 파이프는 @Controller 계층에서 @UsePipes() 데코레이터를 선언해 사용합니다.

(main.ts 파일 안에 Global Pipes 설정은 주석 처리합니다.)

 

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
  Put,
  Query,
  UsePipes,
  ValidationPipe,
 } from '@nestjs/common';
 import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
  
/**
 * @author Ryan
 * @description @Body 방식 - @Body 어노테이션 여러개를 통해 요청 객체를 접근할 수 있습니다.
 *
 * CreateUserDto를 사용해서 @Body 전달 방식을 변경합니다.
 *
 * @param id 유저 고유 아이디
 * @param name 유저 이름
 */
@Post('/create_user')
@UsePipes(ValidationPipe)
onCreateUser(@Body() createUserDto: CreateUserDto): User[] {
  return this.userService.onCreateUser(createUserDto);
}

우리가 생성한 CreateUserDto를 @Body 데코레이터와 연결 후 @UsePipes를 사용해서 등록합니다.

요청 결과

name 속성에 빈 값을 전송한다면  위와 같이 "name should not be empty" 빈 값을 전달받아 에러가 발생한 걸 알 수 있습니다.

 

2. Parameter-level Pipes

전달받은 모든 파라미터를 검사하지 않고 특정 파라미터만 검사하는 방식입니다.

 

  /**
   * @author Ryan
   * @description @Param & @Body 혼합 방식 - 단일 유저 수정
   *
   * @param id 유저 고유 아이디
   * @param name 유저 이름
   */
  @Patch('/user/:id')
  @UsePipes(ValidationPipe)
  setUser(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ): User {
    return this.userService.setUser(id, updateUserDto);
  }

@Query 데코레이터를 보면 두 번째 인자 값은 기본 값을 설정하는 부분입니다.

세 번째 인자 값은 자료형 타입을 정수형 타입이 아니면 에러를 발생합니다.

 

포스트맨 요청

정수형 데이터 타입이 아닌 문자열 타입으로 데이터를 전송했습니다. 자료형 타입을 잘못 지정해 Error가 발생한 걸 확인할 수 있습니다.

 

이번 시간에는 정말 많은걸 작성했는데요... 이상하거나 또는 틀린 부분이 있어 헷갈리시는 부분들은 댓글 남겨주시면 감사하겠습니다.

 

소스 저장소

github: https://github.com/Ryan-Sin/Node_Nest/tree/v3

 

GitHub - Ryan-Sin/Node_Nest

Contribute to Ryan-Sin/Node_Nest development by creating an account on GitHub.

github.com