본문으로 건너뛰기
개발 머꼬
개발 노트NestJS
hohyeon.dev20

DTO에 데코레이터를 다 붙였는데 검증이 걸리지 않은 이유

  • #Engineering Note
  • #NestJS
  • #Security

문제 발생

DTO에 검증 데코레이터를 다 붙였는데 검증이 전혀 걸리지 않았습니다. role: "admin" 같은 보내지 않기로 한 필드도 그대로 통과했습니다.

export interface CreateUserDto {   // ❌ interface
  email: string;
  nickname: string;
}

쿼리 파라미터를 숫자로 받는 곳에서는 비교가 항상 실패했습니다.

@Get()
findAll(@Query("page") page: number) {
  console.log(typeof page);   // "string"
}

원인 분석

첫째, DTO는 클래스여야 합니다. 공식 문서가 이유를 그대로 적습니다 — TypeScript는 제네릭이나 인터페이스에 대한 메타데이터를 저장하지 않기 때문에, DTO에 그것들을 쓰면 ValidationPipe가 들어온 데이터를 제대로 검증하지 못할 수 있습니다. 그래서 구체 클래스를 쓰라고 권합니다. 같은 이유로 DTO를 타입 전용 import(import type)로 가져오면 런타임에 지워져 동작하지 않습니다.

둘째, 선언하지 않은 속성은 기본적으로 통과합니다. 검증 데코레이터가 없는 속성은 검사 대상이 아닐 뿐 제거되지도 않습니다. whitelist가 그걸 바꿉니다 — 문서의 설명대로 검증 데코레이터를 쓰지 않는 속성을 결과 객체에서 걷어냅니다.

셋째, 경로·쿼리 파라미터는 문자열입니다. transform을 켜면 문서가 말하는 대로 ValidationPipe가 원시 타입 변환도 수행합니다 — 핸들러 시그니처에 선언한 타입을 보고 바꿔줍니다.

해결 방안

  1. DTO를 클래스로 만들고 데코레이터를 붙입니다.
export class CreateUserDto {
  @IsEmail()
  email!: string;

  @IsString()
  @Length(2, 20)
  nickname!: string;
}
  1. 전역으로 ValidationPipe를 켭니다.
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }),
);
  1. whitelistforbidNonWhitelisted의 차이를 압니다. 앞은 조용히 걷어내고, 뒤는 문서의 설명대로 걷어내는 대신 예외를 던집니다. 클라이언트 버그를 빨리 드러내고 싶으면 뒤가 낫고, 관대해야 하는 공개 API면 앞이 낫습니다.

  2. transform: true로 타입을 맞춥니다. 이걸 켜면 payload가 DTO 클래스의 인스턴스로 바뀌고, 원시 타입 파라미터도 선언한 타입으로 변환됩니다.

@Get()
findAll(@Query("page") page: number) {
  console.log(typeof page);   // "number"
}
  1. import type을 쓰지 않습니다. 이게 특히 잘 숨습니다 — 에디터의 자동 정리 기능이 조용히 import type으로 바꿔놓으면, 코드는 그대로인데 검증만 사라집니다.
import { CreateUserDto } from "./dto/create-user.dto";        // 필요
import type { CreateUserDto } from "./dto/create-user.dto";   // 런타임에 사라짐
  1. 서버 검증을 UI 검증으로 대체하지 않습니다. 폼에서 막는 것과 별개로, 요청은 언제든 직접 만들어 보낼 수 있습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.