icon

DTO와 유효성 검사


 DTO(Data Transfer Object)는 프로세스 간 데이터 전송을 위한 객체입니다.

 NestJS에서 DTO는 클라이언트와 서버 간의 데이터 교환을 위한 인터페이스 역할을 하며, 데이터의 유효성을 보장하는 중요한 역할을 합니다.

DTO의 개념과 이점

 DTO 사용의 주요 이점

  1. 타입 안전성 보장
  2. 코드의 가독성 및 유지보수성 향상
  3. 데이터 유효성 검사 용이
  4. API 문서화 간소화

NestJS에서의 DTO 정의 및 사용

// create-user.dto.ts
export class CreateUserDto {
  readonly name: string;
  readonly email: string;
  readonly password: string;
}
 
// users.controller.ts
@Post()
create(@Body() createUserDto: CreateUserDto) {
  return this.usersService.create(createUserDto);
}

class-validator와 transformer 통합

  1. 설치
npm install class-validator class-transformer
  1. DTO에 유효성 검사 규칙 추가
import { IsString, IsEmail, MinLength } from 'class-validator';
 
export class CreateUserDto {
  @IsString()
  readonly name: string;
 
  @IsEmail()
  readonly email: string;
 
  @IsString()
  @MinLength(6)
  readonly password: string;
}
  1. 전역 파이프 설정 (main.ts)
import { ValidationPipe } from '@nestjs/common';
 
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

커스텀 유효성 검사 데코레이터

import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';
 
@ValidatorConstraint({ async: true })
export class IsUserAlreadyExistConstraint implements ValidatorConstraintInterface {
  constructor(private usersService: UsersService) {}
 
  validate(email: any, args: ValidationArguments) {
    return this.usersService.findByEmail(email).then(user => {
      return !user;
    });
  }
}
 
export function IsUserAlreadyExist(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      constraints: [],
      validator: IsUserAlreadyExistConstraint,
    });
  };
}
 
// 사용
export class CreateUserDto {
  @IsEmail()
  @IsUserAlreadyExist({ message: 'User already exists' })
  email: string;
}

글로벌 vs 라우트별 유효성 검사 파이프

 글로벌 파이프

app.useGlobalPipes(new ValidationPipe());

 라우트별 파이프

@Post()
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
create(@Body() createUserDto: CreateUserDto) {
  return this.usersService.create(createUserDto);
}

비동기 유효성 검사

@ValidatorConstraint({ async: true })
export class IsUniqueEmailConstraint implements ValidatorConstraintInterface {
  constructor(private usersService: UsersService) {}
 
  async validate(email: string) {
    const user = await this.usersService.findByEmail(email);
    return !user;
  }
}
 
// 사용
export class CreateUserDto {
  @IsEmail()
  @Validate(IsUniqueEmailConstraint, {
    message: 'Email already in use',
  })
  email: string;
}

DTO 변환 테크닉

 class-transformer를 사용한 DTO 변환

import { Expose, Transform } from 'class-transformer';
 
export class UserResponseDto {
  @Expose()
  id: number;
 
  @Expose()
  name: string;
 
  @Expose()
  email: string;
 
  @Expose()
  @Transform(({ obj }) => obj.profile.age)
  age: number;
}
 
// 컨트롤러에서 사용
@Get(':id')
@UseInterceptors(ClassSerializerInterceptor)
findOne(@Param('id') id: string) {
  return this.usersService.findOne(+id);
}

유효성 검사 오류 처리

 커스텀 오류 메시지

export class CreateUserDto {
  @IsString({ message: 'Name must be a string' })
  @IsNotEmpty({ message: 'Name is required' })
  readonly name: string;
 
  @IsEmail({}, { message: 'Invalid email format' })
  readonly email: string;
}

 글로벌 오류 필터

@Catch(BadRequestException)
export class ValidationExceptionFilter implements ExceptionFilter {
  catch(exception: BadRequestException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.getStatus();
 
    response
      .status(status)
      .json({
        statusCode: status,
        message: 'Validation failed',
        errors: exception.getResponse()['message'],
      });
  }
}
 
