본문 바로가기

Development/Web

Nest JS #1 - 기본 구조 및 기초 개념(Model, Controller, Providers, Service, DI)

들어가며

현재 항해99에 참여중이며 약 2주 동안 알고리즘 공부만 하고 있으니, 흥미도 떨어지고, 텐션도 낮아지는 듯한 느낌이 들었다. 다시 흥미를 끌어올릴겸 잠시 주위를 환기시켜보겠다는 의미로 주특기 공부를 하다가 Nest JS가 눈에 들어오기 시작했다. Nest JS라는 프레임워크를 들어본 적은 있기 때문에 관심이 있는 상태였긴 했는데, 결국 호기심을 참지 못하고 공부한 내용을 글로 남겨본다.

Nest에 관심을 보이게 된 계기

누군가 내게 Node.js로 당장 뭘 할 줄 아냐라고 물어본다면, 간단한 API 서버, 실시간 통신 서버를 흉내내는 정도라고 할 수 있을 정도로 express 프레임워크를 자유자재로 다루지는 못하지만, 혼자 공부하면서 항상 고민하던 것이 있었다. 그건 바로 효율적인 개발을 위해 express에 어떠한 디자인 패턴을 적용시켜야 하는가에 대한 고민이었다.

책상, 폴더 구조, 파일명 등 단정하고, 깔끔한 걸 선호하는 내 성향이 개발할 때에도 큰 영향을 미치는 것 같다. 이게 무슨 느낌인가 하면, 토이 프로젝트를 하다 보면 폴더에 파일이 수두룩하게 쌓이기 마련인데 오류가 발생하거나, 새로운 기능을 추가할 떄 비효율적인 것은 당연하고, VSCode 좌측에 위치한 폴더 목록을 보면 괜히 주위가 산만해지는 느낌이 든달까?

Spring하면 가장 먼저 떠오르는 디자인 패턴으로는 MVC 패턴이 떠오르는 반면, Node.js에는 명확한 아키텍처 디자인 패턴이 없는 것 같았다.

나는 Java를 할 줄은 모른다. 물론 간단한 코드는 짤 수 있겠지만, Java로 어떠한 프로그램도 개발한 경험이 없기 때문에 못한다는 말이 더 정확할 것 같다. 그리고 Spring하면 MVC 패턴이 떠오르는 이유는 유튜브 영상이나 블로그 글만 찾아봐도 MVC 패턴하면 Spring과 연관되는 경우를 자주 보았기 때문이다.

그러던 중 MVC 패턴을 Node.js에 적용시켜보면 어떨까하고 검색한 적이 있었고, 정말 괜찮은 영상을 발견하여 Node.js에 MVC 패턴을 적용시켜보기도 하였다. 그리고 2주 전, 이 경험을 토대로 팀 단위 미니 프로젝트를 수행하였고, Flask 또한 MVC 패턴과 유사하게 바꾸어놓았다. 역할 분담이 확실하기 때문에 규모가 작은 프로젝트에서는 유지보수 및 효율적인 개발이 가능한 MVC 패턴의 특징을 알고 있기에 약 6주 후에 있을 실전 프로젝트에도 적용시키면 효율적이지 않을까라는 생각으로 관련 영상을 찾아보던 중에 개발바닥 채널의 유튜브 영상을 통해 Nest JS의 기본 구성이 Spring의 MVC 패턴과 매우 유사하다는 정보를 알게 되었다. 이를 계기로 잠시 머리를 식힐겸 Nest 공식문서를 살펴보다가, John Ahn님의 유튜브 영상을 보게 되었다. 이 영상에는 기초 개념부터 세세한 부분까지 잘 설명되어 있었고, 약 6시간 가량의 영상을 그냥 보고 지나치기 아까워서 직접 구현해보며 내용을 정리해보았다.

Nest JS 개요

공식문서의 내용을 정리해보면 다음과 같으며, 대충 이러한 내용이 있구나라는 정도만 참고하면 될 듯 하다.

Nest JS(이하, Nest)는 효율적이고 확장 가능한 Node.js 서버 측 애플리케이션을 구축하기 위한 프레임워크이며. 이는 Progressive JavaScript를 사용하고, TypeScript를 완벽하게 지원하며, 개발자가 순수 JavaScript로 코딩할 수 있다는 특징이 있으며, Object Oriented Programming, Functional Programming, Functional Reactive Programming 요소를 사용할 수 있다. Nest JS는 내부적으로 Express와 같은 HTTP 서버 프레임워크를 사용하며, Fastify를 사용하도록 구성할 수 있다. 또한, Express, Fastify와 같은 공통 Node.js 프레임워크에 추상화 수준을 제공하지만, API를 개발자에게 직접 노출시킨다. 이를 통해 개발자는 기본 플랫폼에서 사용할 수 있는 수 많은 타사 모듈을 자유롭게 사용할 수 있다. Node.js를 위한 훌륭한 라이브러리나 도움을 주는 도구가 많이 존재하나, 이들 중 어느것도 아키텍처의 주요 문제를 효과적으로 해결하지 못한다. Nest JS는 개발자와 팀이 고도로 테스트 가능하고, 확장 가능하며, 느슨하게 결합되고, Angular에서 영감을 받아 유지 관리가 쉬우며 즉시 사용 가능한 애플리케이션 아키텍처를 제공한다.

