기억의 실마리
2023. 1. 24. 15:14

Nest.js

Nest.js는 Node.js기반의 웹  API 프레임워크로서 Express, Fastify 프레임워크를

래핑하여 동작한다. 기본적으로는 Exress를 제공한다.

 

Nest.js의 장점

Node.js는 사용하기에 있어 편리하고 뛰어난 확장성을 가지고 있다.
이러한 특성으로 SW의 품질이 일정하지않고 라이브러리를 찾기위해
사용자가 많은 시간을 할애해야 한다. 반면 Nest.js는 데이터베이스,
ORM, Configuration, 유효성 검사 등 수많은 기능을 기본으로 제공하고 있다.
그러면서도 필요한 라이브러리를 쉽게 설치가능하기 떄문에

Node.js의 장점도그대로 가지고 있다.

 

 

Nest.js의 특징

Angular로부터 영향을 받아 모듈,컴포넌트 기반으로 프로그램을 작성함으로서

재사용성을 높인다. IoC(Inversion of Control), DI(Dependency Injection),

AOP(Aspect Oriented Programming)와 같은 객체지향 개념을 도입했다.

언어는 타입스크립트를 기본으로 채택하고 있다.

 

사용예시

Nest.js Install

npm i -g @nestjs/cli
//인스톨이 잘 되었는지 터미널에 nest를 입력해서 확인해본다.

인스톨이 잘 된 경우 터미널에 nest를 입력시 nest의 리스트가 나오게 된다.

 

nest new Project
//Project이름을 가진 Default create-nest가 생성된다.

프로젝트를 새로 만들어야 할 경우 Nest.js의 createt기능을 사용하여

Default프로젝트를 만들 수 있다.

 

 

src 폴더 구조

main.ts

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({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      //transform은 요청데이터를 필요한 데이터형태로 바꿔준다. 원래는 string으로 받지만 number로 바꿔주고 있다.
    }),
  );
  await app.listen(3000); //run start시에 사용되는 서버포트이다.
}
bootstrap();

 

app.modules.ts

import { Module } from '@nestjs/common';
import { MoviesModule } from './movies/movies.module';
import { AppController } from './app.controller';

@Module({
  imports: [MoviesModule],
  controllers: [AppController],
  providers: [],
})
export class AppModule {}

 

app.controller.ts

import { Controller, Get } from '@nestjs/common';

@Controller('')
export class AppController {
  @Get()
  home() {
    return 'Welcome to my Movie API';
  }
}

src폴더는 Nest.js에서 root가 되는 구조이며 service.ts파일은 현재 사용예시 구조상 필요하지않고

movies폴더 내부에서 실질적인 구현을 대신하고 있으므로  movies폴더 내부에 service.ts파일이 있다.

 

 

movies 폴더구조

dto

dto는 코드를 간결하게 만들어 줄 수 있고 클라이언트로 부터 request를 받을때

유효성 검사를 해주기 위함이다.

 

create-movie.dto.ts

import { IsNumber, IsOptional, IsString } from 'class-validator';

export class CreateMovieDTO {
  @IsString()
  readonly title: string;

  @IsNumber()
  readonly year: number;

  @IsOptional()
  @IsString({ each: true })
  readonly genres: string[];
}
//DTO를 만드는이유는 코드를 간결하게 해주고 NestJS가 들어오는 쿼리에 대해 유효성을 검사할 수 있게 도와준다.

 

update-movie.dto.ts

import { PartialType } from '@nestjs/mapped-types';
import { CreateMovieDTO } from './create-movie.dto';

export class UpdateMovieDTO extends PartialType(CreateMovieDTO) {}

 

entities

entities는 DataBase에 넣어 줄 스키마를 미리 지정해주는 역할을 한다.

현재 예시는 간단하게 클래스를 지정하여 만들어 주었지만 실제 서비스에서는

typeORM을 사용하여 직접적으로 DB에 스키마를 만들어 줄 수 있으며

이 외에도 Prisma, Sequelize ORM 등이 있다.

movie.entity.ts

export class Movie {
  id: number;
  title: string;
  year: number;
  genres: string[];
}

 

movies 내부

movies.controller.ts

