Search

3. 파이프로 요청 데이터 검증하기

1. 데코레이터로 요청 데이터에 액세스하기

Nest를 사용할 때 유입되는 요청에서 정보를 추출하기 위해 몇 가지 데코레이터를 알아두어야 한다.
그 전에 HTTP 요청이 전반적으로 어떻게 작동하는지 간단히 상기해보자.
요청은 텍스트로 된 본문이고 텍스트 몇 줄로 되어 있다.
첫 번째 줄은 시작 줄이라고 부르고 요청 메스도와 전체 경로, 그리고 사용된 프로토콜이 포함된다.
이어서 요청 헤더와 본문이 포함된다.
우리는 본문을 추출하려 할 수도 있고, 혹은 쿼리 문자열의 일부를 추출하거나 어떤 URL 파라미터를 추출하려고 할 수도 있다.
Nest는 이러한 정보를 추출하기 위해 각기 다른 데코레이터를 제공하고 있다.
@Param()
유입되는 요청의 URL로부터 파라미터, 즉 와일드카드 값을 추출할 때 사용하는 데코레이터
우리가 액세스하려는 부분 또는 와일드 카드에 관한 문자열을 넣어준다.
예를 들어, URL에 :id라는 와일드카드 부분이 있다면 ‘id’라는 문자열과 함께 @Param() 데코레이터를 사용한다.
@Query()
쿼리 문자열 부분을 추출할 때 사용하는 데코레이터
@Headers()
헤더 부분을 추출할 때 사용하는 데코레이터
@Body()
본문 부분을 추출할 때 사용하는 데코레이터
이 모든 데코레이터는 common 라이브러리로부터 임포트할 수 있다.
import { Controller, Get, Post, Body, Param } from '@nestjs/common'; @Controller('messages') export class MessagesController { @Get() listMessages() {} @Post() createMessage(@Body() body: any) { console.log(body); } @Get('/:id') getMessage(@Param('id') id: string) { console.log(id); } }
TypeScript
복사
@Controller()는 클래스 전체에 적용하고 있기 때문에 클래스 데코레이터
@Get()@Post()는 메서드 전체에 적용하고 있기 때문에 메서드 데코레이터
@Body()@Param()인수 데코레이터
이제 두 라우트 핸들러에 각각 다음과 같은 요청을 보내면 다음과 같은 로그를 확인할 수 있다.
### Create a new message POST http://localhost:3000/messages content-type: application/json { "content": "hi there" }
JSON
복사
### Get a particular message GET http://localhost:3000/messages/123123123
JSON
복사

2. ValidationPipe 사용하기

우리는 POST 요청이 유입될 때마다 요청 본문에 문자열로 된 속성 “content”를 검증해야한다.
그리고 만일 그 데이터가 어떤 식으로든 무효하다면 우리는 그 요청을 거부하고 컨트롤러에 도착하기 전에 그걸 요청자에게 반환해야 한다.
우리는 파이프를 사용해서 이 데이터가 라우트 핸들러에 도달하기 전에 검증할 것이다.
그렇게 함으로써 우리는 createMessage() 메서드를 실행하기 전에 본문이 문자열로 된 “content”라는 속성이 있는 객체라는 걸 확실히 알게 된다.
우리가 직접 파이프를 만들 수 있지만, NestJS가 제공하는 기본 파이프 ValidationPipe를 이용해 검증을 해보자.

2.1. class-validator 및 class-transformer 설치

NestJS에서 유효성 검사를 수행하려면 class-validatorclass-transformer 패키지를 설치해야 한다.
이 두 패키지는 DTO(Data Transfer Object)에서 유효성 검사를 적용하는 데 사용된다.
다음 명령어를 실행하여 설치한다.
npm install class-validator class-transformer
Shell
복사

2.2. 글로벌 파이프로 ValidationPipe 설정