기본 구조 및 개념

영상에 나온대로 기본 구조 및 개념에 대해서 정리하였으며, 동시에 구현해보면서 어떤 방식으로 작동되는지 파악하기 위해 Nest 프로젝트를 생성하였다. Nest CLI를 이용하여 프로젝트를 시작할 수 있다. 이때, 새 프로젝트 디렉토리가 생성되고, 초기 핵심 Nest 파일 및 지원 모듈로 디렉토리가 채워짐으로써 프로젝트의 기본 구조가 생성된다.

$ npm i -g @nestjs/cli
$ nest new project-name

아래 명령어를 입력하여 Nest가 잘 설치되었는지 확인할 수 있다.

$ nest --version

기본 파일 및 폴더

  • eslintrc.js : 개발자들이 특정한 규칙으로 코드를 깔끔하게 작성할 수 있도록 도와주는 라이브러리이다. 이는 TypeScript 가이드 라인 제시, 문법 오류 발생 시 알림 등의 역할을 한다.
  • .prettierrc : 주로 작은 따옴표와 큰 따옴표 중 어떠한 것을 사용할 것인지, indent는 2 또는 4 중에서 얼마로 정할 것인지 등의 코드 형식을 맞추는데 사용된다.
  • nest-cli.json : Nest 프로젝트를 위해 특정한 설정을 할 수 있는 json 파일이다.
  • tsconfig.json : TypeScript 컴파일 방식을 설정할 수 있는 json 파일이다.
  • tsconfig.build.json : tsconfig.json의 내용을 토대로 build 할 때 필요한 추가 설정을 할 수 있는 json 파일이다.(exclude에는 불필요한 파일명을 명시한다.)
  • package.json : 프로젝트의 전반적인 설정을 할 수 있는 json 파일이며, 이 중에서도 build, format, npm 명령어를 주로 설정할 수 있다.
  • /src : 대부분의 비즈니스 로직이 포함되어 있는 디렉토리이며, nest-cli.json파일의 sourceRoot로 설정되어 있다.

비즈니스 로직 흐름

package.json 파일에 명시된 명령어(npm run start:dev)를 터미널에 입력하면 Nest를 실행시킬 수 있다. 애플리케이션의 포트는 main.ts에서 설정할 수 있으며, 기본 포트가 3000으로 설정되어 있다. 웹 브라우저를 통해 localhost:3000으로 접속하면, Hello World!가 출력된 웹 페이지를 확인할 수 있다.

먼저, 애플리케이션은 클라이언트로부터 받은 end-point를 app.controller.ts에게 전달한다.

@Get('/')    
getHello(): string {
  return this.appService.getHello();
}

그리고 appService.getHello()를 통해 현재 end-point(/)에 해당하는 로직이 수행되는데, 이는 app.service.ts 파일의 AppService Class를 통해 실행되는 메소드이다. 즉, Contoller가 Service로부터 로직 처리 결과를 전달받고, Contoller는 이를 클라이언트에게 응답 결과로 전달한다.

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

Module

애플리케이션에 존재하는 여러 모듈(가령, 인증처리를 위한 AuthModule 등)은 root 모듈에 해당하는 AppModule에 포함되며, 각각의 모듈에는 Contoller, Entity, Service 등이 있다.

Module은 Class를 데코레이터(@Module())로 데코레이션하여 정의할 수 있고, 데코레이터(@Module())는 애플리케이션 구조를 구성하는데 사용하는 메타 데이터를 제공한다.

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

Module을 구성할 때에는 밀접하게 관련된 기능 집합으로 구성하는 것이 효과적이며, 같은 기능에 해당하는 Contoller, Entity, Service는 해당 Module과 함께 동일한 디렉토리 안에 넣어서 사용한다. 또한, Module은 기본적으로 1개의 인스턴스만 생성되는 Singletone 패턴이므로 여러 Module 간 공급자의 동일한 인스턴스를 쉽게 공유할 수 있다.

BoardsModule 생성

Module을 직접 생성하기 위해 src 디렉토리에서 main.ts, app.modules.ts를 제외한 나머지 파일들을 삭제해주도록 하자(v0.0.0). Nest에서는 대부분 명령어를 사용하여 Module, Controller, Entity, Service 등을 생성할 수 있다.

$ nest g module module-name

터미널에 다음과 같이 명령어를 입력하여 Boards라는 새로운 Module을 생성할 수 있다. Module이 생성되고 난 후에는 app.modules.ts에 BoardsModule이 자동으로 등록된다(v0.0.1).

$ nest g module boards
@Module({
  imports: [BoardsModule],
})

Controller

Controller는 클라이언트의 요청을 처리하고, 처리한 결과에 따른 결과를 다시 클라이언트에게 응답으로 반환한다.

