본문 바로가기
서버 인프라, 백엔드/Nodejs, PM2

NestJS : Custom Validation 데코레이터로 유효성 검사 수행하기

by 번데기 개발자 2023. 12. 10.
반응형

 

NestJS를 개발하다 보면 들어오는 요청에 대해 유효성검사를 수행해야 할 때가 있습니다. NestJS에서는 class-validator, class-transformer가 구축된 ValidationPipe를 제공해 주는데 이를 통해 유효성 검사를 수행할 수 있습니다.

 

저는 개발 중에 기본적으로 제공하는 class-validator의 데코레이터가 아닌 커스텀한 Validation 데코레이터를 작성할 필요가 있었는데요, 기본적으로 class-validator가 많은 데코레이터를 제공해 주지만 원하는 데코레이터가 없을 때 어떻게 적용하는게 좋은지 정리해 보도록 하겠습니다.

 

NestJS의 유효성 검사

 

먼저 class-validator의 기본 데코레이터를 활용하여 유효성을 검사하는 적용하는 방법을 한번 알아보겠습니다. HTTP 요청에 대해서 Request Body에 대한 유효성을 검사할 때 아래와 같이 유효성을 검증할 수 있습니다.

 

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

export class SignUpDto {
  @IsNotEmpty()
  @IsEmail()
  email: string;
 
  @IsNotEmpty()
  @IsString()
  password: string;
}

 

위 예제는 회원가입을 처리하는 DTO인데요, @IsNotEmpty, @IsEmail 등의 데코레이터를 통해 원하는 유효성 검사에 대한 내용을 지정하면 유효성 검증에 실패했을 시, 기본적인 에러메세지와 함께 Client로 실패 메세지가 전송되게 됩니다.

 

만약 요청 시 Validation에서 유효성 검증이 성공했다면, Controller에서 @Body 데코레이터를 통해 값을 받아와서 다음 로직을 처리할 수 있습니다.

 

@Post()
async signUp(@Body() body: SignUpDto) {
  const { email, password } = body; // 컨트롤러에서 값을 전달받음
  ...
}

 

 

원하는 제약조건을 직접 만들어서 유효성 검사 수행하기 (@Validate 데코레이터 사용)

 

기본적인 데코레이터로도 대부분의 유효성 처리를 쉽게 처리할 수 있지만 실제로 개발을 하다 보면, 기본적으로 제공하는 Validation 데코레이터가 아닌 특수성이 있는 유효성 검증을 수행해야 할 때가 있습니다. 

 

먼저 이해를 위해서 예시를 한번 들어보겠습니다. 제가 만들고 싶은 유효성 검증 시나리오중에, 특정 field의 값이 n차원 배열이면서, 해당 배열의 값이 모두 number타입인 배열만 입력을 받을 수 있는 유효성 검증이 필요하였습니다. 하지만 위와 같은 유효성 검증을 하는 데코레이터는 class-validator에서 기본적으로 제공하지 않습니다.

 

이런 상황에서는 아래와 코드의 예제처럼 원하는 유효성에 대한 제약조건을 직접 구성한 뒤 @Validate 데코레이터를 통해 적용시키면 됩니다. 

 

// is-number-array.decorator.ts

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
  Validate,
} from 'class-validator';

@ValidatorConstraint({ name: 'isNumberArray', async: false })
export class IsNumberArrayConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    return this.isValid(value);
  }

  private isValid(arr: any[]): boolean {
    for (const elem of arr) {
      if (Array.isArray(elem)) {
        // Recursive check for nested arrays
        if (!this.isValid(elem)) {
          return false;
        }
      } else if (typeof elem !== 'number') {
        return false;
      }
    }
    return true;
  }

  defaultMessage(args: ValidationArguments) {
    return 'Each element of the array must be a number.';
  }
}

 

위 코드는 들어오는 N차원 배열에 대해 재귀적으로 모든 배열의 요소가 number 타입인지 확인하는 유효성 검사 제약조건을 구현한 예제입니다.

 

export class GeometryClass {
  @IsString()
  @IsNotEmpty()
  type: string;

  @IsNotEmpty()
  @Validate(IsNumberArrayConstraint)
  coordinates: number | number[] | number[][] | number[][][] | number[][][][];
}

 

이후 위의 DTO Class의 geometry 좌표를 표현하기 위한 coordinates라는 필드에 해당 @Validate 데코레이터를 통해 유효성검사 로직을 적용하였습니다. 

 

 

원하는 제약조건 생성을 위한 주요 class-validator의 인터페이스 및 클래스

 

원하는 제약조건 생성 방법에 대해 좀 더 자세히 알아보도록 하겠습니다.

 

기본적으로 class-validator 패키지에서 제공하는 @ValidatorConstraint, @Validate, ValidatorConstraintInterface 을 이용해서 나만의 Validation 데코레이터를 구축할 수 있습니다. 

 

@ValidatorConstraint

 