import {
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Patch,
  Body,
} from '@nestjs/common';
import { MoviesService } from './movies.service';
import { Movie } from './entities/movie.entity';
import { CreateMovieDTO } from './dto/create-movie.dto';
import { UpdateMovieDTO } from './dto/update-movie.dto';

@Controller('movies')
export class MoviesController {
  constructor(private readonly moviesService: MoviesService) {}
  /* movies.module.ts에서 @Module을 사용해서 controllers와 providers에서 import시켜주었기 때문에
  constructor(private readonly moviesService: MoviesService)와 같이 타입을 추가하는 것만으로
  같이 사용할 수 있게 되었다. 이것을 dependency injection이라고 한다.
   */

  @Get()
  getAll(): Movie[] {
    return this.moviesService.getAll();
  }

  @Get(':id')
  getOne(@Param('id') movieId: number): Movie {
    //필요한 것이 있을땐 반드시 요청을 해야한다. @Param()을 통해서 요청을 하고 props로 받는 구조다.
    console.log(typeof movieId);
    return this.moviesService.getOne(movieId);
  }

  @Post()
  create(@Body() movieData: CreateMovieDTO) {
    //@Body를 사용해서 클라이언트에서 보낸 object형식의 JSON을 추가할 수 있다.
    return this.moviesService.create(movieData);
  }

  @Delete(':id')
  remove(@Param('id') movieId: number) {
    return this.moviesService.deleteOne(movieId);
  }

  @Patch(':id') //데코레이터 Put과 Patch두가지가 있는데 Put은 모든 리소스를 받아오기 때문에 적합하지 않을 수 있다.
  patch(@Param('id') movieId: number, @Body() updateData: UpdateMovieDTO) {
    return this.moviesService.update(movieId, updateData);
  }

 

movies.module.ts

import { Module } from '@nestjs/common';
import { MoviesController } from './movies.controller';
import { MoviesService } from './movies.service';

@Module({
  controllers: [MoviesController],
  providers: [MoviesService],
})
export class MoviesModule {}

 

movies.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { Movie } from './entities/movie.entity';
import { CreateMovieDTO } from './dto/create-movie.dto';
import { UpdateMovieDTO } from './dto/update-movie.dto';

@Injectable()
export class MoviesService {
  private movies: Movie[] = [];

  getAll(): Movie[] {
    return this.movies;
  }
  //가짜 데이터베이스이며, 진짜데이터베이스에서는 Query를 가져온다.

  getOne(id: number): Movie {
    const movie = this.movies.find((movie) => movie.id === id);
    if (!movie) {
      throw new NotFoundException(`Movie with ID: ${id} not found.`);
      //HttpException에서 확장된 NestJS의 제공기능. 예외처리 기능을 한다.
    }
    return movie;
  }

  deleteOne(id: number) {
    this.getOne(id);
    this.movies = this.movies.filter((movie) => movie.id !== id);
  }

  create(movieData: CreateMovieDTO) {
    this.movies.push({
      id: this.movies.length + 1,
      ...movieData,
    });
  }

  update(id: number, updateData: UpdateMovieDTO) {
    const movie = this.getOne(id);
    this.deleteOne(id);
    this.movies.push({ ...movie, ...updateData });
  }
}

service.ts에서의 역할은 주로 서버에서 필요한 function의 집합체라고 이해할 수 있다.

가장 상위에서 @Injectable 데코레이터를 통해서 module내부에 provider로서

기능들을 주입할 수 있다.

 

 

마치며...

아키텍쳐를 정해두고 블록처럼 쌓아가는 형식으로 만들 수 있는것은 협업을 했을땐 오히려 가독성이 좋고 오차를 줄일 수 있는 장점이 있을 것 같다. 또한 프로그래밍의 방식을 고민해야할 것들이 이미 가이드라인으로 잡혀있다는 것 자체도 협업을 하는 시점에서 오해의 여지가 줄어들어 작업 효율이 좋아질 것 같아 보였다.

 

https://github.com/zeriong/Nest_intro

'Backend' 카테고리의 다른 글

[ TypeORM ] 서버와 SQL의 맵핑  (0) 2023.02.18
[ MySQL ] 상관,비상관 서브쿼리와 JOIN  (0) 2023.02.02
[ MySQL ] 데이터베이스 SELECT  (0) 2023.01.30
[ MySQL ] 데이터베이스  (4) 2023.01.29
Node.js란 무엇인가?  (0) 2022.11.27
2022. 11. 27. 10:21

1. Node.js

자바스크립트를 개조해서 만들어진 런타임이다.

자바스크립트는 특정프로그램, 그중에서도

웹브라우저 안에서만 동작한다.

그런데, Node.js를 사용하면 터미널프로그램에서

node를 입력하여 브라우저없이 바로 실행이 가능하게 된다.

웹브라우저에서만 동작하는 자바스크립트의 한계를 극복하게 되고

이로써 서버를 만들 수 있게 되고 자바스크립트 단 하나의 언어로도

웹페이지를 만들 수 있게 해주는 런타임이다.

 

2. Node.js의 Single Thread

싱글스레드로 구성된 이벤트루프를 활용하여 요청을
받아들이고 스레드풀을 활용하여 병렬처리를 지원한다.