Controller는 Class를 데코레이터(@Controller)로 데코레이션하여 정의할 수 있다. 이때, 데코레이터의 인자는 Controller에 의해 처리되는 path(경로)를 전달받는다. 만약, 인자를 users로 지정한다면 해당 Controller는 users에 해당하는 Controller가 된다.

@Controller('boards')
export class BoardController {
  @Get('/')
  getBoards(): string {
    return 'all bords'    
  }
}

위의 코드 중에서 @Get, @Post, @Delete과 같은 데코레이터를 Handler라고 하며, 이는 Controller Class에 존재하는 단순한 메소드이다.

BoardsController 생성

Controller는 다음과 같은 명령어로 생성할 수 있다.

$ nest g controller controller-name

마찬가지로 BoardsModule에 해당하는 Controller를 생성하기 위한 명령어는 다음과 같으며, --no-spec 옵션을 추가하여 테스트용 소스코드를 생성하지 않도록 설정하였다.

$ nest g controller boards --no-spec

Controller 생성이 완료된 후에는 터미널에 다음과 같은 문구를 볼 수 있으며, 문구에 나타난 파일을 열어 확인해보면 새롭게 생성된 Controller가 BoardsModule에 추가된 것을 볼 수 있다(v0.0.2).

CREATE src/boards/boards.controller.ts (101 bytes)
UPDATE src/boards/boards.module.ts (174 bytes)
@Module({  
  imports: [BoardsModule],
})

CLI 명령어를 통해 Controller를 생성하는 과정을 그림을 나타내면 다음과 같다.

Providers와 Service

Provider는 Nest JS의 기본 개념이다. 대부분의 기본 Nest Class는 Service, Repository, Factory, Helper 등 Provider로 취급될 수 있으며, 의존성으로 주입할 수 있다는 것이 Provider의 주요 아이디어이다. 즉, 객체는 서로 다양한 관계를 만들 수 있으며, 객체의 인스턴스를 연결하는 기능은 대부분 Nest Runtime 시스템에 위임될 수 있다.

공식문서에서 나온 용어와 그 개념이 꽤 복잡하기 때문에 조금 더 쉽게 풀어서 설명해보겠다. 예를 들어, 다음 그림과 같이 하나의 Controller에서 여러 개의 Service를 필요로 하는 경우, 각각의 Service를 Controller에 넣어서 사용할 수 있는 환경으로 만드는 것을 의존성 주입이라고 할 수 있으며, 이들을 Provider라고 한다.

Provider 중에서 Service는 Nest 또는 JavaScript에만 국한되지 않는 소프트웨어 개발의 공통된 개념이며, @injectable 데코레이터로 감싸진 채 모듈로 제공되고, 주입된 Service 인스턴스는 애플리케이션 전체에서 사용할 수 있다.

BoardsService 생성

Service는 주로 데이터베이스와 관련된 로직을 처리하며, 아래 명령어를 입력하여 새로운 Service를 생성할 수 있다. Service에 대한 내용은 뒤에서 Provider와 함께 정리하였다.

$ nest g service service-name

마찬가지로 BoardsModule에 해당하는 Service를 생성하기 위한 명령어는 다음과 같으며, --no-spec 옵션을 추가하여 테스트용 소스 코드를 생성하지 않도록 설정하였다.

$ nest g service boards --no-spec

Providers 등록

앞서 언급하였듯이 Service는 다른 컴포넌트에서 해당 Service를 사용할 수 있도록 만들어주는 @Injectable 데코레이터를 볼 수 있으며, boards.module.ts의 Providers에 해당 Service가 등록된 것을 확인할 수 있다(v0.0.3).

@Injectable()
export class BoardsService {}
@Module({  
  controllers: [BoardsController],  
  providers: [BoardsService],
})

의존성 주입

BoardsService를 BoardsController에서 사용하려면 의존성 주입(Dependency Injection)이 필요하다. Nest JS에서는 Controller Class의 생성자를 통해 의존성을 주입할 수 있다.

@Controller('boards')export 
class BoardsController {  
  boardsService: BoardsService;
  constructor(boardsService: BoardsService) {
    this.boardsService = boardsService;  
  }}

이를 TypeScript의 private 접근 제한자를 사용하여 재구성하면 다음과 같은 코드로 나타낼 수 있다. 접근 제한자를 사용하지 않는 경우에는 Class에 선언한 값만 객체의 property로 사용한 반면, 접근 제한자(public, protected, private)를 생성자의 파라미터에 선언하면 해당 파라미터는 암묵적으로 Class의 property로 선언되어 비교적 간결한 코드로 나타낼 수 있다. 또한, BoardsService는 데이터베이스와 관련된 로직을 처리하므로 Controller Class 내부에서만 사용 가능하도록 private 접근 제한자로 설정하였다(v0.0.4).

@Controller('boards')
export class BoardsController {
  constructor(private boardsService: BoardsService) {}
}

마치며

여기까지 Nest JS는 무엇이고, 어떠한 구조로 되어있는지에 대해서 정리해보았다. 현재 데이터베이스 로직 처리 부분을 공부하고 있는데, 이 부분 또한 정리되면 포스팅할 예정이다.

GitHub

참고자료