DTO에 데코레이터를 다 붙였는데 검증이 걸리지 않은 이유
문제 발생
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가 원시 타입 변환도 수행합니다 — 핸들러 시그니처에 선언한 타입을 보고 바꿔줍니다.
해결 방안
- DTO를 클래스로 만들고 데코레이터를 붙입니다.
export class CreateUserDto {
@IsEmail()
email!: string;
@IsString()
@Length(2, 20)
nickname!: string;
}- 전역으로
ValidationPipe를 켭니다.
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);-
whitelist와forbidNonWhitelisted의 차이를 압니다. 앞은 조용히 걷어내고, 뒤는 문서의 설명대로 걷어내는 대신 예외를 던집니다. 클라이언트 버그를 빨리 드러내고 싶으면 뒤가 낫고, 관대해야 하는 공개 API면 앞이 낫습니다. -
transform: true로 타입을 맞춥니다. 이걸 켜면 payload가 DTO 클래스의 인스턴스로 바뀌고, 원시 타입 파라미터도 선언한 타입으로 변환됩니다.
@Get()
findAll(@Query("page") page: number) {
console.log(typeof page); // "number"
}import type을 쓰지 않습니다. 이게 특히 잘 숨습니다 — 에디터의 자동 정리 기능이 조용히import type으로 바꿔놓으면, 코드는 그대로인데 검증만 사라집니다.
import { CreateUserDto } from "./dto/create-user.dto"; // 필요
import type { CreateUserDto } from "./dto/create-user.dto"; // 런타임에 사라짐- 서버 검증을 UI 검증으로 대체하지 않습니다. 폼에서 막는 것과 별개로, 요청은 언제든 직접 만들어 보낼 수 있습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.