NestJS DTO & Pipe

류지승·2024년 9월 18일
0

nestjs

목록 보기
5/10
post-thumbnail

DTO

DTO(Data Transfer Object)는 계층간 데이터 교환을 위한 객체이다. DB에서 데이터를 얻어 Service나 Controller 등으로 보낼 때 사용하는 객체이다. Interface와 Class 둘 다 사용가능하지만, Nest에서는 Class를 사용하는 걸 추천하고 있다.

DTO를 쓰는 이유가 뭘까?

  • 데이터 유효성을 체크하는 데 효율적이기 때문
  • 안정적인 코드를 만들어준다. 타입스크립트의 type으로도 이용 가능하기 때문

Property들을 여러 곳에서 사용하고 있다. 현재는 간단한 애플리케이션이기 때문에, 몇 군데에서 불러주기만 하면 되는데, 만약 property 수가 많아지고, 불러내는 곳 또한 많아지면, 유지보수하기 힘들다. 만약 프로퍼티가 변경되었다고 하면 모든 프로퍼티를 변경해야하므로 비효율적이다 따라서 우리는 이를 해결하기 위해 DTO를 사용한다.

DTO 생성

DTO 네이밍 규칙은 동사-명사.dto.ts로 이루어져 있다.

DTO 정의시 interface보단 class를 사용하는 이유

  • interface같은 경우 ts compile 단계에서 처리되기 때문에, 런타임에는 존재하지 않는다. class는 런타임 단계에도 존재하므로 Nest의 여러 기능(pipes, decorates)를 사용할 수 있다.
  • 데코레이터를 사용할 수 있다. 이는 interface에서는 사용하지 못하며, 간편하게 속성의 타입과 유효성 검사를 할 수 있다.
  • swagger를 자동 문서화할 때 class로 정의하면 쉽게 통합할 수 있다.

많이 쓰이는 DTO Decorator

Decorator 정리

class-validator Decorator in Basic & Type

데코레이터설명예시
@IsString()값이 문자열인지 확인@IsString() name: string;
@IsNumber()값이 숫자인지 확인@IsNumber() age: number;
@IsInt()값이 정수인지 확인@IsInt() count: number;
@IsBoolean()값이 불리언인지 확인@IsBoolean() isActive: boolean;
@IsOptional()해당 속성이 선택사항임을 나타냄@IsOptional() @IsString() nickname?: string;
@IsNotEmpty()값이 비어 있지 않은지 확인@IsNotEmpty() @IsString() title: string;
@IsEmail()값이 이메일 형식인지 확인@IsEmail() email: string;
@IsDateString()값이 ISO 8601 형식의 날짜 문자열인지 확인@IsDateString() startDate: string;
@IsArray()값이 배열인지 확인@IsArray() @IsString({ each: true }) tags: string[];
@IsDefined()값이 정의되어 있는지 확인@IsDefined() value: any;
@Equals(value)값이 특정 값과 동일한지 확인@Equals('ADMIN') role: string;
@NotEquals(value)값이 특정 값과 동일하지 않은지 확인@NotEquals('GUEST') role: string;
@IsEmpty()값이 비어 있는지 확인@IsEmpty() value: any;
@IsIn(values: any[])값이 지정된 배열 안에 있는지 확인@IsIn(['ADMIN', 'USER']) role: string;
@IsNotIn(values: any[])값이 지정된 배열 안에 없는지 확인@IsNotIn(['BANNED', 'RESTRICTED']) status: string;

헷갈릴 수 있는 Class Validator Decorator
@IsDefined() vs @ IsNotEmpty()

  • IsDefined : 값이 undefined가 아니여야한다. null, "", 0, false은 통과가 된다는 얘기다
  • IsNotEmpty : 값이 undefined, null, ""가 아니여야 한다. 0, false 는 통과가 된다는 얘기다

@IsEnum() vs @IsIn()

  • IsEnum : 값이 Typescript의 Enum에 포함된 값인지 확인한다. Enum Type을 명확하게 지정하여 해당 Enum의 정의된 값만 허용한다. 주로 특정 Enum으로 정적 값을 제한하고 싶을 때 사용한다.
import { IsEnum } from 'class-validator';

enum EStatus {
  ACTIVE = 'active',
  INACTIVE = 'inactive',
  PENDING = 'pending',
}

class ExampleDto {
  @IsEnum(EStatus)
  status: EStatus;
}

IsIn : 값이 지정된 배열 안에 포함되는 지를 확인한다. Enum이 아닌 일반 배열을 사용하여 검증 대상을 동적으로 지정할 수 있다. 허용 가능한 값의 목록을 동적으로 생성되거나 코드 외부에서 주어지는 경우에 사용한다.

import { IsIn } from 'class-validator';

class ExampleDto {
  @IsIn(['active', 'inactive', 'pending'])
  status: string;
}

class-validator Decorator in Number & character

