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
복사
Related Posts
Search