2024. 11. 4. 20:19ㆍBACKEND
Nest.js란
위 이미지는 구글 트렌드에서 나오는 Nest.js의 최근 5년간의 관심 변화량이다.
Nest.js는 Node.js의 서버 애플리케이션 프레임워크로, TypeScript를 기본적으로 사용하고, 모듈러 아키텍처와 의존성 주입(DI)을 통해 유지보수성과 확장성이 높은 애플리케이션을 만들 수 있도록 설계되었고
Angular에서 영감을 받아 비슷한 구조와 패턴을 따른다고 한다.
Node로 백엔드 개발을 할 때 많은 선택을 받았다는 것을 알고 있었고 사용 해보고 있다.
Angular에서 많은 철학적 영향을 받았다고 하는데 나는 Nest.js를 전혀 모르는 상태에서 코드를 보았을 때 Spring boot와 유사한 느낌을 많이 받았다.
Javascript는 너무 자유롭고 Standard를 찾기 어렵다는 생각이 많이 든다. 나는 개인적으로 잘하는 사람을 적극적으로 따라하는 것이 매우 가성비가 좋으면서도 빠르게 성장 할 수 있는 방법이라고 생각한다.
Nest.js를 왜 써야되냐는 의문을 가진 동료를 만난적이 있는데 개인적인 생각으로 나는 이렇게 잘 만들어진 프레임워크를 사용하는 것 자체가 결국 SOLID 원칙을 지키는 것에 가까워지는 것이며 안티패턴을 제거하고 우리가 제대로 인지하지 못하는 나쁜 개발스타일과 강한 의존성 결합 등에 문제를 해결해준다고 생각한다. Spring이라는 프레임워크가 유명해지고 인기가 많아지는 것 역시 비슷한 이유인 듯 싶다. 시장 점유율이 높은 프레임워크를 사용하는 것 자체가 많은 개발자들이 선택한 것을 나도 선택하게 되는 일이고 우리가 알지 못할 뿐 뛰어난 개발자들의 가이드를 따르는 행위라고 생각 한다.
데코레이터의 개념
Nest.js에서 자주 사용되는 핵심적인 데코레이터들은 컨트롤러, 라우트 핸들러, 서비스, 종속성 주입 등의 다양한 역할을 수행 한다.. 이러한 데코레이터들을 통해 개발자는 간단하고 직관적으로 기능을 정의하고 모듈화된 애플리케이션을 구축할 수 있게 된다. 아래는 Nest.js에서 가장 중요하고 자주 사용되는 주요 데코레이터와 그 역할에 대한 설명이다
1. @Controller()
- 역할: 컨트롤러 클래스를 정의, 이 데코레이터는 클래스를 요청 처리의 진입점으로 정의하고, 요청이 들어오면 특정 경로에 대한 처리를 담당 한다.
- 사용 예:
- @Controller('users'): 기본 경로를 /users로 설정
- @Controller('users') export class UsersController { // '/users' 경로의 요청을 처리. }
2. @Get(), @Post(), @Put(), @Delete()
- 역할: 특정 HTTP 메소드(GET, POST, PUT, DELETE 등)에 대한 라우트 핸들러를 정의. 이 데코레이터들은 해당 메소드 요청에 응답하는 핸들러 메소드를 연결 한다.
- 사용 예:
- @Get(): GET 요청을 처리하는 메소드를 지정 한다.
- @Post(): POST 요청을 처리하는 메소드를 지정 한다.
- @Controller('users') export class UsersController { @Get() findAll() { return '모든 사용자'; } @Post() create() { return '사용자 생성'; } }
3. @Param(), @Query(), @Body()
- 역할: 요청의 매개변수를 가져오는 데 사용 된다.
- @Param(): URL 경로에서 매개변수를 추출.
- @Query(): URL 쿼리스트링에서 매개변수를 추출.
- @Body(): 요청 본문에서 데이터를 추출.
- 사용 예:
- @Param('id'): URL 경로에서 id 값을 추출 한다.
- @Body(): 요청 본문 데이터를 가져와 객체로 변환 한다.
- @Get(':id') findOne(@Param('id') id: string) { return `ID가 ${id}인 사용자`; } @Post() create(@Body() createUserDto: CreateUserDto) { return `사용자 생성: ${createUserDto}`; }
4. @Injectable()
- 역할: 서비스 클래스를 정의하여 의존성 주입(Dependency Injection)이 가능하게 한다. 이 데코레이터를 사용하면 Nest.js에서 해당 클래스를 자동으로 주입하여 사용할 수 있다.
- 사용 예:
- @Injectable(): 서비스나 다른 모듈에서 사용할 수 있도록 의존성 주입이 가능하게 만든다.
- @Injectable() export class UsersService { // 비즈니스 로직을 포함하는 서비스 클래스 }
5. @Inject()
- 역할: 특정 토큰을 통해 의존성 주입을 수행할 때 사용. 일반적으로 의존성 주입이 자동으로 처리되지만, 명시적으로 토큰을 지정해야 할 경우 사용 된다.
- 사용 예:
- @Inject('TOKEN'): 특정 토큰으로 의존성을 주입.
- constructor(@Inject('TOKEN') private readonly service: SomeService) {}
6. @UseGuards()
- 역할: 인증 및 권한 부여 로직을 추가하는 데 사용되고 Guard는 요청이 라우트 핸들러에 도달하기 전에 인증이나 인가를 확인 한다.
- 사용 예:
- @UseGuards(AuthGuard): 요청이 특정 핸들러에 도달하기 전에 AuthGuard를 사용해 인증 절차를 거친다.
- @UseGuards(AuthGuard) @Get('profile') getProfile() { return '프로필 정보'; }
7. @UseInterceptors()
- 역할: 요청/응답에 대해 로직을 가로채어 추가적인 처리를 할 수 있다. 예를 들어, 로깅, 변환, 캐싱 등을 위해 사용 된다.
- 사용 예:
- @UseInterceptors(LoggingInterceptor): 해당 핸들러 전후로 로깅 작업을 수행합니다.
- @UseInterceptors(LoggingInterceptor) @Get() findAll() { return '모든 사용자'; }
8. @Module()
- 역할: 모듈을 정의하여 애플리케이션의 구성 요소(컨트롤러, 서비스 등)를 그룹화 한다. 모듈은 Nest.js 애플리케이션의 기본 구성 요소이다.
- 사용 예:
- @Module(): 이 모듈 내에서 사용할 컨트롤러와 프로바이더(서비스)를 정의한다, 이렇게 Module에 정의한 Controller와 Service도 의존성에 등록된 것이라고 볼 수 있을 것 같다 Springboot로 치면 Bean으로 등록되었다고 표현하는 것 처럼 Bean대신 Provider라는 용어로 사용되는 것 같다 즉, Nest.js에서 프로바이더라는 표현은 Spring boot에서의 빈으로 생각하면 될 것 같다.
- @Module({ controllers: [UsersController], providers: [UsersService], }) export class UsersModule {}
요약
Nest.js는 여러 데코레이터를 제공하여 직관적이고 선언적인 방식으로 애플리케이션을 정의하고 구조화할 수 있게 해준다. 이를 통해 다음과 같은 장점이 있습니다:
- @Controller(), @Get(), @Post() 등: 요청 핸들러 및 경로 정의.
- @Param(), @Query(), @Body(): 요청의 매개변수를 쉽게 가져옴.
- @Injectable(), @Inject(): 의존성 주입 및 서비스 관리.
- @UseGuards(), @UseInterceptors(): 인증, 권한 및 요청 전후 처리를 위한 로직 구현.
이러한 데코레이터들은 개발자에게 가독성이 높고 유지보수하기 쉬운 코드를 작성할 수 있게 해준다. Nest.js는 이와 같은 데코레이터들을 통해 모듈화, 확장성, 테스트 가능성을 고려한 엔터프라이즈 애플리케이션 개발을 지원한다.
Nest CLI
Nest.js는 Module 이라는 개념으로 컨트롤러와 서비스 테스트코드를 관리한다.
AppModule은 필수적으로 존재해야 한다, nest.js는 새로운 것을 만들거나 작업을 할 때 cli로 생성하도록 자동화 된 부분이 많다.
새로운 module을 만들고 싶다면 nest g mobule boards 를 입력하면 된다. 디렉토리를 생성하고 자동으로 app.moduble.ts에 추가까지 된다.
컨트롤러를 생성 할 때에도 nest g controller boards --no-spec 명령어로 생성 할 수 있다 테스트파일인 spec을 생략 할 수 있다.
위 예제처럼 terminal에서 alias를 만들어서 입력한 이름으로 Module, Controller, Service를 만들게 하면 편하다.
의존성 주입
import { Controller, Get } from '@nestjs/common';
import { BoardsService } from './boards.service';
@Controller('boards')
export class BoardsController {
boardsService: BoardsService;
constructor(boardsService: BoardsService) {
this.boardsService = boardsService;
}
}
위 코드를 보면 java에서 bean을 주입받고 싶으면 @requiredargsconstructor 어노테이션을 사용해야 하지만 nest에서는 별도의 데코레이터가 없어도 자동으로 주입받게 된다. 만약 이름을 명시해야 하는 경우
@Inject를 사용하면 된다.
import { Controller, Get } from '@nestjs/common';
import { BoardsService } from './boards.service';
@Controller('boards')
export class BoardsController {
// boardsService: BoardsService;
constructor(private boardsService: BoardsService) {
this.boardsService = boardsService;
}
}
위 코드를 보면 boardsService를 생성자에서 바로 처리 할 수 있다 private 접근제어자를 붙이면 타입스크립트에서 자동으로 인수를 프로퍼티로 처리하고 주입을 받게 처리한다. 마치 java에서 final을 붙이고 @requiredargsconstructor 어노테이션을 사용해야 주입받는다고 정해놓은 것과 비슷한 개념일 것 같다.
CRUD 해보기
Prisma 혹은 Type ORM을 사용해 ORM방식으로 개발을 하는 것이 요즘의 추세인 것 같다. 여기서는 Type ORM을 사용하는 예제로 작성해 보겠다. Model을 정의 할 때에는 Class를 이용하거나 Interface를 이용하게 된다. 간단한 차이는 아래와 같다.
- Interface → 변수의 타입만을 체크한다. 런타임에 영향이 없고 컴파일 단계에서의 타입체크가 주 목적이다.
- Classes → 변수의 타입도 체크하고 인스턴스를 생성 할 수도 있다. 런타임 인스턴스를 생성하고 구현 및 동작을 포함 할 수 있다 ORM에서 데이터베이스와의 셀제 매핑을 위해 사용하는 것이 일반적이다.
board.model.ts
export interface Board {
id: string;
title: string;
description: string;
status: BoardStatus
}
export enum BoardStatus {
PUBLIC = 'PUBLIC',
PRIVATE = 'PRIVATE'
}
위 코드처럼 모델을 생성하자.
DTO 사용
DTO를 만들어 요청하는 값과 응답하는 값을 따로 관리하는 것이 좋다.
실제 어플리케이션을 개발 할 때에는 backend와 front에서 사용하는 필드가 다를 가능성이 놓고 개발하는 과정에서 프로퍼티가 추가되거나 없어져야 하는 경우가 많기 때문에 DTO를 만드는게 귀찮다고 생각 할 수 있지만 DTO를 만들어 관리하는 것이 나은 것 같다.
Nest.js에서 DTO는 인터페이스와 클래스를 모두 사용 할 수 있지만 클래스를 사용하는 것이 더 추천된다고 한다. 런티암 시점에 작동하기 때문에 파이트와 같은 기능을 이용 할 때 더 유용하다고 한다.
export class CreateBoardDto{
title: string;
description: string;
}
boards.controller.ts
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
import { BoardsService } from './boards.service';
import { Board, BoardStatus } from './board.model';
import { CreateBoardDto } from './dto/CreateBoardDto';
@Controller('boards')
export class BoardsController {
constructor(private boardsService: BoardsService) {
this.boardsService = boardsService;
}
@Post()
createBoard(@Body() createBoardDto: CreateBoardDto): Board {
return this.boardsService.createBoard(createBoardDto);
}
@Get()
getAllBoards(): Board[]{
return this.boardsService.getAllBoards();
}
@Get("/:id")
getBoard(@Param("id") id: string): Board{
return this.boardsService.getBoardById(id);
}
@Patch("/:id")
updateBoardStatus(@Param("id") id: string, @Body('status') status: BoardStatus): Board{
return this.boardsService.updateBoardStatus(id, status);
}
@Delete("/:id")
deleteBoard(@Param("id") id: string): void{
return this.boardsService.deleteBoardById(id);
}
}
boards.service.ts
import { Injectable } from '@nestjs/common';
import {v1 as uuid} from 'uuid'
import { Board, BoardStatus } from './board.model';
import { CreateBoardDto } from './dto/CreateBoardDto';
@Injectable()
export class BoardsService {
private boards: Board[] = [];
createBoard(createBoardDto: CreateBoardDto): Board {
const {title, description} = createBoardDto;
const board: Board = {
id: uuid(),
title,
description,
status: BoardStatus.PUBLIC
}
this.boards.push(board);
return board;
}
getAllBoards(): Board[]{
return this.boards;
}
getBoardById(id: string): Board {
return this.boards.find(board => board.id === id);
}
updateBoardStatus(id: string, status: BoardStatus): Board {
const board = this.getBoardById(id);
board.status = status;
return board
}
deleteBoardById(id: string): void {
this.boards = this.boards.filter(board => board.id !== id);
}
}
단순한 내용이라 개발 경험이 있다면 쉽게 이해 할 것 같다. 지금은 DB를 사용하지 않고 메모리에 변수로 값을 넣도록 했다.
Pipe
nest.js에서는 파이프라는 개념이 있다. 파이프란 2가지의 상황에서 사용 된다.
- Data Transformation
- Data Validation
springboot에서 요청값의 유효성을 확인하기 위해 @Validation을 사용하거나 HTTP 요청값에 대한 jon schema로 검사를 하는 등의 행위에 해당한다. String 7로 오는 값을 Integer 7로 변형하거나 전달한 값의 길이가 10글자 이하여야 하는데 유효한지를 검사하는 등의 기능을 위해 사용된다.
Pipe를 사용하는 방법은 크게 세가지로 나뉜다.
Parameter-level Pipes (메서드)
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id')
getUserById(@Param('id', ParseIntPipe) id: number) {
return `유저 아이디: ${id}`;
}
}
Handler-level Pipes (컨트롤러)
import { Controller, Get, Param, UsePipes, ValidationPipe } from '@nestjs/common';
@Controller('users')
@UsePipes(new ValidationPipe())
export class UsersController {
@Get(':id')
getUserById(@Param('id') id: number) {
return `유저 아이디: ${id}`;
}
@Get()
getAllUsers() {
return '모든 유저 리스트';
}
}
Global Pipes (전역)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 전역 파이프 설정
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
이와 같은 방식으로 파이프를 사용해 애플리케이션의 다양한 요구 사항에 맞게 데이터를 변환하고 검증할 수 있다.
파이프를 사용 한다는 것 자체가 애플리케이션에 들어오는 요청에 대해 데이터 검증과 형변환을 수행하는 것이기 때문에 결국 파이프는 컨트롤러를 위한 것이라고 볼 수 있겠다.
(메서드 단위에서의 파이프도 결국 parameter를 검증하기 위한 용도가 대부분 일 것이다.)
Bulit-in Pipes
- ValidationPipe
- ParseIntPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- DefaultValuePipe
Nest.js에서는 이미 기본적으로 사용 할 수 있게 만들어 놓은 6개의 파이프가 있다.
1. ValidationPipe
- 역할: 들어오는 요청 데이터를 검증하는 파이프이다.
- 사용 방법: DTO와 함께 사용하여 클라이언트가 보낸 데이터가 사전에 정의된 규칙을 따르는지 확인 한다.
- 예시: DTO에서 특정 필드를 필수로 지정하거나 숫자, 문자열 등 타입에 맞는 데이터를 요구할 때 사용됩니다. 잘못된 데이터가 들어오면 자동으로 오류를 발생 시킨다.
- @Post() createUser(@Body(new ValidationPipe()) createUserDto: CreateUserDto) { // 유효성 검사가 자동으로 수행됨 return createUserDto; }
2. ParseIntPipe
- 역할: 문자열 데이터를 정수로 변환하는 파이프이다.
- 사용 방법: 요청의 파라미터가 숫자여야 할 때, 이를 정수로 자동 변환해준다.
- 예시: URL 파라미터로 받은 id 값을 숫자로 변환해야 하는 경우 사용합니다. 만약 id가 숫자가 아닐 경우 400 오류를 반환 한다.
- @Get(':id') getUserById(@Param('id', ParseIntPipe) id: number) { return `유저 아이디: ${id}`; }
3. ParseBoolPipe
- 역할: 문자열 데이터를 불리언(boolean) 타입으로 변환하는 파이프이다.
- 사용 방법: 요청 데이터에 "true"나 "false" 같은 문자열이 들어왔을 때, 이를 불리언 값으로 변환.
- 예시: URL 쿼리 파라미터가 불리언으로 해석되어야 할 때 사용 한다.
- @Get() getItems(@Query('isActive', ParseBoolPipe) isActive: boolean) { return `활성 상태: ${isActive}`; }
4. ParseArrayPipe
- 역할: 요청 데이터를 배열로 변환하고 배열의 타입을 검증하는 파이프 이다.
- 사용 방법: 배열로 기대되는 데이터를 검증하고, 특정 타입인지 확인하고 싶을 때 사용 한다.
- 예시: 쿼리 파라미터로 [1,2,3] 같은 배열을 받아야 할 때 사용 된다.
- @Get() getItems(@Query('ids', new ParseArrayPipe({ items: Number })) ids: number[]) { return `아이템 IDs: ${ids}`; }
5. ParseUUIDPipe
- 역할: 요청 데이터를 UUID 형식으로 변환하고 검증하는 파이프이다.
- 사용 방법: 특정 파라미터가 UUID 형식이어야 할 때 사용 한다.
- 예시: 리소스를 식별하는 ID가 UUID 형식일 때 사용하여 유효한 형식인지 검증 한다.
- @Get(':uuid') getUserByUUID(@Param('uuid', ParseUUIDPipe) uuid: string) { return `유저 UUID: ${uuid}`; }
6. DefaultValuePipe
- 역할: 요청 데이터에 값이 없을 경우 기본값을 설정해주는 파이프이다.
- 사용 방법: 특정 파라미터에 값이 없으면 기본값을 지정하고 싶을 때 사용 한다.
- 예시: 쿼리 파라미터가 없을 때 기본값을 설정해 원하는 값으로 사용하게 한다.
- @Get() getItems(@Query('limit', new DefaultValuePipe(10)) limit: number) { return `아이템 개수: ${limit}`; }
요약
- ValidationPipe: 들어오는 요청 데이터를 DTO 규칙에 맞게 검증.
- ParseIntPipe: 문자열을 정수로 변환하며, 유효하지 않으면 오류를 반환.
- ParseBoolPipe: 문자열 "true" 또는 "false"를 **불리언(boolean)**으로 변환.
- ParseArrayPipe: 요청 데이터를 배열로 변환하고 타입 검증.
- ParseUUIDPipe: 요청 데이터를 UUID 형식으로 검증.
- DefaultValuePipe: 요청 데이터가 없을 경우 기본값을 설정.
이 파이프들을 적절히 활용하여 NestJS 애플리케이션에서 요청 데이터를 효과적으로 검증하고 변환할 수 있다. 이를 통해 데이터 무결성을 유지하고, 서버에서 처리할 데이터를 적절하게 전처리하는 것이 가능해 진다.
import { Injectable, UsePipes, ValidationPipe } from '@nestjs/common';
import {v1 as uuid} from 'uuid'
import { Board, BoardStatus } from './board.model';
import { CreateBoardDto } from './dto/CreateBoardDto';
@Injectable()
export class BoardsService {
private boards: Board[] = [];
@UsePipes(ValidationPipe) // 파이프를 사용 한다고 데코레이터 사용
createBoard(createBoardDto: CreateBoardDto): Board { // DTO내부에 @IsEmpty사용
const {title, description} = createBoardDto;
const board: Board = {
id: uuid(),
title,
description,
status: BoardStatus.PUBLIC
}
this.boards.push(board);
return board;
}
}
import { IsNotEmpty } from 'class-validator';
export class CreateBoardDto{
@IsNotEmpty() // 데코레이터 사용
title: string;
@IsNotEmpty()
description: string;
}
실제 컨트롤러에서 요청 값에 대한 유효성 검사를 하고 싶다면 위 코드와 같이 적용하면 된다.
'BACKEND' 카테고리의 다른 글
Nest, Adonis, Express 비교 하기 (1) | 2024.12.09 |
---|---|
Nest.js 공부하기 (2) (0) | 2024.11.27 |
Prisma 공부하기 (3) | 2024.10.20 |
SOAP 프로토콜 사용하기 (0) | 2024.05.05 |
SpringBoot + Next.js 프로젝트 회고 (1) | 2024.01.11 |