NestJS, TypeORM 이해하기
Table of Content
1. 프로젝트 기본구조
1.1 프로젝트 생성
프로젝트 생성 하기
•
nestjs/cli 를 설치 했으며 환경변수 설정까지 완료 했으므로 터미널을 통해서 다음과 같은 명령어로 기본 프로젝트 구조를 생성 할 수 있게 되었다.
•
주의할 것은 CLI 명령어가 입력되는 현재 경로를 잘 확인해야 한다.
•
VSCode 또한 에디터 하단에 터미널을 이용 할 수 있으므로 해당 부분에 CLI 명령어를 통해서 프로젝트를 생성 하면 된다.
기본적인 커맨드라인 명령어
•
/projects/ 경로로 이동해서 다음과 같은 프로젝트 생성 명령어를 입력
nest new nestjs-board-app
Shell
복사
•
이처럼 CLI 명령어로 Nest프로젝트가 생성되면 IDE로 사용할 VSCode로 해당 폴더를 연다.
•
아래와 같은 프로젝트 기본 요소들이 생성 되어 있는 것을 확인
그 외 IDE를 사용하는경우 ex)IntelliJ IDEA 등
1.2 프로젝트 기본 요소
프로젝트의 기본 요소
•
다음과 같이 다양한 파일들이 기본적으로 생성된다.
•
개발자가 직접 로직을 작성하는 부분과 기타 설정, 컴파일 결과 등으로 구성되어 있다.
src 폴더
•
개발자가 주로 코드를 작성하는 부분
◦
{resource}.controller.ts 요청과 응답의 관문인 컨트롤러 계층 (Node.js에서의 Router)
◦
{resource}.service.ts
▪
컨트롤러 전달받은 파라미터를 연산, 처리 → 반환 해주는 비지니스 로직 서비스 계층
▪
+ Repository로 Database 접근하는 리포지토리 계층 포함
왜 NestJS는 Spring과 달리 Repository 계층을 {resource}.repository.ts 분리하지 않는지?
Repository를 나눌 수도 있다.
◦
app.module.ts IoC 컨테이너의 역할
▪
NestJS 애플리케이션의 주요 모듈을 정의. 이 모듈은 애플리케이션의 구성 요소들을 묶어주는 역할을 하며, 개발자가 작성해 나가는 서비스, 컨트롤러, 프로바이더 등 다양한 의존성 주입을 설정
▪
애플리케이션의 의존성을 관리하고 주입하는 책임
◦
main.ts 애플리케이션의 시작과 실행을 담당. (= 부트스트랩)
▪
부트스트랩 과정 (NestFactory를 통한 인스턴스 생성(app.module.ts에서 정의된 관계들을 담은 상태) → 초기화(설정 추가) → 리스닝)
▪
Pipe, CORS 필터 등 설정 할 때 초기화 부분에 설정을 추가하게 됨
그 외 중요 요소
•
의존성 관리
◦
package.json
▪
프로젝트 메타 데이터, 정보, 빌드 스크립트 등을 담아두고 있는 파일
▪
npm install packagename으로 설치한 의존성(Dependencies)의 개요가 작성됨
◦
package-lock.json
▪
의존성 설치 시 정확한 버전과 관련 의존성 트리를 기록하여 일관된 의존성 버전 관리 관리를 위한 파일
•
테스트
◦
Test 폴더에서는 통합테스트(Intergration Test)를 작성
◦
{resource}.service.spec.ts 특정 소스코드의 단위테스트(Unit Test)를 작성
•
버전관리 등 기타
◦
.git 해당 프로젝트 폴더가 git으로 관리되고 있는 설정파일들이 담긴 폴더 git init 시 생성
◦
.gitignore git이 변경사항을 추적(track)하지 않도록 설정하는 파일
◦
README.md github 메인 페이지
◦
dist 폴더 컴파일된 결과 소스 코드들이 담기는 폴더
◦
node_modules 폴더 의존성 설치 시 실제 다운로드 된 패키지의 소스코드들이 저장되는곳
2. 어플리케이션의 기본실행 과정
2.1 요청과 응답의 흐름
클라이언트 요청과 응답의 흐름은 다음과 같다
•
NestJS는 Express(웹서버)를 내장하고 있으며 Express위에 NestJS 웹 어플리케이션 서버(WAS)이 구축된 것
•
NestJS 애플리케이션은 Express를 통해 웹 서버의 기능을 제공하며, 전체적인 애플리케이션 서버 역할도 수행하는 형태라고 볼 수 있음
2.2 NestJS 어플리케이션 실행과 흐름
NestJS 어플리케이션이 실행되는 과정
•
터미널에서 다음과 같이 해당 명령어를 실행한다.
npm run start:dev
Shell
복사
•
이 명령어가 수행되는 과정은 다음과 같다.
NestFactory.create() 에 AppModule에서 정의한 설정을 가진 app 생성
◦
위와 같은 정보를 가진 app은 listen()을 통해 지정된 port 3000으로 요청을 듣는 상태로 계속하여 실행됨
2.3 NestJS의 모듈(Module)
모듈이란?
•
NestJS 어플리케이션이 실행되는 과정에서도 살펴 보았지만 루트 모듈인 AppModule의 중요성을 알 수 있다.
•
하나의 어플리케이션에 여러 기능들을 추가해 나가게 될 것인데 관련된 기능, 역할에 따른 코드들은 모아서 모듈화 한다. 그리고 그 소스들을 묶어주는 역할을 하는 것이 module.ts 모듈 파일로 생각하면 된다.
•
이러한 각 기능의 모듈을 묶어서 어플리케이션에 적용 시켜주는 것이 Application Module로 루트모듈이라 불리우며 app.module.ts 파일이다. 이처럼 모듈 들의 집합, 계층적 관계 구조를 가지고 있는 특징이 있다. 객체 지향 프로그래밍에서 특정 객체의 상태와 행동을 모아서 관리하는 설계 방법을 따르고 있는 것이다.
•
모듈간의 상호작용, 관계를 활용한 의존성 관리를 프레임워크 자체(ex: @Module)가 책임을 지고 있으므로 적절한 DI, IoC 개념을 적용하고 있어 이 또한 객체 지향 프로그래밍(OOP)을 지향하는 것으로 볼 수 있다.
◦
어떤 기능을 만들때 nestjs/cli 명령어로 생성하면 다음과 같은 구조로 폴더로 모듈화된 구조를 자동 생성해준다.
nest generate resource auth
Shell
복사
nest generate resource board
Shell
복사
▪
또한 생성된 파일은 아래처럼 기본 코드 템플릿이 작성되어 있어 각 소스 코드가 모듈에 연결되어 있는 것을 볼 수 있다.
2.4 NestJS의 모듈 기반 주의점
모듈 관계에 대한 이해도 필요
•
다른 웹 프레임워크와 전반적인 요청과 응답의 흐름은 같지만 모듈이 모듈을 포함 할 수 있는 형태인 계층적인 모듈을 기반으로 작동한다는 것을 주의해야 한다.
◦
Spring의 경우 이러한 부분을 Spring Container인 ApplicationContext가 자동으로 전역으로 관리, 검색하는 구조
▪
클라이언트 요청 → 컨테이너(컨트롤러 → 서비스 → 리포지토리) → 클라이언트 응답
◦
NestJS는 모듈단위로 분리된 클래스, 기능들을 조합하는 방식을 가지고 있음
▪
1개인 경우는 아래와 같이 동일해 보이지만,
•
클라이언트 요청 → 모듈(컨트롤러 → 서비스+리포지토리) → 클라이언트 응답
▪
다수 도메인이 생기는 경우,
•
클라이언트 요청 → 루트모듈[모듈1(C,S,R), 모듈2(C,S,R), 모듈3(C,S,R)] → 클라이언트 응답
◦
따라서, 개발자가 소스코드를 작성 할 때 모듈간의 사용 관계 등 계층적 구조를 파악 할 필요가 있음
명시적으로 모듈의 관계를 명확하게 작성해야 한다.
위 구조적 특징에 따라서 Spring의 자동 환경과는 다르게 직접 개발자가 모듈간의 관계를 명시해야되는 차이점을 알 수 있었다.
앞으로 우리는 user.controller.ts , board.controller.ts와 같이 각 도메인별로 코드가 작성되고 각 모듈이 구성될 것인데
예시로 memo 로 새로운 기능을 추가했다면, 루트 모듈인 app.module.ts에서 @Module 에 memo 관련 모듈이 추가되어야 한다는 것 → 다행히 CLI generate로 자동 추가 되지만 확인할 필요가 있다.
3. 구성요소 생성 해보기
3.1 초기 상태
우리는 nest new nestjs-board-app 으로 생성한 프로젝트 시작점에 있다.
•
src 폴더 내 자동 생성된 파일 삭제
◦
app.controller.spec.ts
◦
app.controller.ts
◦
app.service.ts
◦
루트모듈 app.module.ts 자동 템플릿 내용 삭제 수정
import { Module } from '@nestjs/common';
@Module({
imports: [],
})
export class AppModule {}
TypeScript
복사
3.2 모듈 생성
초기 상태에서 부터 모듈을 생성해 나가보자
•
nestjs/cli 명령어 다음과 같이 입력한다.
◦
g는 generate의 약자
nest g module boards
Shell
복사
•
다음과 같이 CREATE, UPDATE 내용이 나타나게 된다.
•
실제로 변경된 파일들을 살펴보자
◦
boards 폴더가 생성되었다.
◦
boards.module.ts 파일이 생성되었으며 기본 코드 템플릿이 작성되어 있다.
◦
루트모듈인 app.module.ts 의 @Module 데코레이터 내부
▪
생성된 boards.module.ts의 클래스인 BoardModule이 자동 추가되었다.
3.3 컨트롤러 생성
이번엔 컨트롤러 계층을 생성해보자
•
nestjs/cli 명령어 다음과 같이 입력한다.
◦
--no-spec 은 단위테스트 파일을 생성하지 않는 것
nest g controller boards --no-spec
Shell
복사
•
다음과 같이 CREATE, UPDATE 내용이 나타나게 된다.
•
실제로 변경된 파일들을 살펴보자
◦
자동적으로 boards 폴더 를 찾고 그 내부에 boards.controller.ts 파일이 생성되었으며 기본 코드 템플릿이 작성되어 있다.
◦
@Controller(’boards’) 데코레이터가 클래스 위에 선언되어 있다.
▪
해당 클래스는 컨트롤러 역할을 수행하게 된다.
▪
해당 컨트롤러의 기본 엔드포인트(prefix)가 /boards 임을 나타낸다.
•
boards 기능 모듈인 boards.module.ts 의 @Module 데코레이터 내부
◦
생성된 boards.controller.ts의 클래스인 BoardsController가 controllers 배열에 자동 추가되었다.
3.4 서비스 생성
이번엔 서비스 계층을 생성해보자
•
nestjs/cli 명령어 다음과 같이 입력한다.
◦
--no-spec 은 단위테스트 파일을 생성하지 않는 것
nest g service boards --no-spec
Shell
복사
•
다음과 같이 CREATE, UPDATE 내용이 나타나게 된다.
•
실제로 변경된 파일들을 살펴보자
◦
자동적으로 boards 폴더 를 찾고 그 내부에 boards.service.ts 파일이 생성되었으며 기본 코드 템플릿이 작성되어 있다.
◦
@Injectable() 데코레이터가 클래스 위에 선언되어 있다.
▪
해당 클래스는 DI Container에 등록되며 주입 될 수 있는 클래스임이 명시된다.
•
boards 기능 모듈인 boards.module.ts 의 @Module 데코레이터 내부
◦
생성된 boards.service.ts의 클래스인 BoardsService가 providers 배열에 자동 추가되었다.
◦
등록된 인스턴스를 주입받을 클래스에서는 생성자 주입(constructor)을 통해 사용 할 수 있다.
4. 추가 개념 - DI 사용과 주입 방식
다른 클래스를 사용하기 위해서는?
•
@Injectable() 데코레이터가 작성된 클래스는 DI Container가 인스턴스 생성, 주입 등 의존성 관리를 하게 된다고 하였다. 이는 주입 준비 과정이라 생각하면 된다.
•
다른 클래스에서 다른 클래스를 사용하기 위해서는 위와 같이 주입 준비가 된 클래스를 주입하는 과정이 필요한데 생성자 주입을 통해서 사용 할 수 있다.(DI)
◦
예로 BoardsService에 문자열을 반환하는 hello()라는 함수가 정의되어 있다고 해보자.
boards.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class BoardsService {
async hello(): Promise<string> {
return 'Hello from BoardsService!';
}
}
TypeScript
복사
◦
지금까지 생성한 controller, service를 보면 3 Layer Architecture 역할과 기능의 분담 개념에 따라 BoardsController에서는 요청을 받을 뿐 요청에 대한 연산 등의 처리를 BoardsService 계층으로 전달해야 한다.
◦
이 때 BoardsService로 전달하기 위해서는 메서드의 호출이 필요하므로 사용이 필요한 상태가 된다.
◦
주입받아 사용해야 하는 경우 작성할 생성자 주입 관련 코드는 다음과 같다.
boards.controller.ts
import { Controller } from '@nestjs/common';
import { BoardsService } from './boards.service'; //auto import
@Controller('boards')
export class BoardsController {
//생성자 주입
constructor(private boardsService: BoardsService){}
...
//주입된 인스턴스는 메서드 내에서 사용
@Get('hello')
async getHello(): Promise<string> {
return this.boardsServce.hello();
}
}
TypeScript
복사
생성자 주입말고는 어떤것들이 있고 왜 생성자 주입을 하는지?
•
필드 주입 - @Inject()로 가능하긴 하지만 권장되지 않음
•
메서드 주입 - setBoardsService() 와 같이 별도 메서드로 가능하긴 하지만 권장되지 않음
•
생성자 주입을 하는 이유
◦
위 두 다른 방식의 경우 객체의 생성과 의존성 주입이 분리되어 있다.
▪
객체가 생성 될 때 의존성이 주입 됨 = 이 말은 이후에도 객체를 생성하면 의존성이 다시 주입 될 수 있으므로 객체의 상태가 변경 될 가능성이 있음
▪
불변성을 보장하지 못하는 방식
◦
생성자 주입은 객체가 생성 될 때 모든 의존성 주입이 완료되고 변경되지 않음
▪
불변성을 보장 할 수 있음
▪
생성자에 모든 의존성이 명시되므로 사용하는 클래스에서 필요한 의존성을 명확히 구분 가능
DI가 가능한 범위는?
•
providers에 등록된 주입 가능한 클래스는 해당 모듈이 등록된 범위로 한정된다.
•
아래 코드를 통해서 보면 @Module({ … }) 범위내에서는 주입받아 사용 할 수 있다고 이해하면 됨
•
또한 해당 모듈을 포함하고 있는 상위 모듈(ex: 루트모듈인 AppModule)도 사용 할 수 있다.
import { Module } from '@nestjs/common';
import { BoardsController } from './boards.controller';
import { BoardsService } from './boards.service';
@Module({
controllers: [BoardsController, ...],
providers: [BoardsService, ...]
})
export class BoardsModule {}
TypeScript
복사
•
추가적으로 외부까지 해당 의존성을 사용할 수 있도록 확장하는 방법도 있다.
•
아래와 같이 exports 배열에 등록하면 외부 사용을 허용하게 됨
@Module({
imports: [],
providers: [BoardsService],
exports: [BoardsService] // 외부 모듈에서 사용할 수 있도록 BoardsService를 export
})
export class BoardsModule {}
TypeScript
복사
Related Posts
Search