main.ts 파일에서 전역적으로 ValidationPipe를 설정하면 모든 컨트롤러에 일괄적으로 적용할 수 있다.
// main.ts import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; import { MessagesModule } from './messages/messages.module'; async function bootstrap() { const app = await NestFactory.create(MessagesModule); app.useGlobalPipes(new ValidationPipe()); // 글로벌 ValidationPipe 적용 await app.listen(3000); } bootstrap();
TypeScript
복사
그렇다고 모든 라우트 핸들러에 검증 규칙을 추가할 필요는 없는게, 핸들러에 검증 규칙을 추가하지 않으면 ValidationPipe는 그 핸들러에서 작동하지 않게 된다.

3. 검증 규칙 사용하기

다음으로는 ValidationPipe에 아주 특별한 검증 규칙들을 제공하고 createMessage() 라우트 핸들러에 요청이 유입될 때마다 그 검증 규칙을 사용하도록 해야 한다.
설정은 크게 4단계로 이루어진다.
1.
전역적인 검증을 사용하라고 Nest에게 알린다.
2.
요청 본문이 가져야 할 다양한 속성을 설명하는 클래스를 작성한다.
3.
클래스에 유효성 검사 규칙을 추가한다.
4.
해당 클래스를 요청 핸들러에 적용한다.
우리는 1단계는 이미 이전 단계에서 진행했기 때문에, 이후 단계부터 진행하면 된다.
우리가 특정한 라우트 핸들러에서 ValidationPipe를 사용하려 할 때마다 2단계부터 4단계를 매번 반복할 것이다.

3.1. DTO(Data Transfer Object) 클래스 생성하기

우리는 요청 본문이 가져야 할 다양한 속성을 설명하는 클래스를 만들어야 한다.
NestJS에서는 이를 DTO(Data Transfer Object)라고 부른다.
DTO = 데이터 전송 객체
DTO를 사용하면 요청 데이터의 구조를 명확하게 정의할 수 있으며, 유효성 검사와 타입 검사를 쉽게 수행할 수 있다.
messages 폴더 안에 dtos라는 새 폴더를 생성한 뒤, 그 안에 create-message.dto.ts 라는 파일을 생성한 뒤 다음과 같이 클래스를 생성한다.
그리고 이 클래스 안에는 POST 요청 핸들러가 받을 것으로 예상하는 모든 속성들을 기술한다.
// dtos/create-message.dto.ts export class CreateMessageDto { content: string; }
TypeScript
복사
content: 메시지의 본문을 나타내는 필드이며, 반드시 문자열이어야 한다.

3.2. 검증 규칙 추가하기

class-validator라는 이름의 라이브러리를 사용해서 클래스 자체에 검증 규칙을 추가한다.
// dtos/create-message.dto.ts import { IsString } from 'class-validator'; export class CreateMessageDto { @IsString() content: string; }
TypeScript
복사
이렇게 해주면 우리가 CreateMessageDto의 인스턴스를 생성할 때마다 이 content 속성이 number나 undefined, null 등이 아니라 실제 문자열인지 확인할 수 있게 된다.

3.3. DTO를 요청 핸들러에 적용하기

이제 생성한 CreateMessageDto를 컨트롤러에서 사용할 수 있도록 수정하자.
import { Controller, Get, Post, Body, Param } from '@nestjs/common'; import { CreateMessageDto } from './dtos/create-message.dto'; @Controller('messages') export class MessagesController { @Post() createMessage(@Body() body: CreateMessageDto) { console.log(body); } }
TypeScript
복사

3.4. 유효성 검사 테스트

이제 유효성 검사가 제대로 동작하는지 확인해보자.

정상적인 요청

POST http://localhost:3000/messages content-type: application/json { "content": "Hello, NestJS!" }
JSON
복사
정상적으로 동작하며, console.log(body);를 통해 { "content": "Hello, NestJS!" }가 출력된다.
HTTP/1.1 201 Created X-Powered-By: Express Date: Tue, 11 Mar 2025 08:34:19 GMT Connection: close Content-Length: 0
JSON
복사