// main.ts에서 적용
app.useGlobalFilters(new ValidationExceptionFilter());

Best Practices 및 성능 고려사항

  1. DTO 재사용 : 상속을 통한 DTO 확장
export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  name: string;
 
  @IsEmail()
  email: string;
}
 
export class UpdateUserDto extends PartialType(CreateUserDto) {}
  1. 유효성 검사 그룹 사용
export class UserDto {
  @IsOptional({ groups: ['update'] })
  @IsNotEmpty({ groups: ['create'] })
  @IsString()
  name: string;
}
 
// 컨트롤러에서 사용
@Post()
@UsePipes(new ValidationPipe({ groups: ['create'] }))
create(@Body() createUserDto: UserDto) {
  // ...
}
  1. 중첩 객체 유효성 검사
export class AddressDto {
  @IsString()
  @IsNotEmpty()
  street: string;
 
  @IsString()
  @IsNotEmpty()
  city: string;
}
 
export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  name: string;
 
  @ValidateNested()
  @Type(() => AddressDto)
  address: AddressDto;
}
  1. 조건부 유효성 검사
@ValidatorConstraint({ name: 'customAge', async: false })
export class CustomAgeValidator implements ValidatorConstraintInterface {
  validate(age: number, args: ValidationArguments) {
    const [relatedPropertyName] = args.constraints;
    const relatedValue = (args.object as any)[relatedPropertyName];
    return relatedValue === 'adult' ? age >= 18 : true;
  }
 
  defaultMessage(args: ValidationArguments) {
    return 'Age must be at least 18 for adult users';
  }
}
 
export class CreateUserDto {
  @IsString()
  @IsIn(['adult', 'child'])
  type: string;
 
  @Validate(CustomAgeValidator, ['type'])
  age: number;
}
  1. 성능 최적화
  • 필요한 경우에만 transform: true 옵션 사용
  • 복잡한 유효성 검사 로직은 서비스 레이어로 이동
  • 대규모 DTO의 경우 부분 유효성 검사 고려
  1. 보안 고려사항
  • whitelist : true 옵션을 사용하여 알려진 속성만 허용
  • 민감한 데이터는 DTO에서 제외 (@Exclude() 데코레이터 사용)

 DTO는 클라이언트와 서버 간의 데이터 교환을 명확히 정의합니다.

 class-validator와 class-transformer를 통한 강력한 유효성 검사 메커니즘은 데이터의 정확성을 보장합니다.

 커스텀 유효성 검사 데코레이터와 비동기 유효성 검사를 통해 복잡한 비즈니스 규칙을 DTO 레벨에서 구현할 수 있으며, 이는 컨트롤러와 서비스 레이어의 로직을 간소화하는 데 도움이 됩니다.

 또한 DTO 변환 테크닉을 활용하면 내부 데이터 모델과 외부 API 응답을 효과적으로 분리할 수 있습니다.

 성능 측면에서는 유효성 검사 로직의 복잡성과 빈도를 고려해야 합니다. 간단한 유효성 검사는 DTO 레벨에서 처리하고, 복잡하거나 시간이 많이 소요되는 검사는 서비스 레이어로 이동하는 것이 좋습니다.

 또한 대규모 애플리케이션에서는 부분 유효성 검사나 유효성 검사 그룹을 활용하여 성능을 최적화할 수 있습니다.

 보안 관점에서 DTO는 입력 데이터를 필터링하고 검증하는 첫 번째 방어선 역할을 합니다.

 whitelist 옵션을 활용하여 알려진 속성만을 허용하고, 민감한 데이터는 DTO에서 명시적으로 제외하는 것이 중요합니다.

 이러한 방식으로 DTO와 유효성 검사를 활용하면, 더 안전하고 견고한 NestJS 애플리케이션을 구축할 수 있습니다.