NestJS DTO & PIPE

DTO (Data Transfer Object)

DTO는 계층 간 데이터 교환을 위한 객체이다.
DTO를 이해하기 쉽게 도표로 나타냈다.

NestJS에서 DTO는 주로 Client로 받은 Plain JSON을 서버에서 정의한 클래스의 인스턴스로 변환할 때 사용하는 객체다.
DTO 특징은 인스턴스 생성을 통해 캡슐화되는 것, DTO 내 PIPE를 통해 Transformer, Validation을 구현할 수 있는 점이다.


DTO를 좀 더 쉽게 얘기하면 날 것의 데이터는 자유도가 너무 높기 때문에 사용하기 힘들다. 어디서 문제가 생길지 모르기 떄문이다.
그래서 내 입맛에 맞게 어느 정도 제한을 줄 수 있는 데이터로 바꿔주는 것이다.
날 것의 농산물(Plain JSON)을 조리가 가능한 재료(DTO)로 검수(plainToInstance/역직렬화) 하는 과정을 떠올리면 된다. 요리사는 검수 과정에서 원하는 모양으로 당근을 자를 수도 있고(class-transformer) 요리에 적합하지 않은 크기나 상태는 반려(class-validator) 할 수 있다.

DTO & PIPE 구현 (with ParameterDecorator)

NestJS에서 ParameterDecorator(@Body, @Param, @Query 등)를 사용해서 DTO 및 PIPE 구현하는 것은 쉽다. DTO를 작성하고 ParameterDecorator 데코레이터만 잘 써주면 똑똑한 NestJS가 다 알아서 해주기 때문이다.
아래는 Query를 콘트롤러 파라미터에서 받으며 DTO 및 PIPE를 구현하는 코드이다.

// hello.dto.ts
import { Type, Transform } from 'class-transformer'
import { IsString, IsOptional, IsInt } from 'class-validator'

export class queryDTO {
  @IsString() // 3
  name: string

  @Type(() => Number) // 1
  @IsInt() // 4
  age: number

  @Transform(({ value }) => {
    if (value === 'true') return true
    else if (value === 'false') return false
    return
  }) // 2
  @IsBoolean() // 5
  @IsOptional()
  is_active?: boolean
}

// hello.controller.ts
import { Get, Query } from '@nestjs/common'

// ...
@Get('/user')
getUser(@Query() query: queryDTO) {
  // ...
}

여기서 알아두면 좋은 것은 실행되는 순서다.
Pipe는 모든 칼럼의 transformer가 끝나고 validator가 실행된다.
만약 transformer 과정에서 에러가 발생한다면 validator는 실행되지 않은 상태에서 에러가 발생한다는 것이다.


DTO & PIPE 수동 구현

DTO & PIPE를 수동으로 구현해 줘야 하는 경우도 있다.
한 가지 예를 들면 엑셀 등의 파일에서 데이터를 읽고 가공한 데이터를 DTO로 만들어야 하는 경우다.

즉, 위 예시에서 @Query() 데코레이터가 알아서 해줬던 기능을 직접 구현해 줘야 한다.
핵심 코드는 아래와 같다.

import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";

// dto Mapping & transformer
const transformedQuery: queryDTO[] = plainToInstance(queryDTO, query, {
    excludeExtraneousValues: true,
});

// validator
const validationErrors = await validate(transformedQuery);

plainToInstance 함수는 1번째 인수 클래스로 인스턴스를 생성하고 2번째 인수에 들어온 데이터를 매핑 시킨다. 그리고 클래스에 정의된 transform 데코레이터를 실행한다.

validate 함수는 이미 인스턴스화가 된 객체를 인수로 받으며 validation을 실행한다. 유효성 검사가 통과되지 않은 항목이 있을 때 에러를 값으로 리턴해준다.


마무리

솔직히 NestJS를 사용하면 DTO나 PIPE 등 원리를 잘 몰라도 구현하는데 큰 어려움이 없다.
하지만 최근 엑셀 업로드 기능을 작업하면서 수동으로 DTO 매핑과 PIPE 구현이 필요했고 원리도 좀 더 깊게 이해할 수 있었다.

그동안 DTO 정의가 추상적으로만 느껴졌었는데 이번에 제대로 이해할 수 있었다.

어떤 개념에 대해 잘 모르면 추상적으로 느껴지고 누군가에게 설명하기 어렵다.
반면 쉬운 표현으로 아이에게도 설명할 수 있으면 그 개념을 잘 안다고 볼 수 있다.
쉬운 말로 아이에게 설명이 가능할 때까지 이해하려고 노력하자!

댓글남기기