데코레이터설명예시
@IsDivisibleBy(number)값이 특정 숫자로 나누어떨어지는지 확인@IsDivisibleBy(5) count: number;
@IsNegative()값이 음수인지 확인@IsNegative() balance: number;
@IsPositive()값이 양수인지 확인@IsPositive() amount: number;
@Min(value)숫자가 최소값 이상인지 확인@Min(10) age: number;
@Max(value)숫자가 최대값 이하인지 확인@Max(100) score: number;
@Contains(value)문자열에 특정 값이 포함되어 있는지 확인@Contains('example') url: string;
@NotContains(value)문자열에 특정 값이 포함되어 있지 않은지 확인@NotContains('spam') comment: string;
@IsAlphanumeric()값이 알파벳과 숫자로만 이루어져 있는지 확인@IsAlphanumeric() username: string;
@IsCreditCard()값이 유효한 신용카드 번호인지 확인@IsCreditCard() cardNumber: string;
@IsHexColor()값이 유효한 16진수 색상 코드인지 확인@IsHexColor() color: string;
@MaxLength(value)문자열의 길이가 최대값 이하인지 확인@MaxLength(50) description: string;
@MinLength(value)문자열의 길이가 최소값 이상인지 확인@MinLength(5) password: string;
@Length(min, max)문자열 길이가 지정된 범위 내에 있는지 확인@Length(5, 20) username: string;
@Matches(regex)값이 지정된 정규 표현식과 일치하는지 확인@Matches(/^[A-Za-z0-9]+$/) username: string;
@IsUUID(version?)값이 유효한 UUID 형식인지 확인 (버전은 선택적으로 지정 가능)@IsUUID('4') id: string;
@IsLatLong()값이 latitude,longitude 형식의 문자열인지 확인@IsLatLong() location: string;

Pipe class validator에서 에러 발생 시 반환 에러 구조

// 실제로 반환 에러로 던져주는 게 아닌 검증 실패 시 
// ValidationError 객체가 반환해주는 에러메세지
// 실질적으로 client 측으로 에러 메세지를 넘겨주는 건 
// ValidationPipe에서 간소하되게 처리함.
{
  target: Object; // 검증한 객체
  property: string; // 검증 실패한 프로퍼티
  value: any; // 검증 실패한 값
  constraints?: { // 검증 실패한 제약 조건
  	[type: string]: string;
  }
  children?: ValidationError[]; // 프로퍼티의 모든 검증 실패 제약 조건
}

class-transformer Decorator

데코레이터설명예시
@Type(() => Type)지정한 타입으로 변환@Type(() => AddressDto) address: AddressDto;
@Transform()사용자 정의 변환 로직 적용@Transform(({ value }) => parseInt(value)) @IsInt() price: number;
@Exclude()변환 시 해당 속성을 제외@Exclude() password: string;
@Expose()기본적으로 숨겨진 필드를 명시적으로 노출@Expose() firstName: string;

Pipe

Pipe는 @injectable() 데코레이터로 주석이 달린 클래스다. 파이프는 data transformation과 data validation을 위해서 사용된다. 파이프는 컨트롤러 경로 처리기에 의해 처리되는 이수에 대해 작동한다. nest는 메서드가 호출되기 직전에 파이프를 삽입하고 파이프는 메서드로 향하는 인수를 수신하고 이에 대해 작동한다.

Data Transformation & Data Validation
Data Transformation - 입력 데이터를 원하는 형식으로 변환(문자열 -> 정수)
Data Validation - 입력 데이터를 평가하고 유효한 경우 변경되지 않은 상태로 전달 만약 입력 데이터가 올바르지 않는다면 error를 발생시킨다.

Pipe를 사용하는 방법

파이프를 사용하는 방법은 총 세 가지로 나눠질 수 있다.
Handler-level Pipes, Parameter-level Pipes, Global-level Pipes

Parameter-level Pipes
특정 파라미터에서만 적용되는 파이프
Handler-level Pipes
controller에서 한 handler만 적용되는 파이프 주로 @UsePipes(ValidationPipe)를 이용하여 파이프 처리함.
Global-level Pipes
global 전역에서 모두 적용되는 파이프 주로, main.ts에서 app.useGlobalPipes(new Validation())을 적용시켜줌

bulit-in Pipe

  • ValidationPipe - 모든 Validation Decorator가 적용되도록 해준다.
  • ParseIntPipe - Int 값으로 변환 검증한다.(@Params string -> number)
  • ParseFloatPipe - Float 값으로 변환 검증한다.
  • ParseBoolPipe - Bool 값으로 변환 검증한다.
  • ParseArrayPipe - Array 값으로 검증한다.
  • ParseUUIDPipe - UUID 값으로 검증한다.
  • ParseEnumPipe - Enum 값인 지 검증한다.
  • DefaultValuePipe - Default를 설정한다. (pagingation할 때 skip 부분 default 처리할 때 사용됨)
  • ParseFilePipe - File을 검증하지만, 실질적으로 Multer로 처리한다.

Custom Pipe

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
	if(value.length <= 2)
      throw new BadRequestException('영화의 제목은 3자 이상 작성해주세요')
  }
  return value;
}

