NestJS Pipe란?
NestJS에서 Pipe는 컨트롤러 핸들러가 실행되기 전에 인자를 변환(transformation)하거나 검증(validation)하는 레이어입니다. @Injectable() 데코레이터가 붙은 클래스로, PipeTransform 인터페이스를 구현합니다. Express/Fastify 미들웨어와 달리 Pipe는 인자 단위로 동작하며, 메타데이터(ArgumentMetadata)를 통해 어떤 파라미터에 적용되는지 정확히 알 수 있습니다.
PipeTransform 인터페이스 구조
모든 Pipe의 기반이 되는 인터페이스입니다.
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class CustomPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
// value: 실제 전달된 값
// metadata: { type, metatype, data }
// type: 'body' | 'query' | 'param' | 'custom'
// metatype: 파라미터의 TypeScript 타입 (예: Number, String, CreateUserDto)
// data: @Body('name')에서 'name' 같은 키
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException(`"${value}"는 숫자가 아닙니다`);
}
return val;
}
}
ArgumentMetadata의 metatype은 TypeScript의 타입 정보를 런타임에 전달합니다. 이를 활용하면 DTO 클래스 기반의 자동 검증이 가능해집니다.
빌트인 Pipe 완전 정리
NestJS는 6개의 빌트인 Pipe를 제공합니다.
| Pipe | 역할 | 실패 시 |
|---|---|---|
ValidationPipe |
class-validator 기반 DTO 검증 | 400 Bad Request |
ParseIntPipe |
정수 변환 | 400 Bad Request |
ParseFloatPipe |
실수 변환 | 400 Bad Request |
ParseBoolPipe |
불리언 변환 | 400 Bad Request |
ParseUUIDPipe |
UUID 포맷 검증 | 400 Bad Request |
ParseEnumPipe |
Enum 값 검증 | 400 Bad Request |
@Get(':id')
findOne(
@Param('id', ParseUUIDPipe) id: string, // UUID 검증
@Query('page', new ParseIntPipe({ optional: true })) page?: number, // 선택적 정수
@Query('active', ParseBoolPipe) active: boolean, // 불리언 변환
) {
return this.service.findOne(id, page, active);
}
ValidationPipe 심화 옵션
ValidationPipe는 NestJS에서 가장 많이 사용되는 Pipe입니다. class-validator와 class-transformer를 기반으로 DTO를 자동 검증합니다.
// main.ts - 글로벌 설정
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // DTO에 정의되지 않은 속성 자동 제거
forbidNonWhitelisted: true, // 정의되지 않은 속성이 있으면 400 에러
transform: true, // 자동 타입 변환 (string → number 등)
transformOptions: {
enableImplicitConversion: true, // @Type() 없이도 변환
},
disableErrorMessages: false, // 프로덕션에서 true 고려
stopAtFirstError: true, // 첫 에러에서 즉시 중단
exceptionFactory: (errors) => {
// 커스텀 에러 포맷
const messages = errors.map(err => ({
field: err.property,
errors: Object.values(err.constraints || {}),
}));
return new BadRequestException({ message: 'Validation failed', details: messages });
},
}));
whitelist + forbidNonWhitelisted 조합은 보안상 필수입니다. 클라이언트가 isAdmin: true 같은 예상치 못한 필드를 보내는 Mass Assignment 공격을 방지합니다.
DTO 검증 패턴: 그룹과 조건부 검증
같은 DTO를 생성/수정에서 다른 규칙으로 검증해야 할 때 validation groups를 사용합니다.
import { IsString, IsEmail, IsOptional, MinLength, ValidateIf } from 'class-validator';
export class UserDto {
@IsString({ groups: ['create', 'update'] })
@MinLength(2, { groups: ['create'] }) // 생성 시에만 최소 길이 검증
name: string;
@IsEmail({}, { groups: ['create'] }) // 생성 시에만 필수
@IsOptional({ groups: ['update'] }) // 수정 시 선택
email: string;
@ValidateIf(o => o.role === 'admin') // role이 admin일 때만 검증
@IsString()
adminSecret: string;
}
// 컨트롤러에서 그룹 지정
@Post()
create(
@Body(new ValidationPipe({ groups: ['create'] })) dto: UserDto,
) { ... }
@Patch(':id')
update(
@Body(new ValidationPipe({ groups: ['update'] })) dto: UserDto,
) { ... }
ValidateIf는 동적 조건부 검증에 유용합니다. 특정 필드의 값에 따라 다른 필드의 검증 여부를 결정할 수 있어, 복잡한 비즈니스 로직을 DTO 레벨에서 처리할 수 있습니다.
커스텀 Pipe 실전 패턴
빌트인으로 부족할 때 커스텀 Pipe를 만듭니다. 대표적인 실전 패턴들입니다.
// 1. 파일 검증 Pipe
@Injectable()
export class FileValidationPipe implements PipeTransform {
constructor(
private readonly maxSize: number = 5 * 1024 * 1024, // 5MB
private readonly allowedTypes: string[] = ['image/jpeg', 'image/png'],
) {}
transform(file: Express.Multer.File, metadata: ArgumentMetadata) {
if (!file) throw new BadRequestException('파일이 필요합니다');
if (file.size > this.maxSize)
throw new PayloadTooLargeException(`파일 크기 초과: ${this.maxSize}bytes`);
if (!this.allowedTypes.includes(file.mimetype))
throw new UnsupportedMediaTypeException(`허용 타입: ${this.allowedTypes}`);
return file;
}
}
// 2. 정렬 파라미터 변환 Pipe
@Injectable()
export class ParseSortPipe implements PipeTransform {
private readonly allowedFields = ['createdAt', 'name', 'price'];
transform(value: string): { field: string; order: 'ASC' | 'DESC' } {
if (!value) return { field: 'createdAt', order: 'DESC' };
const [field, order] = value.split(':');
if (!this.allowedFields.includes(field))
throw new BadRequestException(`정렬 불가 필드: ${field}`);
return { field, order: order?.toUpperCase() === 'ASC' ? 'ASC' : 'DESC' };
}
}
// 사용
@Get()
findAll(
@Query('sort', ParseSortPipe) sort: { field: string; order: 'ASC' | 'DESC' },
) { ... }
@Post('upload')
upload(
@UploadedFile(new FileValidationPipe(10 * 1024 * 1024, ['image/png']))
file: Express.Multer.File,
) { ... }
Pipe 적용 범위와 실행 순서
Pipe는 4가지 레벨에서 적용할 수 있으며, 실행 순서가 정해져 있습니다.
// 1. 글로벌 (main.ts)
app.useGlobalPipes(new ValidationPipe());
// 2. 모듈 레벨 (DI 활용 가능)
@Module({
providers: [{ provide: APP_PIPE, useClass: ValidationPipe }],
})
export class AppModule {}
// 3. 컨트롤러 레벨
@UsePipes(new ValidationPipe({ groups: ['admin'] }))
@Controller('admin')
export class AdminController {}
// 4. 파라미터 레벨
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {}
실행 순서: 글로벌 → 컨트롤러 → 라우트 → 파라미터. 파라미터 레벨 Pipe가 가장 나중에 실행됩니다. 글로벌 Pipe를 모듈의 APP_PIPE로 등록하면 DI 컨테이너를 활용할 수 있어, ConfigService 등을 주입받아 동적 설정이 가능합니다. 이는 NestJS Middleware의 실행 순서와 함께 이해해야 요청 파이프라인 전체를 파악할 수 있습니다.
비동기 Pipe와 DB 검증
Pipe의 transform은 Promise를 반환할 수 있습니다. DB 조회가 필요한 검증에 활용합니다.
@Injectable()
export class EntityExistsPipe implements PipeTransform {
constructor(
@InjectRepository(User) private readonly repo: Repository<User>,
) {}
async transform(id: string, metadata: ArgumentMetadata): Promise<User> {
const entity = await this.repo.findOne({ where: { id } });
if (!entity) throw new NotFoundException(`User #${id} not found`);
return entity; // id 대신 엔티티 자체를 반환!
}
}
// 컨트롤러에서 바로 엔티티를 받음
@Get(':id')
findOne(@Param('id', EntityExistsPipe) user: User) {
// user는 이미 DB에서 조회된 엔티티
return user;
}
이 패턴은 서비스 레이어의 보일러플레이트를 줄여줍니다. 하지만 Pipe에서 DB를 조회하면 관심사 분리 원칙과 충돌할 수 있으므로, 팀 컨벤션으로 범위를 정하는 것이 좋습니다. NestJS ExecutionContext를 활용한 Guard에서 권한 검증을 하고, Pipe에서 데이터 검증을 하는 식으로 역할을 분리하세요.
정리
NestJS Pipe는 단순한 유효성 검사 도구를 넘어, 요청 데이터의 변환·검증·정제를 담당하는 핵심 레이어입니다. ValidationPipe의 whitelist/transform 옵션으로 보안과 편의를 동시에 챙기고, 커스텀 Pipe로 파일 검증이나 정렬 파라미터 같은 반복 로직을 재사용 가능하게 만드세요. 비동기 Pipe는 강력하지만 관심사 분리를 고려해서 적절히 사용해야 합니다.