유효하지 않은 요청 (content가 숫자일 경우)

POST http://localhost:3000/messages content-type: application/json { "content": 123 }
JSON
복사
400 Bad Request 응답이 반환되며, 다음과 같은 오류 메시지가 출력된다.
HTTP/1.1 400 Bad Request X-Powered-By: Express Content-Type: application/json; charset=utf-8 Content-Length: 79 ETag: W/"4f-Qdav3CHrWuLq389QYvm9FiaGfE0" Date: Tue, 11 Mar 2025 08:35:54 GMT Connection: close { "statusCode": 400, "message": [ "content must be a string" ], "error": "Bad Request" }
JSON
복사

4. 검증 과정 심층 분석

4.1. DTO(Data Transfer Object) 심층 분석

DTO는 NestJS에서만 사용되는 게 아니라 다른 언어나 프레임워크에서도 많이 사용한다.
DTO는 두 곳 사이에 정보나 데이터를 옮기는 역할을 하고, 흔히 네트워크 요청 형태로 옮기게 된다.
데이터 전송 객체에는 보통 어떠한 기능도 연계되어 있지 않고, 그냥 몇 가지 속성을 나열하는 단순한 클래스 형태이다.
그래서 우리는 데이터 전송 객체를 요청 안에서 전송되고 있는 데이터가 어떤 형태인지를 아주 명확히 설명하는 객체라고 생각할 수 있다.

4.2. 검증 규칙 심층 분석

class-transformer 도입 배경

NestJS와 같은 TypeScript 기반 프레임워크에서는 DTO 클래스를 사용하여 요청 데이터를 검증하고 변환하는 것이 일반적이다.
하지만 요청 본문에서 받은 데이터는 기본적으로 일반 객체(Plain Object) 형태이며, 이 데이터를 우리가 정의한 클래스 객체(Class Object)로 변환하는 과정이 필요하다.
문제점
1.
일반 객체와 클래스 객체의 차이
일반 객체는 단순한 JavaScript 객체이며, 메서드나 추가적인 동작을 포함하지 않는다.
클래스 객체는 특정 클래스를 기반으로 생성되며, 메서드와 추가적인 속성을 가질 수 있다.
2.
유효성 검사가 올바르게 동작하지 않을 수 있다.
class-validator와 같은 라이브러리는 클래스 객체에서 동작하도록 설계되어 있다.
하지만, HTTP 요청에서 받은 데이터는 기본적으로 일반 객체이므로, 유효성 검사가 정상적으로 수행되지 않을 수 있다.
3.
객체 변환 과정이 필요하다.
수동으로 변환하는 것은 비효율적이며, 유지보수에 어려움이 있다.
class-transformer의 역할
일반 객체 → 클래스 객체 변환 (plainToInstance)
객체 속성 변환 및 매핑 (@Transform())
자동 직렬화 및 역직렬화 지원

class-validator 도입 배경

NestJS에서 class-validator를 사용하는 이유는 요청 데이터의 유효성을 검사하고 입력값을 안전하게 보호하기 위해서다.
유효성 검사의 필요성
웹 어플리케이션에서는 클라이언트에서 서버로 다양한 데이터가 전달된다.
하지만, 클라이언트에서 들어오는 데이터가 항상 올바른 형식이라고 보장할 수는 없다.
예시
1.
예상치 못한 입력 값으로 인한 오류 방지
예: userId가 숫자여야 하지만 문자열 “abc”가 입력됨
2.
비즈니스 로직 보호
예: 회원 가입 시 password가 너무 짧으면 보안 문제 발생
3.
데이터베이스 무결성 유지
예: 필수 필드가 누락된 상태로 데이터가 저장될 경우 문제 발생
4.
보안 강화 (SQL Injection, XSS 방어)
예: 사용자 입력값을 검증하여 악성 스크립트 방지
기존 검증 방식의 문제점
일반적으로 데이터 검증은 수동으로 진행할 수 있다.
if (!body.content || typeof body.content !== 'string') { throw new Error('content는 문자열이어야 합니다.'); } if (!body.userId || isNaN(body.userId)) { throw new Error('userId는 숫자여야 합니다.'); }
Python
복사
문제점은 매 요청마다 검증 로직을 반복해야 하고, 검증 코드가 많아지면 가독성이 떨어진다.
또한, 여러 곳에서 동일한 검증 로직을 재사용하기 어렵다.
class-validator의 역할
데코레이터 기반의 선언형 유효성 검사
자동 유효성 검사 (ValidationPipe와 함께 사용)
데이터 검증 실패 시 자동 예외 처리

