HooneyLog
© 2026 Seunghoon Shin. All rights reserved.
모든 게시글
Backend
2026. 3. 29.•
1

[NestJS] Pipe 완벽 가이드: 데이터 검증과 변환의 예술

Seunghoon Shin
작성자 Seunghoon Shin풀스택 개발자

[NestJS] Pipe 완벽 가이드: 데이터 검증과 변환의 예술

1. 문제의 배경

API 서버를 개발할 때 가장 번거롭지만 중요한 작업 중 하나는 클라이언트가 보낸 데이터가 올바른지 검증하는 것입니다. 만약 숫자형 데이터가 필요한 곳에 문자열이 들어오거나, 필수 파라미터가 누락된 채로 컨트롤러 로직이 실행된다면 치명적인 런타임 에러나 보안 취약점(예: SQL Injection)으로 이어질 수 있습니다.

과거에는 컨트롤러 내부에서 수동으로 if (!req.body.age || isNaN(req.body.age)) 와 같은 방어 로직을 작성해야 했습니다. 이는 코드를 비대하게 만들고, 비즈니스 로직과 데이터 검증 로직이 강하게 결합되는 문제를 낳았습니다. 단일 책임 원칙(SRP) 관점에서도 좋지 않은 구조입니다.

2. 해결 방안 탐색

Express 기반 환경에서는 Joi나 express-validator 같은 미들웨어를 사용하여 라우터 레벨에서 검증을 수행하기도 합니다. 하지만 미들웨어는 현재 실행될 핸들러나 매개변수의 메타데이터에 접근하기 어렵다는 한계가 있습니다.

NestJS는 이를 우아하게 해결하기 위해 Pipe(파이프) 라는 개념을 도입했습니다. 파이프는 컨트롤러의 라우트 핸들러가 호출되기 직전에 동작하며, 들어오는 인자(Arguments)를 가로채어 다음과 같은 두 가지 주요 역할을 수행합니다.

  1. Transformation (변환): 입력 데이터를 원하는 형태로 변환합니다. (예: 문자열 "10" -> 숫자 10)
  2. Validation (검증): 입력 데이터가 유효한지 평가하고, 유효하지 않으면 예외를 발생시켜 핸들러 실행을 차단합니다.

3. 핵심 개념 및 아키텍처

파이프는 NestJS의 요청 라이프사이클 중 매우 명확한 위치에서 실행됩니다.

파이프는 @Injectable() 데코레이터가 달린 클래스로, 반드시 PipeTransform 인터페이스를 구현해야 합니다. NestJS는 9개의 유용한 내장 파이프를 제공합니다 (예: ValidationPipe, ParseIntPipe, ParseUUIDPipe 등).

4. 구현 및 트러블슈팅

4.1. 내장 파이프 활용 (Transformation)

가장 단순한 형태는 경로 매개변수나 쿼리 스트링을 특정 타입으로 변환하는 것입니다.

import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common'; @Controller('users') export class UsersController { @Get(':id') async findOne(@Param('id', ParseIntPipe) id: number) { // ParseIntPipe 덕분에 id는 무조건 숫자형(number)임이 보장됩니다. // 만약 클라이언트가 '/users/abc'를 요청하면, 자동으로 400 에러를 반환합니다. return this.usersService.findOne(id); } }

4.2. DTO와 ValidationPipe (Validation)

실무에서는 class-validator와 class-transformer를 결합하여 DTO(Data Transfer Object) 기반의 검증을 수행하는 것이 표준입니다.

// create-user.dto.ts import { IsString, IsInt, Min, Max } from 'class-validator'; export class CreateUserDto { @IsString() readonly name: string; @IsInt() @Min(0) @Max(120) readonly age: number; }

컨트롤러에 적용하거나 전역으로 설정할 수 있습니다.

// main.ts (전역 설정) import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe({ whitelist: true, // DTO에 정의되지 않은 속성은 자동 제거 forbidNonWhitelisted: true, // 정의되지 않은 속성이 오면 에러 발생 transform: true, // 클라이언트 페이로드를 DTO 인스턴스로 자동 변환 }), ); await app.listen(3000); } bootstrap();

주의할 점: transform: true를 활성화하면 런타임 성능에 약간의 오버헤드가 발생할 수 있지만, 컨트롤러에서 평범한 자바스크립트 객체가 아닌 실제 DTO 클래스의 인스턴스를 받을 수 있어 메서드(예: DTO 내부의 getter) 사용이 가능해집니다.

4.3. 커스텀 파이프 작성

복잡한 비즈니스 로직 기반의 검증이 필요하다면 커스텀 파이프를 만듭니다.

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; @Injectable() export class ParseDatePipe implements PipeTransform<string, Date> { transform(value: string, metadata: ArgumentMetadata): Date { const val = Date.parse(value); if (isNaN(val)) { throw new BadRequestException('올바른 날짜 형식이 아닙니다 (예: YYYY-MM-DD)'); } return new Date(val); } }

5. 결과 및 Trade-off

도입 이점:

  • 관심사의 분리(SoC): 컨트롤러는 비즈니스 로직에만 집중할 수 있어 코드가 매우 깔끔해집니다.
  • 선언적 검증: 데코레이터를 통한 선언적인 검증으로 가독성이 크게 향상됩니다.
  • 안전성: 의도치 않은 쓰레기 데이터나 악의적인 페이로드가 내부 로직으로 진입하는 것을 원천 차단합니다.

Trade-off (한계점):

  • 글로벌 파이프의 과도한 사용: ValidationPipe의 transform: true나 forbidNonWhitelisted 옵션은 강력하지만, 모든 라우트에 일괄 적용되므로 성능에 민감한 엔드포인트나 유연한 페이로드가 필요한 웹훅(Webhook) 처리 라우트 등에서는 방해가 될 수 있습니다. (이 경우 파이프를 라우트 레벨이나 파라미터 레벨로 분리하여 적용해야 합니다.)

6. 마치며

NestJS의 파이프는 입력 데이터를 통제하는 가장 강력하고 우아한 도구입니다. 기본적인 ValidationPipe 사용을 넘어서, 커스텀 파이프를 통해 시스템에 맞는 정교한 필터링 구조를 설계해 보시기 바랍니다. 데이터가 깨끗하면 비즈니스 로직도 자연스럽게 깨끗해집니다.

참고 자료:

  • NestJS 공식 문서 - Pipes
  • class-validator GitHub
← 이전 글하네스 엔지니어링(Harness Engineering): 2026년 AI 기반 개발의 새로운 패러다임
다음 글 →[TS/JS] 직렬화(Serialization)와 역직렬화(Deserialization) 깊게 파보기