@ValidatorConstraint({ name: 'customText', async: false })
  • 기본적인 Validation에 대한 메타데이터를 설정할 때 사용됩니다. 
  • 기본적으로 꼭 사용될 필요는 없습니다. (생략 시 자동 생성됨)
  • name : ValidationError 발생 시 에러 타입으로 사용됩니다. 
  • async : 비동기 함수일 때 true를 명시하여 줍니다.

 

ValidatorConstraintInterface

 

export class CustomTextValidator implements ValidatorConstraintInterface {
  validate(text: string, args: ValidationArguments) {
    // Custom validation logic
    const isValid = /* Your validation logic here */;
    return isValid;
  }

  defaultMessage(args: ValidationArguments) {
    // Custom error message
    return 'Custom validation failed';
  }
}
  • 기본적인 유효성 검사 함수를 구현하도록 명시해 주는 인터페이스입니다.
  • 해당 인터페이스가 구현되면 validate 함수와 defaultMessage 함수를 구현해야 합니다.
  • validate : 실제 유효성 검사에 대한 비지니스 로직이 들어갑니다.
  • defaultMessage : 유효성검증이 실패하면 보여줄 메세지를 지정합니다. 

 

@Validate

 

export class Post {
  @Validate(CustomTextLength, [3, 20], { // constraint 전달
    message: 'Wrong post title',
  })
  title: string;
}


import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';

@ValidatorConstraint()
export class CustomTextLength implements ValidatorConstraintInterface {
  validate(text: string, validationArguments: ValidationArguments) {
    // 전달받은 constraints를 통해 유효성 검사 수행
    return text.length > validationArguments.constraints[0] && text.length < validationArguments.constraints[1];
  }
}
  • 최종적으로 @Validate 데코레이터를 생성하는 함수입니다. 해당 데코레이터를 구현한 필드에 대해 유효성 검사가 수행됩니다. 
  • @Validate 데코레이터는 첫 번째 파라미터로 ValidatorConstraintInterface 를 구현한 Class가 입력됩니다.
  • 두 번째 인자로 추가적인 값을 제약조건이 구현된 클래스로 전달할 수도 있습니다. 해당 값은 원하는 제약조건에 대한 설정값을 직접 입력받을 때 사용됩니다. 아래 예제에서는 [3, 20] 을 전달하여 해당 범위의 크기에 해당되는 문자열만 입력 설정하였습니다. 

 

Custom Validation 데코레이터 생성해 보기

 

@Validate 데코레이터를 통해 제약조건을 적용하여 유효성검사를 수행할 수도 있지만 나만의 이름을 가진 Custom 한 데코레이터를 생성하여 유효성검사를 수행할 수도 있습니다. 앞서 설명드린 로직과 90%는 동일하고, 단지 차이점은 registerDecorator라는 함수로 감싸주기만 하면 됩니다. 

 

export function IsNumberArray(validationOptions?: ValidationOptions) {
  return function (object: any, propertyName: string) {
    registerDecorator({
      name: 'isNumberArray',
      target: object.constructor,
      propertyName: propertyName,
      constraints: [],
      options: validationOptions,
      validator: {
        validate(arr: any[], args: ValidationArguments) {
          for (const elem of arr) {
            if (Array.isArray(elem)) {
              // Recursive check for nested arrays
              if (!this.isValid(elem)) {
                return false;
              }
            } else if (typeof elem !== 'number') {
              return false;
            }
          }
          return true;
        },
        defaultMessage(args: ValidationArguments) {
          return 'Default message for ' + propertyName;
        },
      },
    });
  };
}

 

export class GeometryClass {
  @IsString()
  @IsNotEmpty()
  type: string;

  @IsNumberArray({ message: 'Each element of the array must be a number.' })
  @IsNotEmpty()
  coordinates: number | number[] | number[][] | number[][][] | number[][][][];
}

 

 

이후에 위와 같이 @IsNumberArray라는 Custom Validation 데코레이터를 통해 유효성 검사를 수행할 수 있습니다. 

 

마무리


오늘은 NestJS에서 원하는 제약조건을 설정하는 방법과 Custom 한 이름을 유효성 검사 데코레이터를 생성하는 방법에 대해 알아보았습니다.

 

회사에서 express기반의 백엔드 코드들을 NestJS를 전환하게 되었는데, 초반에는 조금 사용법 및 구조를 파악하는데 어려웠는데요, 하지만 쓰면 쓸수록 확장성과 유지보수성에 뛰어나고 협업에도 좋은 프레임워크라고 생각이 드는 것 같습니다.

 

앞으로도 NestJS에 대한 내용을 포스팅 해보도록 하겠습니다.

 

감사합니다.

 

 

참고

 

 

GitHub - typestack/class-validator: Decorator-based property validation for classes.

Decorator-based property validation for classes. Contribute to typestack/class-validator development by creating an account on GitHub.

github.com

 

반응형