전체적인 검증 과정 정리

위 다이어그램은 ValidationPipe를 이용한 전체적인 검증 과정을 보여주고 있다.
1.
먼저 class-transformer 패키지를 자동으로 사용해서 유입되는 요청에 있는 본문을 DTO 클래스의 인스턴스로 변환한다. (일반 JSON 객체 → 클래스 객체)
2.
이어서 class-validator를 사용하여 인스턴스에 있는 다양한 속성들을 검증한다.
3.
검증이 실행되면 검증 오류가 발생했는지 확인하고 오류가 있었다면 즉시 응답을 요청자에게 반환하고, 그렇지 않다면 우리가 컨트롤러 안에 정의한 요청 핸들러에게 그 요청을 전달한다.

5. (심화) 타입 정보가 JavaScript에서도 보존되는 이유

class CreateMessageDto { content: string; } @Post() createMessage(@Body() body: CreateMessageDto) { console.log(body); }
TypeScript
복사
위 코드에서 body의 타입은 CreateMessageDto이다.
그런데 TypeScript는 컴파일 후 JavaScript로 변환될 때 타입 정보를 제거하는데, 어떻게 NestJS는 런타임에도 CreateMessageDto의 타입 정보를 알 수 있을까?
NestJS는 reflect-metadata 라이브러리를 사용해 런타임에 타입 정보를 유지한다.
tsconfig.json 설정을 보면 다음과 같은 두 개의 속성이 켜져있을 것이다.
"emitDecoratorMetadata": true, "experimentalDecorators": true
TypeScript
복사
이 두 개의 속성이 켜져있다면 NestJS는 데코레이터를 활용하여 클래스와 메서드의 타입 정보를 런타임에 유지할 수 있다.
emitDecoratorMetadata: true 설정은 클래스의 타입 정보를 메타데이터로 저장하도록 TypeScript에 지시한다.
experimentalDecorators: true 설정은 데코레이터 문법을 활성화하여 클래스 및 프로퍼티에 추가 정보를 부여할 수 있도록 한다.
// TypeScript @Post() createMessage(@Body() body: CreateMessageDto) { console.log(body); } // JavaScript __decorate([ (0, common_1.Post)(), __param(0, (0, common_1.Body)()), __metadata("design:type", Function), __metadata("design:paramtypes", [create_message_dto_1.CreateMessageDto]), __metadata("design:returntype", void 0) ], MessagesController.prototype, "createMessage", null);
TypeScript
복사
실제로 dist 폴더에서 createMessage 함수가 저장된 컨트롤러의 JavaScript 파일을 확인하면, TypeScript의 데코레이터와 타입 정보가 Reflect 메타데이터 API를 사용하여 변환된 형태로 존재한다.
위 코드를 보면, NestJS는 __metadata("design:paramtypes", [CreateMessageDto]) 부분을 활용하여 CreateMessageDto 타입을 유지한다.
즉, NestJS는 런타임에서 reflect-metadata를 사용하여 DTO 클래스의 타입 정보를 유지하고, ValidationPipe 등의 기능에서 이를 활용한다.