Blog

[NestJS] 4. Pipe를 통한 유효성 체크와 예외처리

Category
Author
citeFred
citeFred
PinOnMain
1 more property
NestJS, TypeORM 이해하기
Table of Content

1. 유효성 검사에 대하여

올바르지 않은 데이터를 걸러내고 보안을 유지하기 위한 기본 검사
유효성 검사와 그에 대한 예외 처리는 백엔드와 프론트엔드 모두에서 중요하게 다뤄져야 한다.
→ 이는 애플리케이션의 무결성보안성을 유지하는 데 필수적
프론트엔드에서의 유효성 검사
사용자 경험 (UX) 향상
사용자가 잘못된 데이터를 입력했을 때 즉시 피드백을 제공하여, 오류를 실시간으로 수정 → UX 향상
비용 절감
잘못된 데이터가 서버로 전송되는 것을 막아, 불필요한 서버 요청 최소화
기본적인 보안
가장 일반적인 입력 오류를 초기에 차단
악의적인 사용자가 기본적인 유효성 검사를 우회하는 것을 차단
다만, 프론트엔드 유효성 검사는 클라이언트 측에서 실행되므로, 보안 목적으로만 의존해서는 안됨
백엔드에서의 유효성 검사
데이터 무결성
데이터베이스에 저장되기 전 데이터를 최종적으로 확인하여 데이터베이스의 무결성을 보장
예를 들어, 이메일 형식이 맞지 않는 데이터를 저장하는 것, null 값 등
보안성 강화
악의적인 사용자가 프론트엔드에서 유효성 검사를 우회할 수 있기 때문에, 백엔드에서의 검사는 필수
모든 입력 데이터가 서버에서 한 번 더 검증되어야 함
SQL 인젝션, XSS 등 다양한 보안 취약점을 방어
시스템 안정성
예상치 못한 입력으로 인한 시스템 오류를 방지
예를 들어, 백엔드에서 예상치 못한 형식의 데이터를 처리하지 못해 서버의 중단 등 방지
NestJS의 유효성 검사 라이브러리
Nestjs에서는 class-validator라는 라이브러리를 통해 사용
데코레이터(@) 형태로 제약 조건을 달아줘서 쉽게 검증할 수 있도록 돕는 라이브러리
DTO 에서 데이터를 검증하면, 애플리케이션의 안전성과 유연성을 크게 향상시킬 수 있음
애너테이션 기반, DTO에서 작성, 컨트롤러에 @Body() @UsePipes(ValidationPipe) 또는 간단한 전역 설정을 통해 설정하는 등 유사한 형태를 가짐

2. 파이프(Pipe) 개념과 종류

Pipe란?
@Injectable 데코레이터를 가지고 있으며 PipeTransfrom을 구현한 class
주요 기능
데이터 변환(Transformation): 예를 들어, 문자열을 숫자로 변환하거나, 요청 데이터를 특정 DTO 객체로 변환하는 등 데이터를 원하는 형식으로 변환할 수 있음
데이터 검증(Validation): 요청된 데이터가 특정 조건을 만족하는지 검사하여, 유효하지 않은 데이터는 요청을 거부하고 예외를 발생 시킬 수 있음
흐름별 설명으로 이해하기
파이프의 역할: 파이프는 컨트롤러의 메서드(라우트 핸들러)가 받는 입력 데이터(클라이언트 요청)를 다루는 기술
작동 시점: NestJS는 라우트 핸들러 메서드가 실행되기 바로 전에 파이프를 거치게 됨. 이때 파이프는 메서드가 받을 데이터를 먼저 확인하고, 그 데이터를 필요에 따라 변환하거나 검증
변환과 검증: 예를 들어, 문자열로 들어온 숫자 데이터를 실제 숫자로 변환하거나, 데이터가 규칙에 맞는지 확인하는 유효성 체크 실행
타입 : ”1”과 1은 string, number로 타입이 다름
검증 : email 형식, 패스워드의 특수문자 필수 포함 등
메서드 호출: 이후 파이프가 처리한 데이터를 메서드(라우트 핸들러)에 넘겨주고, 이 데이터를 사용해 비지니스 로직을 수행
적용 범위별 파이프의 종류(범위가 좁은 것부터~큰 것)
Parameter Level - 컨트롤러 메서드의 특정 매개변수에 대해 파이프를 적용
@Get('/:id') findById(@Param('id', ParseIntPipe) id: number) { ... }
TypeScript
복사
Handler Level - 특정 라우트 핸들러(메서드)에서 사용
@Post() @UsePipes(ValidationPipe) saveCatInfo(@Body() req: CreateCatDto) { ... }
TypeScript
복사
Controller Level - 특정 컨트롤러의 핸들러 모두 적용
@Controller('cats') @UsePipes(ValidationPipe) export class CatsController { @Get() ... @Post() ... }
TypeScript
복사
Global Level - 애플리케이션이 bootstrap되는 main.ts에서 설정 전역 설정
async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()) await app.listen(3000); } bootstrap();
TypeScript
복사
위처럼 main.ts 파일에서 전역 파이프를 직접 설정할 경우 유지보수성이 떨어짐
NestJS 내장 파이프(Built-in pipes)
ValidationPipe : 유효성 검사**
ParseIntPipe : 정수로 변환
ParseFloatPipe : 실수로 변환
ParseBoolPipe : 불리언으로 변환
ParseArrayPipe : 배열로 변환
ParseUUIDPipe : UUID로 변환
ParseEnumPipe : 열거형으로 변환
DefaultValuePipe : null 인경우 기본값 설정
ParseFilePipe : 파일 형식 또는 크기 검증

