API를 만들 때 요청 데이터를 검증하지 않으면, 잘못된 데이터가 서비스 레이어까지 흘러들어갑니다. NestJS는 DTO + Pipe 조합으로 컨트롤러 진입 시점에서 데이터를 걸러냅니다. Spring의 @Valid + @RequestBody와 같은 역할인데, 어떻게 다른지 살펴보겠습니다.


컨트롤러 기본 구조

컨트롤러는 HTTP 요청을 받아서 적절한 서비스 메서드를 호출 하는 역할입니다. 비즈니스 로직은 넣지 않습니다.

TYPESCRIPT
@Controller('users')  // 기본 경로: /users
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()              // GET /users
  findAll() {
    return this.userService.findAll();
  }

  @Get(':id')         // GET /users/:id
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.userService.findOne(id);
  }

  @Post()             // POST /users
  create(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
  }

  @Put(':id')         // PUT /users/:id
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.userService.update(id, updateUserDto);
  }

  @Delete(':id')      // DELETE /users/:id
  @HttpCode(204)      // 응답 코드 지정
  remove(@Param('id') id: string) {
    return this.userService.remove(id);
  }
}

Spring과의 대응을 정리하면 이렇습니다.

NestJSSpring역할
@Controller('users')@RequestMapping("/users")기본 경로
@Get(), @Post()@GetMapping, @PostMappingHTTP 메서드
@Param('id')@PathVariable("id")경로 파라미터
@Body()@RequestBody요청 본문
@Query('page')@RequestParam("page")쿼리 파라미터
@Headers('authorization')@RequestHeader헤더 값

DTO — 요청 데이터의 타입 정의

DTO(Data Transfer Object) 는 요청 데이터의 형태를 정의하는 클래스입니다. TypeScript의 인터페이스로도 가능하지만, 클래스 를 써야 class-validator의 데코레이터를 붙일 수 있습니다.

TYPESCRIPT
import { IsString, IsEmail, IsOptional, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name: string;

  @IsEmail()
  email: string;

  @IsOptional()
  @IsString()
  phone?: string;
}

Spring의 @NotNull, @Size, @Email 어노테이션과 같은 역할입니다.


ValidationPipe — 요청 검증 자동화

DTO에 데코레이터를 붙여도 ValidationPipe를 설정하지 않으면 검증이 동작하지 않습니다.

TYPESCRIPT
// main.ts — 전역 설정
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,        // DTO에 없는 속성 자동 제거
    forbidNonWhitelisted: true,  // DTO에 없는 속성이 오면 400 에러
    transform: true,        // 문자열 → 타입 자동 변환
  }));
  await app.listen(3000);
}

각 옵션이 하는 일을 정리하면 이렇습니다.

옵션동작왜 필요한가
whitelistDTO에 정의되지 않은 속성 제거악의적인 추가 필드 차단
forbidNonWhitelisted정의되지 않은 속성이 있으면 에러클라이언트 실수를 조기 발견
transform@Param('id') 문자열을 숫자로 변환수동 타입 변환 불필요

whitelist를 켜지 않으면 생기는 일

JSON
// POST /users 요청 본문
{
  "name": "홍길동",
  "email": "hong@example.com",
  "isAdmin": true          // ← DTO에 없는 필드!
}

whitelist: true가 없으면 isAdmin이 그대로 서비스까지 전달됩니다. 이걸로 권한 상승 공격이 가능합니다. ** 반드시 켜야 합니다.**


내장 Pipe — 타입 변환과 검증

NestJS에는 자주 쓰는 변환/검증용 Pipe가 내장되어 있습니다.

TYPESCRIPT
// ParseIntPipe — 문자열을 정수로 변환, 실패 시 400
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {}

// ParseUUIDPipe — UUID 형식 검증
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {}

// DefaultValuePipe — 기본값 설정
@Get()
findAll(@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number) {}

Spring에서 @PathVariable에 타입을 지정하면 자동 변환되는 것과 비슷하지만, NestJS는 Pipe라는 명시적 단계 를 거칩니다. 변환 실패 시 자동으로 400 Bad Request를 반환합니다.


커스텀 Pipe 만들기

특수한 변환이나 검증이 필요하면 PipeTransform 인터페이스를 구현합니다.

TYPESCRIPT
@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
  transform(value: string, metadata: ArgumentMetadata): Date {
    const date = new Date(value);
    if (isNaN(date.getTime())) {
      throw new BadRequestException(`"${value}"는 유효한 날짜가 아닙니다`);
    }
    return date;
  }
}

// 사용
@Get()
findByDate(@Query('date', ParseDatePipe) date: Date) {}

주의할 점

DTO에 인터페이스를 쓰면 검증이 안 된다

TypeScript 인터페이스는 **컴파일 후 사라집니다 **. 런타임에 존재하지 않기 때문에 class-validator가 메타데이터를 읽을 수 없습니다.

TYPESCRIPT
// 이렇게 하면 검증 안 됨
interface CreateUserDto {
  name: string;
  email: string;
}

// 반드시 class로 정의
class CreateUserDto {
  @IsString()
  name: string;
}

이것은 TypeScript를 처음 쓰는 개발자가 가장 많이 하는 실수입니다.

class-transformer의 @Exclude로 응답 필드 제어

응답에 비밀번호 같은 민감한 필드가 포함되지 않도록 해야 합니다.

TYPESCRIPT
import { Exclude } from 'class-transformer';

export class User {
  id: number;
  name: string;

  @Exclude()
  password: string;  // 응답에서 제외
}

ClassSerializerInterceptor를 전역으로 등록해야 @Exclude()가 동작합니다.


정리

포인트내용
컨트롤러HTTP 요청 수신만 담당, 비즈니스 로직은 서비스로
DTOclass로 정의해야 검증 가능 (인터페이스 불가)
ValidationPipewhitelist: true 필수 — 없으면 보안 취약점
내장 PipeParseIntPipe, ParseUUIDPipe 등으로 타입 변환 자동화
핵심 기억Pipe가 컨트롤러 진입 전 데이터를 걸러주는 관문 역할
댓글 로딩 중...