  • Node.js는 Non-Blocking 방식을 사용한다. 클라이언트로부터
    요청을 받으면 다른 스레드에게 요청을 전달한다. 그렇게 되면
    싱글스레드는 자유로워지고 바로 다음 요청을 받을 수 있게 된다.
    그렇게 두번째 요청을 받으면 또 다시 다른 스레드에게 전달하고
    두번째 요청을 전달받은 스레드는 다른서버나 DB에 쿼리를 날려
    요청을 처리한다.
  • asynchronous(비동기/async)방식을 사용하기 때문에
    클라이언트 요청을 처리한 스레드가 응답을 가져오면
    (스레드가 전달받은 요청을 서버 또는 DB에 쿼리를 날려 응답을 받아오면)
    콜백함수를 실행하게 된다.

 

3. 비동기 작업이 가능하게 된 이유

Node.js의 구조를 보면

v8 자바스크립트 엔진과 비동기 작업을 처리하는

libuv라는 라이브러리로 이루어져 있다.

v8에는 memory heap과 하나의 call stack이 있다.

(call stack은 싱글스레드와 같은 의미로 이해하자.)

memory heap은 메모리 할당이 일어나는 곳이며,

call stack은 코드 실행에 따라 호출스택이 차례대로 실행된다.

그렇기 때문에 v8에서는 비동기식 처리를 할 수가 없다.

 

즉, 비동기 작업을 가능하게 만들어 주는 것은 libuv라는 라이브러리가

Non-Blocking IO라는 기능을 가능하게 만들어주는 이벤트루프를

제공하기 때문이다. libuv는 c언어로 만들어졌고 시스템 커넬을 사용한다.

커넬은 멀티스레드를 사용하기 때문에 Node.js가 비동기처리를 할 수 있게 만든다.

 

4. eventLoop

  1. Node.js API로 요청이 들어오면, 들어온 요청은
    event queue(이하 큐)에 추가 된다.

  2. Node.js의 이벤트 루프는 큐를 리슨하여 들어온 요청이 있다면
    선착순으로 요청이 처리된다.

  3. 요청은 스레드풀로 보내진다. 스레드풀은 이벤트루프의 일부로서
    여러 요청을 수행할 수 있다. 이 후에는
    (이벤트루프는 큐에 요청이 있는지 리슨)  =>  (요청이있다면? 스레드풀로)
    이렇게 반복한다.

  4. 스레드풀은 DB또는 파일, 다른 서버 등에 보낸 요청을 수행한다.

  5. 요청 수행을 마쳤다면, 콜백함수를 실행시켜 이벤트 루프로 응답을 전달한다.
  6. 이벤트루프는 응답을 클라이언트로 보낸다.

 

5. 정리

Node.js는 자바스크립트 이벤트 기반 모델에서 영감을 받은

이벤트 루프가 있는 싱글 스레드이다. 자바스크립트와 비슷한 싱글 스레드 이지만

네트워크 호출, 파일시스템 작업 등과 같이 비동기적으로 수행되는 작업은

자바스크립트 코드가 아니라 thread pool에서 실행된다.

Node.js는 thread pool을 가지고 있고 멀티 스레드의 개념도 가지고 있다.

하지만 정의를 내리자면 자바스크립트 환경에서는

싱글 스레드로 사용된다 정의할 수 있다.