04. 파이프를 사용한 유효성 검사

작성일:2024.06.27|조회수:1

04. 파이프를 사용한 유효성 검사

Nest.js에서 요청 데이터를 어떻게 다루느냐는 애플리케이션의 안정성과 직결된다. 클라이언트로부터 전달되는 데이터는 항상 신뢰할 수 없으며, 타입이 다르거나, 누락되었거나, 의도하지 않은 값이 포함되어 있을 가능성을 전제로 처리해야 한다. 이러한 문제를 컨트롤러 내부에서 매번 직접 처리하는 방식은 코드 중복을 낳고, 책임의 경계를 흐리게 만든다.

Nest.js는 이 문제를 파이프(Pipe)라는 개념으로 해결한다. 파이프는 컨트롤러에 요청이 전달되기 이전 단계에서 데이터의 변환과 유효성 검사를 담당하며, 애플리케이션 전반에 걸쳐 일관된 입력 처리 전략을 적용할 수 있도록 한다.

Pipes

Nest.js는 이 문제를 파이프(Pipe)라는 개념으로 해결한다. 파이프는 요청 데이터의 유효성 검사와 변환을 담당하는 메커니즘으로, 컨트롤러에 전달되기 이전 단계에서 데이터를 검증(validation)하거나 필요한 형태로 변환(transformation)하는 역할을 한다.

Nest.js는 다양한 상황에서 바로 사용할 수 있도록 여러 빌트인 파이프를 제공한다. 타입 변환을 담당하는 ParseIntPipe, ParseBoolPipe와 같은 파이프들이 있고, DTO 기반 검증을 수행하는 ValidationPipe, 기본 값을 설정하는 DefaultValuePipe도 제공된다. 물론 프레임워크에서 제공하지 않는 동작이 필요하다면 커스텀 파이프를 직접 구현할 수도 있다.

파이프는 적용 위치에 따라 글로벌 파이프, 핸들러 파이프, 파라미터 파이프로 구분된다.

JS
// 글로벌 파이프
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({ stopAtFirstError: true }),
  );
  await app.listen(3000);
}
TS
// 핸들러 파이프
@Get()
@UsePipes(customPipes)
createBoard(@Body() createBoardDTO: CreateBoardDTO) {}
TS
// 파라미터 파이프
@Get()
createBoard(
  @Body('title', new DefaultValuePipe('제목 없음'))
  title: string,
) {}

Custom Pipe

Nest.js에서 제공하는 빌트인 파이프로는 표현하기 어려운 동작이 필요하다면 커스텀 파이프를 구현할 수 있다. 커스텀 파이프는 PipeTransform 인터페이스를 구현하며, 반드시 transform 메서드를 정의해야 한다.

transform 메서드는 두 개의 인자를 받는다. 첫 번째는 실제 전달된 값인 value, 두 번째는 파이프가 호출된 맥락 정보를 담고 있는 metadata이다.

TS
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-validatorclass-transformer 라이브러리를 함께 사용한다.

SH
npm i class-validator class-transformer --save

class-validator는 다양한 검증 데코레이터를 제공하는 라이브러리로, 문자열, 숫자, 열거형, 배열 등 실무에서 필요한 대부분의 검증 케이스를 커버한다.

TS
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)를 인자로 받는다. 이를 활용해 전처리나 조건부 검증 로직을 구현할 수 있다.

TS
// 공백 제거
@Transform(({ value }) => value.trim())
TS
// 비밀번호 일치 확인
@Transform(({ obj, value }) => {
  if (obj.password === obj.checkPassword) return value;
  throw new BadRequestException(
    '비밀번호와 비밀번호 확인이 일치하지 않습니다',
  );
})

class-validatorclass-transformer를 함께 사용하려면 ValidationPipe를 글로벌 파이프로 등록하고 transform 옵션을 활성화해야 한다.

JS
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({ transform: true }),
  );
  await app.listen(3000);
}

Custom Validator Decorators

@Transform에 검증 로직까지 포함시키는 것이 어색하게 느껴진다면, class-validatorregisterDecorator를 활용해 커스텀 검증 데코레이터를 만들 수 있다. 이는 검증 책임을 명확하게 분리하는 데 도움이 된다.

TS
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 패키지를 제공한다.

SH
npm i @nestjs/mapped-types --save

이 패키지는 PartialType, PickType, OmitType, IntersectionType과 같은 유틸리티 클래스를 제공하며, 모두 기존 DTO를 기반으로 새로운 DTO를 생성한다.

JS
export class UpdateTitleDTO extends PickType(
  PartialType(CreateBoardDTO),
  ['title'] as const,
) {}

부록1: 잉여 속성 제거

ValidationPipewhitelist: true 옵션을 사용하면 DTO에 정의되지 않은 속성을 자동으로 제거할 수 있다. 이는 입력 검증을 넘어 보안 측면에서도 중요한 역할을 한다.

JS
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
  }),
);

예를 들어 DTO가 아래와 같을 때,

JAVA
class CreateUserDto {
  @IsNotEmpty()
  @IsString()
  email: string;
}

클라이언트가 다음과 같은 요청을 보내더라도,

JSON
{
  "email": "ayden@naver.com",
  "role": "admin"
}

role 속성은 자동으로 제거되고 email만 컨트롤러로 전달된다.

부록2: 응답 데이터 필터링

요청뿐 아니라 응답 역시 검열할 수 있다. class-transformerplainToInstance 함수를 사용하면 일반 객체를 DTO 인스턴스로 변환하면서 @Expose, @Exclude 데코레이터를 적용할 수 있다.

JAVA
@Get('user')
@UseGuards(AuthenticatedGuard)
getUser(@Req() req: Request): UserResDTO {
  return plainToInstance(UserResDTO, req.user);
}
TS
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

    13. Jest 테스트 구성

    이 포스트를 시작하기에 앞서 한 가지 분명히 밝혀두고 싶은 점이 있다. 나는 컨트롤러(혹은 GraphQL 기준으로는 리졸버) 와 레포지토리 클래스에 대한 테스트를 거의 작성하지 않는다.이유는 단순하다. 애플리케이션에서 가장 많은 비즈니스 규칙이 응집되어 있는 곳은 서비스 계층이며, 테스…

  • 2024.11.14

    12. GraphQL로 요청 처리

    GraphQL은 페이스북에서 개발한 데이터 쿼리 언어이자 런타임으로, 기존 REST API가 가진 구조적 한계를 보완하기 위해 등장했다. 전통적인 REST API에서는 각 엔드포인트마다 반환되는 데이터 구조가 고정되어 있고, 동일한 리소스에 대해 서로 다른 작업을 수행하기 위해 HTTP…

  • 2026.04.11

    Trie 자료구조

    문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…

  • 2026.03.19

    Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까

    웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…

댓글

댓글을 불러오는 중...