export interface ArgumentMetadata {
    /**
     * Indicates whether argument is a body, query, param, or custom parameter
     */
    readonly type: Paramtype;
    /**
     * Underlying base type (e.g., `String`) of the parameter, based on the type
     * definition in the route handler.
     */
    readonly metatype?: Type<any> | undefined;
    /**
     * String passed as an argument to the decorator.
     * Example: `@Body('userId')` would yield `userId`
     */
    readonly data?: string | undefined;
}

ValidationPipe Options

app.useGlobalPipe(new ValidationPipe({
  // default false
  // 정의하지 않는 값들은 아에 들어가지 않음. 
  // 예를 들어서 dto property로 name과 phone 정의
  // request body로 name / phone / address을 전달했다면
  // 자동으로 정의되지 않은 값 즉 address를 받지 않고 
  // 정의된 name과 phone만 받는다.
  whitelist: true
  
  // default: false
  // 정의되지 않는 값이 들어오면 에러를 반환함
  // 예를 들어서 dto property로 name과 phone 정의
  // request body로 name / phone / address을 전달했다면
  // property address 에러 처리한다.
  forbidNonWhitelisted: true
}))
옵션설명기본값자주 사용 여부
whitelistDTO에 정의되지 않은 속성을 요청 데이터에서 자동으로 제거.false매우 자주 사용
forbidNonWhitelistedDTO에 정의되지 않은 속성이 들어오면 에러를 반환.false자주 사용
transform요청 데이터를 DTO 타입으로 자동 변환 (class-transformer 기반).false매우 자주 사용
transformOptions데이터 변환에 사용할 class-transformer 옵션 (예: enableImplicitConversion: true).undefined사용 상황에 따라
disableErrorMessages검증 실패 시 에러 메시지를 반환하지 않음.false보안 요구에 따라 사용
exceptionFactory검증 실패 시 에러 메시지를 사용자 정의 방식으로 처리 (예: 커스텀 에러 객체 반환).undefined커스터마이징 시 사용
stopAtFirstError검증 실패 시 첫 번째 에러에서 바로 종료.false성능 최적화 시 유용
validateCustomDecorators커스텀 데코레이터를 ValidationPipe에서 검증 가능하도록 활성화.false커스텀 데코레이터 사용 시
skipMissingProperties요청 데이터에서 누락된 필드를 무시 (해당 필드가 DTO에 정의되어 있어도).false상황에 따라 사용
forbidUnknownValuesDTO 외부 값이나 알 수 없는 객체가 들어오면 에러를 반환.true자주 사용
validateFallback데이터 타입이 DTO로 인식되지 않을 때 기본 DTO로 검증 시도.false드물게 사용
개념주요 역할적용 위치주요 용도호출 시점
Pipe데이터 변환유효성 검사메서드 수준, 애플리케이션 수준요청 데이터를 검증하거나 변환할 때 사용컨트롤러 핸들러 직전
Guard인증 및 권한 부여메서드, 클래스, 애플리케이션 수준사용자가 요청할 수 있는지 판단컨트롤러 직전
Middleware요청 전/후 공통 로직 실행애플리케이션 수준, 경로 수준로깅, CORS 설정, 요청 파싱 등컨트롤러 및 핸들러 직전
Filter에러 처리응답 형식 통일메서드, 클래스, 애플리케이션 수준예외 발생 시 응답 커스터마이징예외 발생 시점
Interceptor요청 전/후 로직응답 가공메서드, 클래스, 애플리케이션 수준응답 데이터 포맷팅, 로깅, 캐싱, 에러 재포맷팅컨트롤러 핸들러 전후

Mapped Types

CRUD(생성/읽기/업데이트/삭제)와 같은 기능을 구축할 때는 기본 엔티티 유형에 변형을 구성하는 것이 종종 유용하다. Nest는 이 작업을 더 편리하게 하기 위해 유형 변환을 수행하는 여러 유틸리티 함수를 제공한다.

PartialType()

클래스의 프로퍼티 정의를 모두 optional로 만든다.

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

export class UpdateCatDto extends PartialType(CreateCatDto) {}
/*
{
  name?: string;
  email?: string;
  password?: string;
}
*/

PickType()

특정 프로퍼티만 골라 사용 할 수 있다. (Omit의 반대)

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

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

OmitType()

특정 프로퍼티만 생략 할 수 있다. (Pick의 반대)

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


export class UpdateCatDto extends OmitType(CreateCatDto, ['name'] as const) {}
/*
{
  age: number
  breed: string
}
*/

IntersectionType()

두 타입의 프로퍼티를 모두 모아서 사용 할 수 있다.


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

export class AdditionalCatInfo {
  color: string;
}


export class UpdateCatDto extends IntersectionType(
  CreateCatDto,
  AdditionalCatInfo,
) {}
/*
{
  name: string
  breed: string
  color: string
}
*/

Composition

Mapped Types를 다양하게 조합해서 중첩 적용 가능하다.

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

export class UpdateCatDto extends PartialType(
  OmitType(CreateCatDto, ['name'] as const),
) {}
/*
{
  age?: number
  brees?: string
}
*/
profile
성실(誠實)한 사람만이 목표를 성실(成實)한다

0개의 댓글

관련 채용 정보