
Nest.js에서 요청 데이터를 어떻게 다루느냐는 애플리케이션의 안정성과 직결된다. 클라이언트로부터 전달되는 데이터는 항상 신뢰할 수 없으며, 타입이 다르거나, 누락되었거나, 의도하지 않은 값이 포함되어 있을 가능성을 전제로 처리해야 한다. 이러한 문제를 컨트롤러 내부에서 매번 직접 처리하는 방식은 코드 중복을 낳고, 책임의 경계를 흐리게 만든다.
Nest.js는 이 문제를 파이프(Pipe)라는 개념으로 해결한다. 파이프는 컨트롤러에 요청이 전달되기 이전 단계에서 데이터의 변환과 유효성 검사를 담당하며, 애플리케이션 전반에 걸쳐 일관된 입력 처리 전략을 적용할 수 있도록 한다.
Pipes
Nest.js는 이 문제를 **파이프(Pipe)**라는 개념으로 해결한다. 파이프는 요청 데이터의 유효성 검사와 변환을 담당하는 메커니즘으로, 컨트롤러에 전달되기 이전 단계에서 데이터를 검증(validation)하거나 필요한 형태로 변환(transformation)하는 역할을 한다.
Nest.js는 다양한 상황에서 바로 사용할 수 있도록 여러 빌트인 파이프를 제공한다. 타입 변환을 담당하는 ParseIntPipe, ParseBoolPipe와 같은 파이프들이 있고, DTO 기반 검증을 수행하는 ValidationPipe, 기본 값을 설정하는 DefaultValuePipe도 제공된다. 물론 프레임워크에서 제공하지 않는 동작이 필요하다면 커스텀 파이프를 직접 구현할 수도 있다.
파이프는 적용 위치에 따라 글로벌 파이프, 핸들러 파이프, 파라미터 파이프로 구분된다.
// 글로벌 파이프
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({ stopAtFirstError: true }),
);
await app.listen(3000);
}// 핸들러 파이프
@Get()
@UsePipes(customPipes)
createBoard(@Body() createBoardDTO: CreateBoardDTO) {}// 파라미터 파이프
@Get()
createBoard(
@Body('title', new DefaultValuePipe('제목 없음'))
title: string,
) {}Custom Pipe
Nest.js에서 제공하는 빌트인 파이프로는 표현하기 어려운 동작이 필요하다면 커스텀 파이프를 구현할 수 있다. 커스텀 파이프는 PipeTransform 인터페이스를 구현하며, 반드시 transform 메서드를 정의해야 한다.
transform 메서드는 두 개의 인자를 받는다. 첫 번째는 실제 전달된 값인 value, 두 번째는 파이프가 호출된 맥락 정보를 담고 있는 metadata이다.
type:"body" | "query" | "param" | "custom"중 하나의 값으로, 어떤 종류의 매개변수에 적용되었는지를 나타낸다.metatype: 원본 매개변수의 타입 정보이다.data: 데코레이터에 전달된 추가 데이터로,@Body('userTypes')처럼 특정 필드를 지정한 경우 해당 값이 들어온다.
export class CustomPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if (value === undefined) {
throw new BadRequestException('');
}
return value;
}
}커스텀 파이프를 핸들러 레벨에 적용하면 해당 핸들러의 모든 매개변수에 대해 순차적으로 실행되고, 파라미터 레벨에 적용하면 특정 매개변수 하나에 대해서만 동작한다.
DTO와 ValidationPipe
DTO(Data Transfer Object)는 애플리케이션 내부에서 데이터를 안전하게 주고받기 위한 객체이며, 입력 검증의 기준이 되는 타입이다. Nest.js에서는 DTO와 파이프를 결합해 입력 데이터의 구조와 유효성을 동시에 보장한다.
이 과정에서 일반적으로 class-validator와 class-transformer 라이브러리를 함께 사용한다.
npm i class-validator class-transformer --saveclass-validator는 다양한 검증 데코레이터를 제공하는 라이브러리로, 문자열, 숫자, 열거형, 배열 등 실무에서 필요한 대부분의 검증 케이스를 커버한다.
export class CreateBoardDTO {
@IsNotEmpty()
@IsString()
title: string;
@IsNotEmpty()
@IsString()
description: string;
@IsOptional()
@IsEnum(BoardStatus)
status: BoardStatus;
@IsIn(['맛집', '관광지', '숙소'], { each: true })
@IsArray()
@IsOptional()
tag: Array<Tag>;
}@Transform
앞서 살펴본 검증 데코레이터들이 개별 프로퍼티 단위의 검증을 담당한다면, class-transformer가 제공하는 @Transform 데코레이터는 값 변환 과정에 커스텀 로직을 삽입할 수 있도록 한다.
콜백 함수는 해당 프로퍼티의 값(value)과, 그 프로퍼티가 속한 객체 전체(obj)를 인자로 받는다. 이를 활용해 전처리나 조건부 검증 로직을 구현할 수 있다.
// 공백 제거
@Transform(({ value }) => value.trim())// 비밀번호 일치 확인
@Transform(({ obj, value }) => {
if (obj.password === obj.checkPassword) return value;
throw new BadRequestException(
'비밀번호와 비밀번호 확인이 일치하지 않습니다',
);
})class-validator와 class-transformer를 함께 사용하려면 ValidationPipe를 글로벌 파이프로 등록하고 transform 옵션을 활성화해야 한다.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({ transform: true }),
);
await app.listen(3000);
}Custom Validator Decorators
@Transform에 검증 로직까지 포함시키는 것이 어색하게 느껴진다면, class-validator의 registerDecorator를 활용해 커스텀 검증 데코레이터를 만들 수 있다. 이는 검증 책임을 명확하게 분리하는 데 도움이 된다.
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
export function IsLongerThan(
property: string,
validationOptions?: ValidationOptions,
) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isLongerThan',
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return (
typeof value === 'string' &&
typeof relatedValue === 'string' &&
value.length > relatedValue.length
);
},
},
});
};
}Mapped Types for DTO
Express.js 환경에서는 superstruct를 사용해 하나의 타입으로부터 여러 구조를 파생시켰다. Nest.js에서는 이를 위해 @nestjs/mapped-types 패키지를 제공한다.
npm i @nestjs/mapped-types --save이 패키지는 PartialType, PickType, OmitType, IntersectionType과 같은 유틸리티 클래스를 제공하며, 모두 기존 DTO를 기반으로 새로운 DTO를 생성한다.
export class UpdateTitleDTO extends PickType(
PartialType(CreateBoardDTO),
['title'] as const,
) {}부록1: 잉여 속성 제거
ValidationPipe의 whitelist: true 옵션을 사용하면 DTO에 정의되지 않은 속성을 자동으로 제거할 수 있다. 이는 입력 검증을 넘어 보안 측면에서도 중요한 역할을 한다.
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);예를 들어 DTO가 아래와 같을 때,
class CreateUserDto {
@IsNotEmpty()
@IsString()
email: string;
}클라이언트가 다음과 같은 요청을 보내더라도,
{
"email": "ayden@naver.com",
"role": "admin"
}role 속성은 자동으로 제거되고 email만 컨트롤러로 전달된다.
부록2: 응답 데이터 필터링
요청뿐 아니라 응답 역시 검열할 수 있다. class-transformer의 plainToInstance 함수를 사용하면 일반 객체를 DTO 인스턴스로 변환하면서 @Expose, @Exclude 데코레이터를 적용할 수 있다.
@Get('user')
@UseGuards(AuthenticatedGuard)
getUser(@Req() req: Request): UserResDTO {
return plainToInstance(UserResDTO, req.user);
}export class UserResDTO {
@Expose()
id: string;
@Expose()
email: string;
@Expose()
nickname: string;
@Exclude()
password: string;
@Exclude()
createdAt?: Date;
}이 방식은 민감한 정보가 실수로 응답에 포함되는 것을 방지하는 데 매우 효과적이다.
더 읽어보기
2025.05.21
14. Redis를 사용한 세션 관리 및 캐싱
새로운 프로젝트를 준비하면서 인증 방식부터 다시 고민하게 되었다. 이전 프로젝트에서는 JWT를 사용해 유저 인증과 상태 관리를 처리했지만, 이번에는 Redis를 활용한 세션 방식으로 전환하기로 결정했다. 이 방식은 유저 상태를 서버에서 직접 관리할 수 있어 보안 측면에서도 유리하고, 필…
2025.01.30
비즈니스 로직은 어디에 있어야 할까
Nest.js에서 컨트롤러와 각각의 프로바이더는 분명한 책임을 갖는다. 하지만 막상 서버를 개발하다 보면, 이 책임들을 깊이 고민하지 않은 채 코드를 작성하게 되고, 그 결과 클래스들이 서로의 영역을 침범하는 상황이 반복된다. 나 역시 컨트롤러에 비즈니스 로직이 섞이거나, 서비스가 지나…
2024.12.07
라이브러리를 죽여버릴 수야 없겠지만
상황 Nest.js 서버에서 jest를 사용한 테스트 코드를 작성하고 있었다. 평소에는 아래와 같이 ConfigService를 모의하여 configService.get으로 환경 변수를 처리했다. 그런데 이런 방식이 마음에 들지 않았다. 가장 큰 이유는 필요한 문자열이 하드코딩 되어있어…
2024.12.07
13. Jest 테스트 구성
이 포스트를 시작하기에 앞서 한 가지 분명히 밝혀두고 싶은 점이 있다. 나는 컨트롤러(혹은 GraphQL 기준으로는 리졸버) 와 레포지토리 클래스에 대한 테스트를 거의 작성하지 않는다. 이유는 단순하다. 애플리케이션에서 가장 많은 비즈니스 규칙이 응집되어 있는 곳은 서비스 계층이며, 테…
2026.06.01
React Server Components를 위한 컴포넌트 아키텍처
이 포스트는 Vercel의 Next.js 팀 소속 개발자 Aurora Scharff가 자신의 블로그에 올린 Component Architecture for React Server Components 게시글을 번역한 것이다. 번역하는 과정에서 다소 의역이 있을 수 있으며, 일부 번역에는…
2026.05.26
차트는 멈췄는데 윈도우가 움직인다
상황 어느날 서비스를 살펴보시던 팀장님께서 이런 말씀을 slack에 남기셨다. 진호님, 예측 차트에서 zoom을 계속하면 어느 순간 라인 차트가 아니라 단일 스캐터 차트처럼 보이는 데, 이거 수정하면 좋을 거 같아요. 어느정도 zoom을 하면 그 이후로는 zoom이 안 되도록 할 수 없…
2026.05.21
피자가게로 이해하는 디자인 패턴
에이든 피자는 처음부터 복잡한 시스템을 만들 생각이 없었다. 처음에는 메뉴 몇 개만 만들면 됐다. 그런데 손님은 커스텀 주문을 넣기 시작했고, 주방은 상태를 나눠야 했고, 결제와 배달앱과 알림이 하나씩 붙었다. 코드도 가게를 닮는다. 장사가 잘될수록 이상하게 더 쉽게 망가진다. 디자인…
2026.05.21
7. Decorator — 토핑 추가할 때마다 클래스를 새로 만들 수 없다
에이든 피자에서 주문서를 객체로 만들자 취소와 재주문은 한결 편해졌다. 그런데 주문이 편해지자 손님들도 한결 편해졌다. 편해진 손님은 더 많은 요구를 한다. "치즈 추가요", "올리브도 추가요", "소스 많이요", "조금 더 바삭하게 구워주세요" 같은 요청이 주문대 위로 쌓이기 시작했다…
댓글
댓글을 불러오는 중...