3. 내장 파이프 사용해보기

create-board.dto.ts
null 값을 허용하지 않는 @IsNotEmpty()를 각 필드마다 추가해준다.
import { IsNotEmpty } from "class-validator"; export class CreateBoardDto { @IsNotEmpty() // null 값 체크 author: string; @IsNotEmpty() title: string; @IsNotEmpty() contents: string; }
TypeScript
복사
boards.controller.ts
핸들러 레벨의 파이프를 사용
@UsePipes(ValidationPipe)를 핸들러 메서드 상단에 작성해준다.
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, UsePipes, ValidationPipe } from '@nestjs/common'; import { BoardsService } from './boards.service'; import { Board } from './board.entity'; ... @Controller('api/boards') export class BoardsController { // 생성자 주입(DI) constructor(private boardsService: BoardsService){} // 게시글 작성 기능 @Post('/') // PostMapping 핸들러 데코레이터 @UsePipes(ValidationPipe) // 핸들러 레벨 유효성 검사 파이프 설정 createBoard(@Body() createBoardDto: CreateBoardDto) { return this.boardsService.createBoard(createBoardDto) } ... }
TypeScript
복사
요청으로부터 파이프를 거치는 흐름은 다음과 같다.
HTTP 요청 수신(데이터 도착)
→ 요청 본문(body)을 CreateBoardDto 객체로 변환
→ 파이프 실행
→ 유효성 검사(null이 있는지 등 애너테이션별 값 체크 검증)
→ 데이터 규칙에 맞지 않으면 예외 발생 후 클라이언트로 오류 메시지, 상태 코드 전달
→ 데이터 규칙에 맞으면 DTO를 컨트롤러 핸들러 메소드에 전달
POSTMAN으로 테스트 진행

4. 예외 처리

boards.service.ts
잘못된 번호의 게시글을 조회하는 경우 예외 처리
잘못된 번호 또는 경로로 이동하는 것을 제한하기 위해서는 예외처리로 접근을 제어 할 수 있다.
import { Injectable, NotFoundException } from '@nestjs/common'; import { Board } from './board.entity'; import { BoardStatus } from './board-status.enum'; import { CreateBoardDto } from './dto/create-board.dto'; import { UpdateBoardDto } from './dto/update-board.dto'; @Injectable() export class BoardsService { private boards: Board[] = []; // 임시 DB처럼 사용할 배열(로컬 메모리) // 저장되는 데이터 타입 Board[] 배열 ... // 특정 번호의 게시글 조회 getBoardById(id: number): Board { const foundBoard = this.boards.find((board) => board.id == id); if(!foundBoard) { throw new NotFoundException(`Board with ID ${id} not found`); // 메시지 ` = 백틱으로 감싸야 ${id} 동작 } return foundBoard; ... } }
TypeScript
복사
POSTMAN을 통한 테스트
boards.service.ts
잘못된 번호의 게시글을 삭제하려는 경우에도 위 특정 게시글 조회와 기능이 중복되는 부분이 있다.
이처럼 특정 게시글을 조회하는 기능이 기본적으로 사용되는 메서드마다 공통 메서드처럼 활용할 수 있다.
변수 명을 좀 더 직관적으로 모두 foundBoard로 변경
수정 기능 2개도 특별히 객체를 반환할 필요는 없으므로 임시 반환 타입 및 return 삭제(컨트롤러도)
import { Injectable, NotFoundException } from '@nestjs/common'; import { Board } from './board.entity'; import { BoardStatus } from './board-status.enum'; import { CreateBoardDto } from './dto/create-board.dto'; import { UpdateBoardDto } from './dto/update-board.dto'; @Injectable() export class BoardsService { private boards: Board[] = []; // 임시 DB처럼 사용할 배열(로컬 메모리) // 저장되는 데이터 타입 Board[] 배열 ... // 특정 번호의 게시글 조회 getBoardById(id: number): Board { const foundBoard = this.boards.find((board) => board.id == id); if(!foundBoard) { throw new NotFoundException(`Board with ID ${id} not found`); // 메시지 ` = 백틱으로 감싸야 ${id} 동작 } return foundBoard; } ... // 특정 번호의 게시글 삭제 deleteBoardById(id: number): void{ const foundBoard = this.getBoardById(id); this.boards = this.boards.filter((board) => board.id != foundBoard.id); } ... // 특정 번호의 게시글의 일부 수정 updateBoardStatusById(id: number, status: BoardStatus) { const foundBoard = this.getBoardById(id); foundBoard.status = status; } // 특정 번호의 게시글의 전체 수정 updateBoardById(id, updateBoardDto : UpdateBoardDto) { const foundBoard = this.getBoardById(id); const {author, title, contents, status} = updateBoardDto; foundBoard.author = author; foundBoard.title = title; foundBoard.contents = contents; foundBoard.status = status; } }
TypeScript
복사
POSTMAN을 통한 테스트

5. 커스텀 파이프 구현과 적용 레벨 별 구현 해보기

커스텀 파이프
기본적으로 내장된 파이프들은 매우 유용하지만, 특정 요구사항이나 비즈니스 로직을 처리하기에는 제한적일 수 있음
특정 비즈니스 로직 처리 - 특정 필드의 값이 특정 조건을 만족해야 하는 경우, 또는 외부 시스템과의 상호작용이 필요한 경우 등
데이터 변환 및 포맷팅 - 날짜 문자열을 특정 형식으로 변환하거나, 복잡한 객체 구조를 단순화하는 등의 작업이 필요할 때 등
그 외 유효성 검사 강화 용도로 사용
공식 문서에 나와있는 기본적인 골격은 다음과 같다.
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common'; @Injectable() export class BoardStatusValidationPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { return value; } }
TypeScript
복사
게시글 일부(공개상태) 수정에 커스텀 파이프 적용하기
pipes 폴더 생성
board-status-validation.pipe.ts 파일 생성
게시글 공개 상태를 수정하는 로직에서 지정된 PRIVATE, PUBLIC두개의 값 외에 입력값이요청에 포함된 경우를 처리하기 위함
normalizeValue() 메서드를 통해 입력값이 소문자인 경우에도 판별하기 위해 대문자화
혹시나 다른 타입인 가능성을 배제하기 위하여 string으로 반환
isStatusValid() 메서드를 통해 BoardStatus 타입의 status 변수가 위 readonly 로 지정된 statusOptions 에 포함되는지 확인
조건문을 통해 예외 처리 / 로직 진행
false일 경우 위 배열에 없는 값이므로 잘못된 입력에 대한 예외처리, 오류 메시지 반환
true일 경우 비지니스 로직 진행
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; import { BoardStatus } from '../board-status.enum'; @Injectable() export class BoardStatusValidationPipe implements PipeTransform { private readonly statusOptions = [ BoardStatus.PRIVATE, BoardStatus.PUBLIC ] transform(value: any, metadata: ArgumentMetadata) { // 메타데이터는 필수는 아니지만 type 속성을 사용하여 파라미터 타입 확인 가능. console.log('Parameter type:', metadata.type); const status = this.normalizeValue(value); // 입력값 대문자, 문자열string 변환 if (!this.isStatusValid(status)) { // true가 !아니면 (==false면) 예외 처리 throw new BadRequestException(`${value} is not a valid status. Allowed values are: ${this.statusOptions.join(', ')}`); } return status; } private normalizeValue(value: any): string { return value.toUpperCase(); } private isStatusValid(status: string): boolean { return this.statusOptions.includes(status as BoardStatus); } }
TypeScript
복사
boards.controller.ts
이번엔 파라미터 레벨에서 커스텀 파이프를 적용
@Body('status', BoardStatusValidationPipe) 파라미터인 바디 데코레이터에 파이프 클래스를 다음 처럼 작성하면 해당 파이프가 적용 된다.
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, UsePipes, ValidationPipe } from '@nestjs/common'; import { BoardsService } from './boards.service'; import { Board } from './board.entity'; import { CreateBoardDto } from './dto/create-board.dto'; import { BoardStatus } from './board-status.enum'; import { UpdateBoardDto } from './dto/update-board.dto'; import { BoardStatusValidationPipe } from './pipes/board-status-validation.pipe'; @Controller('api/boards') export class BoardsController { // 생성자 주입(DI) constructor(private boardsService: BoardsService){} ... // 특정 번호의 게시글의 일부 수정 @Patch('/:id/status') updateBoardStatusById(@Param('id') id: number, @Body('status', BoardStatusValidationPipe) status: BoardStatus) { this.boardsService.updateBoardStatusById(id, status) } ... }
TypeScript
복사
POSTMAN을 통한 테스트
Global(전역)으로 유효성검사 파이프 설정
모듈화, 유지보수성을 위해서는 위 파라미터레벨, 핸들러레벨, 컨트롤러레벨 파이프 설정은 피하는 것이 좋다.
결국, DTO에 직접 @데코레이터를 작성하기 때문에 전역 설정으로 유효성 체크를 진행하도록 변경하고자 한다.
boards.controller.ts 에서 @UsePipes 핸들러 레벨 파이프 설정을 주석(삭제)처리 해준다.
@Controller('api/boards') export class BoardsController { // 생성자 주입(DI) constructor(private boardsService: BoardsService){} // 게시글 작성 기능 @Post('/') // PostMapping 핸들러 데코레이터 // @UsePipes(ValidationPipe) // 핸들러 레벨 유효성 검사 파이프 설정 -> 글로벌로 변환 createBoard(@Body() createBoardDto: CreateBoardDto): Promise<Board> { return this.boardsService.createBoard(createBoardDto) } ...
TypeScript
복사
루트 경로에 global.module.ts 로 아래와 같이 작성
import { Global, Module, ValidationPipe } from "@nestjs/common"; import { APP_PIPE } from "@nestjs/core"; @Global() @Module({ providers: [ { provide : APP_PIPE, useClass : ValidationPipe, }, ], }) export class GlobalModule {}
TypeScript
복사
app.module.ts 에서 위 글로벌 모듈을 포함시켜 애플리케이션에 적용
import { Module } from '@nestjs/common'; import { BoardsModule } from './boards/boards.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { typeOrmConfig } from './configs/typeorm.config'; import { GlobalModule } from './global.module'; @Module({ imports: [ GlobalModule, TypeOrmModule.forRoot(typeOrmConfig), BoardsModule], }) export class AppModule {}
TypeScript
복사
main.ts 에서 위 의존성 주입 설정된 파이프를 사용하도록 설정
async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); await app.listen(process.env.PORT ?? 3000); } bootstrap();
JavaScript
복사

6. 정규표현식(Regular Expression)

정규표현식이란?
정규 표현식, 또는 정규식은 문자열에서 특정 문자 조합을 찾기 위한 패턴
프로그래밍에서 문자열을 다룰 때, 문자열의 일정한 패턴을 표현하는 일종의 형식 언어
다음 문법과 같이 특정한 기호들을 통해서 규칙이 정해져 있다.
규칙을 이해하고 보면 눈에 들어오지만 처음 보는 경우 암호처럼 느껴질 수 있다.
물론 직접 작성하기도 하지만, 일반적인 사례들은 공식화 되어 있거나 관례적으로 사용하는 것들이 많다.
매우 세밀한 조정을 위해서는 아래 블로그를 통해 문법을 공부하는 것을 추천하며, 일반적인 경우엔 사례를 찾고 보고 읽어낼 수 있을 수준으로 넘어가도록 한다.
아래 북마크를 통해서 정규식을 검증 해볼 수 있다
또한 일반적인 패턴들도 제공하고 있다.
NestJS의 Validation-class의 정규표현식의 사용법
데코레이터 중에 @Matches() 를 통해서 원하는 패턴의 유효성 검사를 진행 할 수 있다.
패스워드를 소문자, 대문자, 특수문자를 포함한 경우로 작성할 수 있도록 하는 경우 다음과 같은 예시로 작성한다.
sample.dto.ts
export class CreateSampleDto { ... @Matches(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, { message: 'Password too weak', }) // 대문자, 소문자, 숫자, 특수문자 포함 password: string; ... }
TypeScript
복사
Search
 | Main Page | Category |  Tags | About Me | Contact | Portfolio