일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- Python
- codestates
- execution context
- JavaScript
- 테스트코드
- context switching
- 자료구조
- algorithm
- useState
- react 기초
- python algorithm
- 코드스테이츠
- 비동기
- node.js
- 운영체제
- 글또
- 개발공부
- Zerobase
- java
- 자바스크립트
- 프로그래머스
- 알고리즘
- 파이썬 알고리즘 인터뷰
- 파이썬
- 컴퓨터공학
- Computer Science
- OS
- 자바
- Operating System
- REACT
- Today
- Total
Back to the Basics
[NestJS] Module의 실행 과정과 종류 본문
NestJS에서 모듈은 애플리케이션에서 비슷한 기능을 논리적으로 묶은 단위이다. 유사한 기능을 하는 컴포넌트들을 한데 모아 관리하는 컨테이너 역할을 한다.
만약 Auth 서비스(로그인, 회원가입 등), 사용자 관련 서비스(사용자 정보 조회, 사용자 정보 수정 등), 주문 관련 서비스(주문 생성, 주문조회 등)가 있다고 한다면 이들 각가이 하나의 모듈이 된다. 이런 특성으로 코드를 깔끔하게 정리할 수 있고 다른 모듈과 독립적으로 동작하도록 한다.
NestJS 모듈은 @Module() 데코레이터가 달린 단일 클래스이며 이 데코레이터를 통해 애플리케이션 구조를 구성하는데 필요한 메타데이터를 제공받는다. 제공받는 메타데이터는 아래와 같이 네 가지가 존재한다.
imports | 다른 모듈에서 export하는 기능을 사용하기 위해 다른 모듈을 가져온다. 의존성 주입의 기본이 되는 부분 |
controllers | 이 모듈에 속한 컨트롤러를 정의. HTTP 요청을 처리하는 부분 |
providers | 모듈에서 사용 할 서비스를 등록하는 부분 import한 다른 모듈에서 export한 provider를 여기에 등록하여 사용한다. |
exports | 다른 모듈에 제공하고자 하는 provider의 집합이다. export에 등록된 provider만이 다른 모듈에서 사용할 수 있다. |
@Module({
imports: [CommonModule], // CommonModule에서 export하고 잇는 provider를 사용하기 위해 가져오는 module집합
controllers: [UserController], // UserModule에서 정의된 모든 Controller 집합
providers: [UserService, CommonService], // UserService와 CommonService를 이 모듈의 비즈니스로직에서 사용하기 위해 provider로 등록한다
exports: [UserService], // UserService를 다른 모듈에서 사용할 수 있도록 제공
})
export class UserModule {}
NestJS의 모듈 시스템 실행 과정
NestJS는 AppModule이라는 최소 하나의 모듈로 이루어져 있다. 그리고 대부분 Application은 다수의 모듈을 사용한다. 아래의 그림과 같이 AppModule 루트모듈로 Application의 시작점이며 AppModule에서 하위 모듈을 import 하여 그래프 구조를 갖는다.
@Module({
imports: [AuthModule, UsersModule, OrderModule], // AppModule에서 하위 모듈을 모두 import
})
export class AppModule {}
이렇게 등록된 모듈 시스템은 NestJs가 실행될 때 아래와 같은 순서로 작동한다.
1. 애플리케이션 부트스트래핑과 의존성 그래프 구축
애플리케이션이 시작되면 main.ts의 NestFactory.create(AppModule) 을 호출하여 루트 모듈을 등록한다.
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
NestJS 애플리케이션이 부트스트래핑 되면 AppModule에 등록된 하위 Module들은 의존성 그래프를 구축하게 된다. 이 과정에서 아래와 같은 동작이 진행된다.
1. root module의 imports 배열에 선언된 모든 모듈을 재귀적으로 분석한다
2. 각 모듈에 정의된 providers, controllers를 식별한다.
3. 모듈 간 관계와 의존성을 매핑한다
NestJS 코드 내부를 살펴보면,
MODULE_METADATA.IMPORTS를 사용하여 imports의 메타데이터를 가져오는 것을 볼 수 있다.
const modules = !this.isDynamicModule(
moduleDefinition as Type<any> | DynamicModule,
)
? this.reflectMetadata(
MODULE_METADATA.IMPORTS,
moduleDefinition as Type<any>,
)
: [
...this.reflectMetadata(
MODULE_METADATA.IMPORTS,
(moduleDefinition as DynamicModule).module,
),
...((moduleDefinition as DynamicModule).imports || []),
];
그리고 아래로 더 내려가면 재귀적으로 innerModule에 대해 scanForModules를 호출하면서 재귀적으로 imports문에 선언된 모든 모듈을 분석한다
let registeredModuleRefs: Module[] = [];
for (const [index, innerModule] of modules.entries()) {
// In case of a circular dependency (ES module system), JavaScript will resolve the type to `undefined`.
if (innerModule === undefined) {
throw new UndefinedModuleException(moduleDefinition, index, scope);
}
if (!innerModule) {
throw new InvalidModuleException(moduleDefinition, index, scope);
}
if (ctxRegistry.includes(innerModule)) {
continue;
}
const moduleRefs = await this.scanForModules({
moduleDefinition: innerModule,
scope: ([] as Array<Type>).concat(scope, moduleDefinition as Type),
ctxRegistry,
overrides,
lazy,
});
registeredModuleRefs = registeredModuleRefs.concat(moduleRefs);
}
public async scanModulesForDependencies(
modules: Map<string, Module> = this.container.getModules(),
) {
for (const [token, { metatype }] of modules) {
await this.reflectImports(metatype, token, metatype.name);
this.reflectProviders(metatype, token);
this.reflectControllers(metatype, token);
this.reflectExports(metatype, token);
}
}
2. 공급자(Provider) 인스턴스화
의존성 그래프가 구축되면, NestJS는 아래와 같은 순서로 Provider를 인스턴스화한다.
1. 글로벌 공급자 먼저 인스턴스를 한다.
2. 모듈별 공급자를 인스턴스화 한다
3. 각 공급자의 의존성을 주입한다
글로벌 공급자는 ApplicationConfig에 등록된 글로벌 가드, 파이프, 인터셉터, 필터 등을 말한다.
코드는 DependenciesScaner 의 applyApplicationProviders 부분과 instance-loader.ts 에서 확인해 볼 수 있다.
3. LifeCycle Hook 실행
https://docs.nestjs.com/fundamentals/lifecycle-events
NestJs는 Provider 인스턴스화 후 아래와 같은 순서로 LifeCycle Hook를 호출한다.
1. onModuleInit() - 해당 모듈의 모든 의존성이 해결된 후 호출
2. onApplicationBootstrap - 모든 모듈이 초기화된 후 호출(하지만 연결을 수신하기 전에 호출된다)
4. 컨트롤러 등록
모든 Provider가 인스턴스화되고 LifeCycle Hook이 실행된 후 NestJS는 각 모듈에 정의된 컨트롤러를 등록한다.
1. 컨트롤러 및 route handler 분석
2. 미들웨어, 가드 등 적용
3. HTTP요청 처리를 위한 라우팅 테이블 설정
code/nest-application.ts의 init을 보면 아래와 같이 NestJS의 LifeCycle Hook 실행 후 Router를 확인하는 함수가 실행되는 것을 볼 수 있다
public async init(): Promise<this> {
if (this.isInitialized) {
return this;
}
this.applyOptions();
await this.httpAdapter?.init?.();
const useBodyParser =
this.appOptions && this.appOptions.bodyParser !== false;
useBodyParser && this.registerParserMiddleware();
await this.registerModules();
await this.registerRouter();
await this.callInitHook();
await this.registerRouterHooks();
await this.callBootstrapHook();
this.isInitialized = true;
this.logger.log(MESSAGES.APPLICATION_READY);
return this;
}
이렇게 모든 모듈, 공급자, 컨트롤러가 초기화되면 애플리케이션은 HTTP 요청을 처리할 준비가 된 상태가 된다.
NestJS 모듈의 종류
https://docs.nestjs.com/modules
NestJs의 모듈 종류는 여러 가지가 있다. 위에서 실행과정을 보면서도 nestjs의 모듈 종류에 따라 다르게 처리하는 부분들을 봤던 것 같다.
1. Feature Module
처음 NestJs를 사용하면 기본적으로 사용하는 모듈이다. 비슷한 기능, 관련된 기능 단위로 관리하며 관련된 컨트롤러, 서비스, 레포지토리 등을 묶는다. 기능별로 분리를 하고 독립적으로 동작하거나 다른 모듈에 의존할 수 있다.
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Module({
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
2. Shared Module
NestJs에서 모듈은 기본적으로 싱글톤이다. 이러한 특징으로 동일한 instance의 provider를 모듈 간 공유할 수 있다. 모든 모듈이 공유모듈이라고 할 수 있다.
3. Dynamic Module
동적모듈은 런타임에 설정을 동적으로 적용할 수 있는 모듈이다. 애플리케이션 실행 시 생성되는 정적 모듈과 다르게 모듈의 구성 요소를 실행 시점에 정할 수 있어 유연성이 좋다. 비슷한 기능을 여러 모듈에서 사용할 때, 설정만 다르게 해서 동일한 모듈을 재활용할 수 있다는 점이 큰 장점이라고 생각한다.
아래의 코드는 동적 모듈로 로그 모듈을 생성하고 사용하는 모듈에서 로그 레벨을 동적으로 설정할 수 있음을 보여준다.
import { Module, DynamicModule } from '@nestjs/common';
import { LoggerService } from './logger.service';
// 정의
@Module({})
export class LoggerModule {
static forRoot(options: { level: string }): DynamicModule {
return {
module: LoggerModule,
providers: [
{ provide: 'LOG_LEVEL', useValue: options.level },
LoggerService,
],
exports: [LoggerService],
};
}
}
// 사용
@Module({
imports: [LoggerModule.forRoot({ level: 'debug' })],
})
export class AppModule {}
글로벌 모듈은 @Global() 데코레이터를 사용하여 전역 범위에서 사용 가능한 모듈이다. 한 번 정의하면 다른 모듈에서 import 없이도 provider를 주입받을 수 있다.
주의할 점은 전역으로 사용되기 때문에 의존성 추적이 어려울 수 있으므로 신중하게 사용해야 한다. 보통 전역 설정, 공통 유틸리티, 캐싱 서비스 등 애플리케이션 전반에 걸쳐 접근해야 하는 경우 사용하기 좋다
import { Module, Global } from '@nestjs/common';
import { CacheService } from './cache.service';
@Global()
@Module({
providers: [CacheService],
exports: [CacheService],
})
export class CacheModule {}
마치며
이번 포스팅은 NestJS의 모듈에 대해 알아보았다.
다음은 프로바이더의 내부 동작과 종류 그리고 언제 어떤 방식을 사용하면 좋을지에 대해 정리해 보겠다.
'Programming Languages > JavaScript & Node.js' 카테고리의 다른 글
NodeJS의 내부 구성과 동작에 대하여 알아보자 (2) | 2024.12.22 |
---|---|
NODEJS비동기의_이해와_비동기처리에 대하여 (0) | 2022.03.28 |
Javascript의 실행컨텍스트 (1) | 2022.03.22 |
About export & module.exports & export (0) | 2022.03.16 |
About require() (1) | 2022.03.15 |