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-validator와 class-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. 검증 규칙 심층 분석
•
우리는 DTO에 검증 규칙을 추가하기 전에 class-validator 패키지와 class-transformer라는 또 다른 패키지를 설치했다.
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 등의 기능에서 이를 활용한다.