NestJS 컨테이너, DI는 어떻게 작동할까 (1)
0. 들어가며
- NestJS에서 의존성들을 관리하고 주입해 주는 것은 알고 있지만 어떤 흐름으로 어떻게 주입하고 관리하는지에 대해서는 잘 모르고 있습니다.
- 이를 파악해 보려 NestJS 코드를 직접 확인해보며 이해해보려고 합니다.
- 분석의 목적은 NestJS를 더 잘 이해하고 잘 쓰는것 입니다!
- 해당 글은 NestJS v10.3.6을 기준으로 작성되었습니다.
1. 기준 코드 작성
-
현재 진행하고 있는 프로젝트의 모든 의존성들에 대한 주입 방식을 모두 따라가보려했지만, 모듈이 너무 많은 나머지 이해하기 더 어려웠었습니다.
-
그래서 간단한 예시 모듈 하나만 작성하고 해당 코드를 기준으로 살펴보도록 하겠습니다.
- 필수적인 bootstrap, module, controller, service만을 간단하게 작성하겠습니다.
// bootstrap // 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(); // module // app.module.ts import { Module } from "@nestjs/common"; import { AppController } from "./app.controller"; import { AppService } from "./app.service"; @Module({ imports: [], controllers: [AppController], providers: [AppService], }) export class AppModule {} // controller // app.controller.ts import { Controller, Get } from "@nestjs/common"; import { AppService } from "./app.service"; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { return this.appService.getHello(); } } // service // app.service.ts import { Injectable } from "@nestjs/common"; @Injectable() export class AppService { getHello(): string { return "Hello World!"; } }
2. IoC 컨테이너 부터 확인해보자
- 모듈, 서비스, 컨트롤러 등 각종 핵심 데코레이터들의 로직을 보면, 메타데이터를 등록하는 로직들로 이루어진 것을 확인할 수 있습니다.
- 이러한 메타데이터들이 등록되는 이유는 무엇일까요? 이들을 알아채고 관리하고 사용하는 곳이 어디일지부터 살펴보아야 합니다.
- 우선 bootstrap 부분의 await NestFactory.create(AppModule) 로직부터 확인해봐야 합니다.
nest-factory.ts
public async create<T extends INestApplication = INestApplication>(
moduleCls: any,
serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
options?: NestApplicationOptions,
): Promise<T> {
// serverOrOptions 옵션을 주지 않아서 뒤에 배열을 사용합니다.
const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
? [serverOrOptions, options]
: [this.createHttpAdapter(), serverOrOptions];
// 애플리케이션 레벨의 설정을 관리하는 인스턴스를 생성합니다.
const applicationConfig = new ApplicationConfig();
// applicationConfig를 제공해서 새로운 NestContainer를 생성합니다.
// 해당 컨테이너는 **종속성 주입과 모듈 관리**를 담당합니다!
const container = new NestContainer(applicationConfig);
// 애플리케이션 옵션과 컨테이너를 전달하여 의존성 그래프를 분석하는
// GraphInspector 인스턴스를 생성합니다.
const graphInspector = this.createGraphInspector(appOptions, container);
// 입력값에 따라 초기화 오류 시 애플리케이션 동작을 제어하기 위해 플래그를 생성합니다.
this.setAbortOnError(serverOrOptions, options);
// 옵션을 기반으로 로깅을 설정합니다.
this.registerLoggerConfiguration(appOptions);
// 주요 모듈(moduleCls)를 초기화하고, DI 컨테이너를 설정하고, 제공된 매개변수로
// 초기화 작업을 합니다. 인스턴스들도 만듭니다!
// 해당 글에서 가장 중요하게 다룰 부분입니다.
await this.initialize(
moduleCls,
container,
graphInspector,
applicationConfig,
appOptions,
httpServer,
);
// DI 컨테이너, http 서버, 애플리케이션 설정, 그래프검사기, 옵션을 사용해서
// NestApplication 인스턴스를 생성합니다.
const instance = new NestApplication(
container,
httpServer,
applicationConfig,
graphInspector,
appOptions,
);
// 실제 애플리케이션 인스턴스를 중간 레이어인 프록시나 인터셉터로 감쌉니다.
const target = this.createNestInstance(instance);
// 실제 애플리케이션 인스턴스에 위임하는 프록시를 생성해 반환합니다.
// 적절한 http 서버 어댑터가 사용되도록 보장합니다.
return this.createAdapterProxy<T>(target, httpServer);
}
- NestFactory 클래스의 create 메소드입니다.
- 들어가기전에 먼저 1) nestContainer가 무엇인지 2) initialize하는 메소드가 어떤 기능을 하는지에 대해 알아보려고합니다.
2.1. nestContainer란?
container.ts
// packages/core/injector/container.ts
export class NestContainer {
private readonly globalModules = new Set<Module>();
private readonly moduleTokenFactory = new ModuleTokenFactory();
private readonly moduleCompiler = new ModuleCompiler(this.moduleTokenFactory);
private readonly modules = new ModulesContainer();
private readonly dynamicModulesMetadata = new Map<string, Partial<DynamicModule>>();
private readonly internalProvidersStorage = new InternalProvidersStorage();
private internalCoreModule: Module;
constructor(private readonly _applicationConfig: ApplicationConfig = undefined) {}
public async addModule(metatype: Type<any> | DynamicModule | Promise<DynamicModule>, scope: Type<any>[]): Promise<Module | undefined> {
/* ... */
}
public addProvider(provider: Provider, token: string): string | symbol | Function {
/* ... */
}
public addInjectable(injectable: Provider, token: string, host?: Type<Injectable>) {
/* ... */
}
public addController(controller: Type<any>, token: string) {
/* ... */
}
// ...
}
- NestContainer 클래스입니다. Nest의 모듈들을 관리하는 주체입니다.
- 내부 메소드로 모듈, 프로바이더, injectable을 관리하는 메소드들이 있는 것을 알 수 있습니다.
- 이중에서 addModule, addProvider, addInjectable, addController를 확인하려고 합니다.
2.1.1 addModule
public async addModule(
metatype: ModuleMetatype,
scope: ModuleScope,
): Promise<
| {
moduleRef: Module;
inserted: boolean;
}
| undefined
> {
// 제공된 metatype이 undefined거나 null인 경우를 확인합니다.
// 조건문에 해당하면 유효하지 않은 forward reference임을 나타내는
// UndefinedForwardRefException 예외를 던집니다.
if (!metatype) {
throw new UndefinedForwardRefException(scope);
}
// moduleCompiler를 사용하여 전달된 모듈 클래스인 metatype을
// type, dynamicMetadata, 그리고 고유한 token이 포함된 객체로 컴파일합니다.
// 모듈 클래스를 -> 메타데이터, 타입, 토큰(고유)로 반환한다!
const { type, dynamicMetadata, token } =
await this.moduleCompiler.compile(metatype);
// 컴파일된 모듈 토큰이 이미 modules 맵에 존재하는지 확인합니다.
// 이미 존재한다면, 이 모듈이 이미 추가되었음을 나타냅니다.
if (this.modules.has(token)) {
return {
moduleRef: this.modules.get(token),
inserted: true,
};
}
// 이미 모듈이 추가된게 아닌 새로운 모듈이라면
// 새로운 모듈을 포함하는 객체의 반환문을 시작합니다.
// setModule을 호출하여 새로운 모듈을 modules 컬렉션에 추가합니다.
// 이 메서드는 token, type, dynamicMetadata 값을 포함하는 객체와
// scope를 받아 새로 생성된 모듈 참조를 반환합니다.
return {
moduleRef: await this.setModule(
{
token,
type,
dynamicMetadata,
},
scope,
),
inserted: true,
};
}
- 전달받은 모듈(클래스)를 컴파일하여 필요한 타입, 메타데이터, 토큰으로 뽑아냅니다.
- 해당 모듈이 이미 등록되어있다면 가져와서 반환하고, 등록되어있지 않다면 새로 등록하여 반환합니다.
2.1.2 addProvider
public addProvider(
provider: Provider,
token: string,
enhancerSubtype?: EnhancerSubtype,
): string | symbol | Function {
// token을 사용하여 modules 맵에서 해당 모듈에 대한 참조(moduleRef)를 가져옵니다.
// provider 보다 module이 더 큰 단위이다.
// 한개의 module 맵 안에 provider가 있다.
// 따라서 먼저 module를 토큰을 통해 찾고 해당 모듈에서 토큰을 활용해 provider를 찾아야한다.
const moduleRef = this.modules.get(token);
// 제공자(provider)가 null이거나 undefined이면
// 순환 의존성 예외를 의미하는 CircularDependencyException을 발생시킵니다.
// 예외 발생 시, 모듈 참조가 유효하면 모듈 유형 이름을 전달합니다.
if (!provider) {
throw new CircularDependencyException(moduleRef?.metatype.name);
}
// moduleRef가 유효하지 않으면
// 모듈이 존재하지 않음을 나타내는 UnknownModuleException을 던집니다.
if (!moduleRef) {
throw new UnknownModuleException();
}
// moduleRef에 있는 addProvider 메서드를 호출합니다. (모듈에 있는)
// 제공자와 선택적인 enhancerSubtype을 추가합니다.
// 이 메서드는 고유한 providerKey를 반환합니다!
const providerKey = moduleRef.addProvider(provider, enhancerSubtype);
// providerKey를 사용해 모듈 참조에서 제공자에 대한 참조(providerRef)를 가져옵니다.
const providerRef = moduleRef.getProviderByKey(providerKey);
// DiscoverableMetaHostCollection의 inspectProvider 메서드를 사용해서
// 모든 모듈 컬렉션과 제공자 참조를 분석하고 조사합니다.
DiscoverableMetaHostCollection.inspectProvider(this.modules, providerRef);
// 최종적으로 providerKey를 Function 타입으로 반환합니다.
return providerKey as Function;
}
- 프로바이더가 들어있는 모듈을 먼저 찾고, 해당 모듈에 프로바이더를 추가합니다.
- 모듈이 프로바이더보다 상위 개념에 속해있기 때문입니다.
- 현재 메소드와 moduleRef.addProvider (모듈의 메서드)는 다른 메소드입니다.
2.1.3 addInjectable
public addInjectable(
injectable: Provider,
token: string,
enhancerSubtype: EnhancerSubtype, //제공자 유형
host?: Type<Injectable>,
) {
// modules 맵에 주어진 token이 없으면,
// 해당 모듈이 존재하지 않음을 나타내는 UnknownModuleException 예외를 발생시킵니다.
if (!this.modules.has(token)) {
throw new UnknownModuleException();
}
// token을 사용하여 modules 맵에서 해당 모듈에 대한 참조(moduleRef)를 가져옵니다.
const moduleRef = this.modules.get(token);
// 모듈 참조(moduleRef)의 addInjectable 메서드를 호출하여,
// 해당 제공자(injectable), 유형 확장자(enhancerSubtype), 선택적인 host를 모듈에 추가합니다.
return moduleRef.addInjectable(injectable, enhancerSubtype, host);
}
- 모듈을 찾아서 해당 모듈에 injectable를 추가합니다.
- 마찬가지로 현재 메소드와 moduleRef.addInjectable는 다른 메소드입니다.
- 현재 메서드는 특정 모듈(token으로 식별)에 제공자(injectable)를 추가하는 기능을 수행하지만, 실제 제공자를 모듈에 추가하는 작업은 모듈 자체에서 수행됩니다.
- 현재 살펴보고 있는 addInjectable 메서드는 모듈을 찾아 provider를 추가하는 관리 역할을 수행합니다.
- 해당 메서드에서 호출하는 moduleRef.addInjectable 메서드가 실제 provider를 추가하는 역할을 수행합니다.
2.1.4 addController
public addController(controller: Type<any>, token: string) {
// modules 맵에 주어진 token이 없으면,
// 해당 모듈이 존재하지 않는다는 것을 나타내는 UnknownModuleException 예외를 발생시킵니다.
if (!this.modules.has(token)) {
throw new UnknownModuleException();
}
// token을 사용해 modules 맵에서 해당 모듈에 대한 참조(moduleRef)를 가져옵니다.
const moduleRef = this.modules.get(token);
// 모듈 참조(moduleRef)의 addController 메서드를 호출하여 모듈에 전달된 컨트롤러를 추가합니다.
moduleRef.addController(controller);
// 모듈 참조의 controllers 맵에서 추가된 컨트롤러에 대한 참조(controllerRef)를 가져옵니다.
const controllerRef = moduleRef.controllers.get(controller);
// DiscoverableMetaHostCollection의 inspectController 메서드를 사용해
// 모듈 컬렉션(modules)과 해당 컨트롤러 참조(controllerRef)를 조사 및 분석합니다.
DiscoverableMetaHostCollection.inspectController(
this.modules,
controllerRef,
);
}
- 모듈을 찾아서 해당 모듈에 controller를 추가합니다.
- 마찬가지로 현재 메소드와 moduleRef.addController는 다른 메소드입니다.
2.2 initialize
nest-factory.ts
public async create<T extends INestApplication = INestApplication>(
moduleCls: any,
serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
options?: NestApplicationOptions,
): Promise<T> {
- 이제 다시 nest-factory.ts의 create 메서드로 돌아와서 살펴보겠습니다.
- create 메서드에서는 해당 메서드를 다음과 같은 매개변수로 넘깁니다.
const app = await NestFactory.create(AppModule);
await app.listen(3000);
- 처음에는 위와 같이 모듈 하나(AppModule)만 전달하는데, 어떻게 모든 모듈을 스캔하고 생성하는지에 대한 의문이 생겼었습니다.
- nest-factory container에 전달되는 모듈(moduleCls)은 루트모듈입니다.
- 위 기준 코드를 보았을 때에도 AppModule(루트 모듈)을 추가해서 컨테이너를 create합니다.
- 의존성이 모두 루트 모듈에 연결되어 있기 때문에 결국 다른 모듈과 하위 요소들 모두 스캔과 생성이 됩니다.
nest-factory.ts
// packages/core/nest-factory.ts
private async initialize(
module: any,
container: NestContainer,
config = new ApplicationConfig(),
httpServer: HttpServer = null,
) {
// InstanceLoader는 의존성 주입에 사용될 인스턴스들을 로드하는 클래스입니다.
// 이 인스턴스를 생성하여 의존성을 관리할 컨테이너를 연결합니다.
const instanceLoader = new InstanceLoader(container);
// 메타데이터를 스캔하는 MetadataScanner 클래스의 인스턴스를 생성합니다.
const metadataScanner = new MetadataScanner();
// DependenciesScanner는 의존성 및 모듈을 스캔하고 처리하는 클래스입니다.
// 이 클래스는 컨테이너, 메타데이터 스캐너, 그리고 ApplicationConfig 인스턴스를 사용합니다.
const dependenciesScanner = new DependenciesScanner(
container,
metadataScanner,
config,
);
// 컨테이너에서 HTTP 서버 어댑터를 설정합니다.
container.setHttpAdapter(httpServer);
// abortOnError 속성이 false인 경우에는 teardown으로 rethrow 함수를 설정하고,
// 그렇지 않은 경우에는 undefined로 설정합니다.
const teardown = this.abortOnError === false ? rethrow : undefined;
// HTTP 서버 어댑터가 정의되어 있으면 서버 초기화 메서드를 호출합니다.
await httpServer?.init();
try {
// 로거를 통해 애플리케이션 시작 메시지를 로그로 기록합니다.
this.logger.log(MESSAGES.APPLICATION_START);
// ExceptionsZone의 asyncRun 메서드를 호출해
// 예외를 안전하게 처리할 수 있는 비동기 작업 영역을 시작합니다.
await ExceptionsZone.asyncRun(
async () => {
// 의존성 스캐너(dependenciesScanner)를 사용해 전달된 모듈의 모든 의존성을 스캔합니다.
await dependenciesScanner.scan(module);
// 인스턴스 로더(instanceLoader)를 사용해 모든 의존성의 인스턴스를 생성합니다.
await instanceLoader.createInstancesOfDependencies();
// 의존성 스캐너(dependenciesScanner)를 사용해 애플리케이션의 제공자(Providers)를 적용합니다.
dependenciesScanner.applyApplicationProviders();
},
// asyncRun 블록을 종료하고, 예외가 발생할 경우 teardown을 처리하고 로그 자동 플러시 여부를 설정합니다.
teardown,
this.autoFlushLogs,
);
} catch (e) {
this.handleInitializationError(e);
}
}
- 인스턴스를 직접 로드하고 생성할 instanceLoader, 메타데이터를 스캔하는 metadataScanner, 의존성 및 모듈을 스캔하고 처리하는 DependenciesScanner를 생성합니다.
- 비동기 작업 영역(asyncRun)에서 다음 세가지 작업을 진행합니다.
- 전달된 모듈의 모든 의존성을 스캔합니다. - dependenciesScanner.scan
- 모든 의존성의 인스턴스를 생성합니다. - instanceLoader.createInstancesOfDependencies
- 애플리케이션의 프로바이더를 적용합니다. - dependenciesScanner.applyApplicationProviders
- 해당 세가지 작업을 조금 더 세부적으로 보겠습니다!
- 우선 메소드를 살펴보기 전에, instanceLoader, metadataScanner, DependenciesScanner에 대해 알아보겠습니다.
2.2.1 DependenciesScanner
scanner.ts
// packages/core/scanner.ts
export class DependenciesScanner {
private readonly applicationProvidersApplyMap: ApplicationProviderWrapper[] = [];
constructor(private readonly container: NestContainer, private readonly metadataScanner: MetadataScanner, private readonly graphInspector: GraphInspector, private readonly applicationConfig = new ApplicationConfig()) {}
// 전체 애플리케이션 모듈 구조를 스캔하는 메서드입니다.
// 루트 모듈과 선택적인 재정의(overrides)를 받아 핵심 모듈을 등록하고 모든 의존성 및 모듈을 스캔합니다.
public async scan(module: Type<any>, options?: { overrides?: ModuleOverride[] }) {
// 핵심 모듈을 등록합니다.
await this.registerCoreModule(options?.overrides);
// 전달된 루트 모듈 및 하위 모듈들을 스캔합니다.
await this.scanForModules({
moduleDefinition: module,
overrides: options?.overrides,
});
// 각 모듈에서 의존성을 스캔합니다.
await this.scanModulesForDependencies();
// 각 모듈 간의 의존성 거리를 계산합니다.
this.calculateModulesDistance();
// 모든 컨트롤러 메타데이터에 요청이나 일시적으로 전역 스코프의 향상자(Enhancers)를 추가합니다.
this.addScopedEnhancersMetadata();
// 글로벌 스코프를 모든 모듈에 바인딩합니다.
this.container.bindGlobalScope();
}
// 내부 핵심 모듈을 생성하고 등록하는 메서드입니다.
public async registerCoreModule(overrides?: ModuleOverride[]) {
// 내부 코어 모듈을 생성합니다.
const moduleDefinition = InternalCoreModuleFactory.create(this.container, this, this.container.getModuleCompiler(), this.container.getHttpAdapterHostRef(), this.graphInspector, overrides);
// 생성된 코어 모듈을 스캔하고 인스턴스화합니다.
const [instance] = await this.scanForModules({
moduleDefinition,
overrides,
});
// 생성된 핵심 모듈의 인스턴스를 컨테이너에 등록합니다.
this.container.registerCoreModuleRef(instance);
}
// 모듈 정의와 관련된 매개변수를 받아 모듈을 스캔하는 메서드입니다.
public async scanForModules({ moduleDefinition, lazy, scope = [], ctxRegistry = [], overrides = [] }: ModulesScanParameters): Promise<Module[]> {
// 모듈을 삽입하거나 재정의하고 모듈 참조 및 삽입 여부를 반환합니다.
const { moduleRef: moduleInstance, inserted: moduleInserted } = (await this.insertOrOverrideModule(moduleDefinition, overrides, scope)) ?? {};
// 모듈 재정의가 있는 경우 모듈 정의를 업데이트합니다.
moduleDefinition = this.getOverrideModuleByModule(moduleDefinition, overrides)?.newModule ?? moduleDefinition;
moduleDefinition = moduleDefinition instanceof Promise ? await moduleDefinition : moduleDefinition;
// 컨텍스트 레지스트리에 현재 모듈 정의를 추가합니다.
ctxRegistry.push(moduleDefinition);
// 전달 참조(forward reference)인 경우 참조된 모듈을 가져옵니다.
if (this.isForwardReference(moduleDefinition)) {
moduleDefinition = (moduleDefinition as ForwardReference).forwardRef();
}
// 모듈이 동적 모듈인지 아닌지를 판별하고, 관련 메타데이터를 통해 의존성을 반영합니다.
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 || [])];
// 이미 등록된 모듈 참조들을 관리하는 배열을 초기화합니다.
let registeredModuleRefs = [];
// 각 모듈을 순회하며, 순환 의존성이나 잘못된 모듈의 경우 예외를 발생시킵니다.
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: [].concat(scope, moduleDefinition),
ctxRegistry,
overrides,
lazy,
});
registeredModuleRefs = registeredModuleRefs.concat(moduleRefs);
}
// 모듈 인스턴스가 없으면 현재 등록된 모듈 참조들을 반환합니다.
if (!moduleInstance) {
return registeredModuleRefs;
}
// 지연 로딩 모드인 경우 글로벌 모듈들을 현재 모듈에 바인딩합니다.
if (lazy && moduleInserted) {
this.container.bindGlobalsToImports(moduleInstance);
}
// 현재 모듈 인스턴스와 등록된 모듈 참조들을 결합하여 반환합니다.
return [moduleInstance].concat(registeredModuleRefs);
}
// 각 모듈에서 프로바이더, 컨트롤러 및 내보낸 의존성을 반영하는 메서드입니다.
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);
}
}
// 모듈 간의 거리를 계산하는 메서드입니다.
public calculateModulesDistance() {
// 모듈의 값을 가져오는 제너레이터를 생성합니다.
const modulesGenerator = this.container.getModules().values();
// Skip "InternalCoreModule" from calculating distance
// "InternalCoreModule"은 건너뜁니다.
modulesGenerator.next();
// 모듈 스택을 초기화합니다.
const modulesStack = [];
// 각 모듈 참조에서 거리를 계산하는 재귀 함수입니다.
const calculateDistance = (moduleRef: Module, distance = 1) => {
if (!moduleRef || modulesStack.includes(moduleRef)) {
return;
}
// 현재 모듈 참조를 스택에 추가합니다.
modulesStack.push(moduleRef);
const moduleImports = moduleRef.imports;
moduleImports.forEach((importedModuleRef) => {
if (importedModuleRef) {
if (distance > importedModuleRef.distance) {
importedModuleRef.distance = distance;
}
calculateDistance(importedModuleRef, distance + 1);
}
});
};
// 루트 모듈을 가져옵니다.
const rootModule = modulesGenerator.next().value as Module;
// 루트 모듈에서부터 거리를 계산합니다.
calculateDistance(rootModule);
}
/**
* Add either request or transient globally scoped enhancers
* to all controllers metadata storage
*/
// 전역 스코프의 향상자(Enhancer) 메타데이터를 모든 컨트롤러에 추가합니다.
public addScopedEnhancersMetadata() {
// 애플리케이션 프로바이더 맵을 순회합니다.
iterate(this.applicationProvidersApplyMap)
// 요청이나 일시적으로 전역 스코프인 제공자를 필터링합니다.
.filter((wrapper) => this.isRequestOrTransient(wrapper.scope))
// 각 제공자에 대해 메타데이터를 업데이트합니다.
.forEach(({ moduleKey, providerKey }) => {
// 컨테이너에서 모듈을 가져옵니다.
const modulesContainer = this.container.getModules();
const { injectables } = modulesContainer.get(moduleKey);
const instanceWrapper = injectables.get(providerKey);
// 모듈 값을 순회할 수 있는 프로바이더들을 가져옵니다.
const iterableIterator = modulesContainer.values();
iterate(iterableIterator)
.map((moduleRef) => Array.from<InstanceWrapper>(moduleRef.controllers.values()).concat(moduleRef.entryProviders))
// 모든 컨트롤러 및 엔트리 제공자를 평탄화합니다.
.flatten()
// 각 컨트롤러 또는 엔트리 제공자에 메타데이터를 추가합니다.
.forEach((controllerOrEntryProvider) => controllerOrEntryProvider.addEnhancerMetadata(instanceWrapper));
});
}
}
- 의존성 및 모듈을 스캔하고 처리하는 DependenciesScanner입니다.
- 엄청 많은 기능들을 하고 있습니다! 해당 메서드들은 각 클래스들을 살펴본 후 아래에서 차근차근 살펴보려고 합니다.
- 우선 흐름은 아래와 같습니다.
- 1. 핵심(코어)모듈을 등록합니다. -** 2. 루트 모듈 및 하위 모듈들을 스캔합니다.**
- 3. 각 모듈에서 의존성을 모두 스캔하고 의존성 거리를 계산합니다.
2.2.2 InstanceLoader
instance-loader.ts
// packages/core/injector/instance-loader.ts
export class InstanceLoader<TInjector extends Injector = Injector> {
constructor(
protected readonly container: NestContainer,
protected readonly injector: TInjector,
protected readonly graphInspector: GraphInspector,
private logger: LoggerService = new Logger(InstanceLoader.name, {
timestamp: true,
})
) {}
public setLogger(logger: Logger) {
this.logger = logger;
}
// initialize시 해당 메소드를 호출합니다.
// 해당 메서드는 모든 모듈의 의존성을 로드하고 인스턴스화하는 역할을 수행합니다.
// 기본적으로 container의 모든 모듈을 인수로 사용합니다.
public async createInstancesOfDependencies(modules: Map<string, Module> = this.container.getModules()) {
// 각 모듈의 프로토타입을 생성합니다.
this.createPrototypes(modules);
try {
// 비동기적으로 모든 모듈의 인스턴스를 생성합니다.
await this.createInstances(modules);
} catch (err) {
// 실패 시
// 그래프 인스펙터를 통해 모듈을 검사합니다.
this.graphInspector.inspectModules(modules);
// 발생한 예외를 그래프 인스펙터에 부분적으로 등록합니다.
this.graphInspector.registerPartial(err);
throw err;
}
// 그래프 인스펙터를 통해 모듈을 검사합니다.
this.graphInspector.inspectModules(modules);
}
// createPrototypes 메서드는 각 모듈의 모든 프로토타입을 생성합니다.
private createPrototypes(modules: Map<string, Module>) {
// 각 모듈에 대해 반복처리하며
modules.forEach((moduleRef) => {
// 해당 모듈의 모든 제공자(Provider)의 프로토타입을 생성합니다.
this.createPrototypesOfProviders(moduleRef);
// 해당 모듈의 모든 인젝터블(Injectable)의 프로토타입을 생성합니다.
this.createPrototypesOfInjectables(moduleRef);
// 해당 모듈의 모든 컨트롤러의 프로토타입을 생성합니다.
this.createPrototypesOfControllers(moduleRef);
});
}
// 모든 모듈의 인스턴스를 생성하는 비동기 메서드
private async createInstances(modules: Map<string, Module>) {
// 모든 모듈에 대한 인스턴스 생성을 병렬로 수행하기 위해 Promise.all을 사용하고
// 각 모듈에 대해 비동기 작업을 실행
await Promise.all(
[...modules.values()].map(async (moduleRef) => {
// 해당 모듈의 모든 프로바이더 인스턴스를 생성합니다.
await this.createInstancesOfProviders(moduleRef);
// 해당 모듈의 모든 injectable 인스턴스를 생성합니다.
await this.createInstancesOfInjectables(moduleRef);
// 해당 모듈의 모든 컨트롤러 인스턴스를 생성합니다.
await this.createInstancesOfControllers(moduleRef);
// 모듈 참조에서 모듈의 이름을 가져옵니다.
const { name } = moduleRef;
// 모듈 이름이 화이트리스트에 포함되어 있다면, 해당 모듈 초기화 메시지를 로거에 기록합니다.
this.isModuleWhitelisted(name) && this.logger.log(MODULE_INIT_MESSAGE`${name}`);
})
);
}
// 해당 모듈의 모든 프로바이더 프로토타입을 생성하는 메서드
private createPrototypesOfProviders(moduleRef: Module) {
// 모듈 참조에서 프로바이더 목록을 가져옵니다.
const { providers } = moduleRef;
// 각 프로바이더에 대해 인젝터가 프로토타입을 로드하도록 호출합니다.
providers.forEach((wrapper) => this.injector.loadPrototype<Injectable>(wrapper, providers));
}
// 해당 모듈의 모든 프로바이더 인스턴스를 생성하는 비동기 메서드
private async createInstancesOfProviders(moduleRef: Module) {
// 모듈 참조에서 프로바이더 목록을 가져옵니다.
const { providers } = moduleRef;
// 프로바이더 목록을 배열로 변환합니다.
const wrappers = [...providers.values()];
// 모든 프로바이더에 대한 인스턴스 생성을 병렬로 처리합니다.
await Promise.all(
wrappers.map(async (item) => {
// 각 프로바이더에 대해 인젝터가 해당 제공자를 로드하도록 호출합니다.
await this.injector.loadProvider(item, moduleRef);
// 그래프 인스펙터가 각 인스턴스 래퍼를 검사하도록 합니다.
this.graphInspector.inspectInstanceWrapper(item, moduleRef);
})
);
}
// 해당 모듈의 모든 컨트롤러 프로토타입을 생성하는 메서드
private createPrototypesOfControllers(moduleRef: Module) {
// 모듈 참조에서 컨트롤러 목록을 가져옵니다.
const { controllers } = moduleRef;
// 각 컨트롤러에 대해 인젝터가 프로토타입을 로드하도록 호출합니다.
controllers.forEach((wrapper) => this.injector.loadPrototype<Controller>(wrapper, controllers));
}
// 해당 모듈의 모든 컨트롤러 인스턴스를 생성하는 비동기 메서드
private async createInstancesOfControllers(moduleRef: Module) {
// 모듈 참조에서 컨트롤러 목록을 가져옵니다.
const { controllers } = moduleRef;
// 컨트롤러 목록을 배열로 변환합니다.
const wrappers = [...controllers.values()];
// 모든 컨트롤러에 대한 인스턴스 생성을 병렬로 처리합니다.
await Promise.all(
wrappers.map(async (item) => {
// 각 컨트롤러에 대해 인젝터가 해당 컨트롤러를 로드하도록 호출합니다.
await this.injector.loadController(item, moduleRef);
// 그래프 인스펙터가 각 인스턴스 래퍼를 검사하도록 합니다.
this.graphInspector.inspectInstanceWrapper(item, moduleRef);
})
);
}
// 해당 모듈의 모든 injectable 프로토타입을 생성하는 메서드
private createPrototypesOfInjectables(moduleRef: Module) {
// 모듈 참조에서 injectable 목록을 가져옵니다.
const { injectables } = moduleRef;
// 각 injectable에 대해 인젝터가 프로토타입을 로드하도록 호출합니다.
injectables.forEach((wrapper) => this.injector.loadPrototype(wrapper, injectables));
}
// 해당 모듈의 모든 injectable 인스턴스를 생성하는 비동기 메서드
private async createInstancesOfInjectables(moduleRef: Module) {
// 모듈 참조에서 injectable 목록을 가져옵니다.
const { injectables } = moduleRef;
// injectable 목록을 배열로 변환합니다.
const wrappers = [...injectables.values()];
// 모든 injectable에 대한 인스턴스 생성을 병렬로 처리합니다.
await Promise.all(
wrappers.map(async (item) => {
// 각 인젝터블에 대해 인젝터가 해당 인젝터블을 로드하도록 호출합니다.
await this.injector.loadInjectable(item, moduleRef);
// 그래프 인스펙터가 각 인스턴스 래퍼를 검사하도록 합니다.
this.graphInspector.inspectInstanceWrapper(item, moduleRef);
})
);
}
// 특정 모듈이 화이트리스트에 포함되어 있는지 확인하는 메서드
private isModuleWhitelisted(name: string): boolean {
// 모듈 이름이 InternalCoreModule의 이름과 다르면
// true를 반환하여 화이트리스트에 포함됨을 나타냅니다.
return name !== InternalCoreModule.name;
}
}
- InstanceLoader 는 의존성 주입에 사용될 인스턴스들을 load하는 클래스입니다.
- 해당 인스턴스 로더(instanceLoader)를 사용해 모든 의존성의 인스턴스를 생성 하는데 호출됩니다. (instanceLoader.createInstancesOfDependencies())
- 모든 모듈의 프로토타입 / 인스턴스를 생성합니다.
- 각 모듈의 모든 provider, injectable, controller 인스턴스를 생성합니다.
- 해당 클래스 또한 많은 정보량을 가지고 있습니다! 우선은 모듈의 모든 것을 인스턴스화하는 역할을 한다고 간단히 이해하셔도 좋습니다.
2.2.3 MetadataScanner
metadata-scanner.ts
// packages/core/metadata-scanner.ts
import { Injectable } from "@nestjs/common/interfaces/injectable.interface";
import { isConstructor, isFunction, isNil } from "@nestjs/common/utils/shared.utils";
export class MetadataScanner {
private readonly cachedScannedPrototypes: Map<object, string[]> = new Map();
// scanFromPrototype, getAllFilteredMethodNames 메소드는 deprecated 되었습니다.
// getAllMethodNames로 대체되었습니다.
/**
* @deprecated
* @see {@link getAllMethodNames}
* @see getAllMethodNames
*/
public scanFromPrototype<T extends Injectable, R = any>(instance: T, prototype: object, callback: (name: string) => R): R[] {
if (!prototype) {
return [];
}
const visitedNames = new Map<string, boolean>();
const result: R[] = [];
do {
for (const property of Object.getOwnPropertyNames(prototype)) {
if (visitedNames.has(property)) {
continue;
}
visitedNames.set(property, true);
// reason: https://github.com/nestjs/nest/pull/10821#issuecomment-1411916533
const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
if (descriptor.set || descriptor.get || isConstructor(property) || !isFunction(prototype[property])) {
continue;
}
const value = callback(property);
if (isNil(value)) {
continue;
}
result.push(value);
}
} while ((prototype = Reflect.getPrototypeOf(prototype)) && prototype !== Object.prototype);
return result;
}
/**
* @deprecated
* @see {@link getAllMethodNames}
* @see getAllMethodNames
*/
public *getAllFilteredMethodNames(prototype: object): IterableIterator<string> {
// getAllMethodNames 메서드를 사용해 모든 메서드 이름을 반환합니다.
yield* this.getAllMethodNames(prototype);
}
// 프로토타입에서 모든 메서드 이름을 배열로 반환하는 getAllMethodNames 메서드입니다.
public getAllMethodNames(prototype: object | null): string[] {
// 프로토타입이 없으면 빈 배열을 반환합니다.
if (!prototype) {
return [];
}
// 이미 스캔된 프로토타입이라면 캐시된 메서드 이름 배열을 반환합니다.
if (this.cachedScannedPrototypes.has(prototype)) {
return this.cachedScannedPrototypes.get(prototype);
}
// 방문한 이름을 추적하기 위한 Map을 생성합니다.
const visitedNames = new Map<string, boolean>();
// 빈 배열로 결과를 초기화합니다.
const result: string[] = [];
// 현재 프로토타입을 캐시에 추가합니다.
this.cachedScannedPrototypes.set(prototype, result);
// 프로토타입 체인을 순환하기 위한 do-while 루프를 시작합니다.
do {
// 프로토타입의 모든 속성 이름을 순환합니다.
for (const property of Object.getOwnPropertyNames(prototype)) {
// 이미 방문한 속성이면 다음 반복으로 넘어갑니다.
if (visitedNames.has(property)) {
continue;
}
// 속성 이름을 방문한 것으로 표시합니다.
visitedNames.set(property, true);
// reason: https://github.com/nestjs/nest/pull/10821#issuecomment-1411916533
// 속성 설명자를 가져옵니다.
const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
// 접근자(get, set)가 있거나, 생성자이거나, 함수가 아닌 경우
// 다음 속성으로 넘어갑니다.
if (descriptor.set || descriptor.get || isConstructor(property) || !isFunction(prototype[property])) {
continue;
}
// 결과 배열에 속성 이름을 추가합니다
result.push(property);
}
} while (
// 프로토타입 체인 상위로 이동하며, 최상위 객체(Object.prototype)에 도달할 때까지 반복합니다.
(prototype = Reflect.getPrototypeOf(prototype)) &&
prototype !== Object.prototype
);
return result;
}
}
- 해당 클래스를 통해 프로퍼티에서 모든 depscriptor 정보를 가져올 수 있습니다.
- 특정 프로토타입 객체를 넘겨주면, 가장 상위 부모 클래스(부모가 object)인 클래스까지 프로토타입 체인을 타고 올라가면서 모든 메서드들의 이름을 가져옵니다.
쉬어가기
- 글이 너무 길어지고 있어 나누려고 합니다.
- 다음 글에서는 위에서 장황하게 서술한 instanceLoader, metadataScanner, DependenciesScanner 에 대해 조금 더 깊이 알아보고, 특히 모듈이 어떻게 scan 되는지에 대해 알아보겠습니다.
