NestJS module는 어떻게 등록되나요? (2)
1. 목표
이전 글과 같이 NestJS 모듈이 초기화되는 내부 구현체를 살펴보고 작동 방식을 이해하며 전체 구조를 파악하는 것이 목표입니다.
Module 데코레이터가 metaData와 함께 어떻게 작동하는지 예시를 통해서 직접 사용해보며 알아보려고 합니다.
2. 기준 코드
간단한 기준 모듈 두개를 작성해두었습니다. 해당 모듈을 통해서 직접 테스트해보며 어떻게 모듈이 등록되는지 확인해볼 예정입니다. (메타데이터 확인 등)
// module
// app.module.ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { TestModule } from "./test.module";
@Module({
imports: [TestModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
// module
// test.module.ts
import { Module } from "@nestjs/common";
@Module({
imports: [],
controllers: [],
providers: [],
})
export class TestModule {}
3. scanForModules
// scanner.ts
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;
// // 비동기적으로 모듈이 정의되었을 때 await를 적용합니다
moduleDefinition =
moduleDefinition instanceof Promise
? await moduleDefinition
: moduleDefinition;
// 컨텍스트 레지스트리에 현재 모듈 정의를 추가합니다.
ctxRegistry.push(moduleDefinition);
// 전달 참조(forward reference)인 경우 참조된 모듈을 가져옵니다.
if (this.isForwardReference(moduleDefinition)) {
moduleDefinition = (moduleDefinition as ForwardReference).forwardRef();
}
// 모듈이 동적 모듈인지 아닌지를 판별하고, 관련 메타데이터를 통해 의존성을 반영합니다.
// import에 있는 모듈들의 메타데이터를 모두 가져옵니다.
const modules = !this.isDynamicModule(
moduleDefinition as Type<any> | DynamicModule,
)
// 동적 모듈이 아니면, 모듈의 imports에 들어가 있는 배열을 가져옵니다.
? this.reflectMetadata(
MODULE_METADATA.IMPORTS,
moduleDefinition as Type<any>,
)
: [
...this.reflectMetadata(
MODULE_METADATA.IMPORTS,
(moduleDefinition as DynamicModule).module,
),
...((moduleDefinition as DynamicModule).imports || []),
];
// 이미 등록된 모듈 참조들을 관리하는 배열을 초기화합니다.
let registeredModuleRefs = [];
// 각 import 된 모듈을 순회하며, 순환 의존성이나 잘못된 모듈의 경우 예외를 발생시킵니다.
for (const [index, innerModule] of modules.entries()) {
// 순환 참조시 undefined
if (innerModule === undefined) {
throw new UndefinedModuleException(moduleDefinition, index, scope);
}
if (!innerModule) {
throw new InvalidModuleException(moduleDefinition, index, scope);
}
// 이미 컨텍스트 레지스트리에 있는 모듈을 건너뜁니다.
if (ctxRegistry.includes(innerModule)) {
continue;
}
// 내부 모듈을 스캔합니다.
// 해당 메소드를 보면 재귀라는 것을 알 수 있습니다.
// scope는 현재 모듈의 부모 모듈들이 루트(AppModule)부터 순서대로 들어있습니다.
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);
}
이어서 다시 scanForModules 메서드부터 살펴보겠습니다.
이전 글에서 사용자가 정의한 모듈 외에 nest 자체에서 필수적으로 필요한 기능들이 담긴 모듈인 InternalCoreModule를 동적으로 만드는 것을 확인하였고, 이제는 해당 모듈을 스캔하는 과정을 살펴볼 차례입니다. 이제 한줄씩 따라가며 살펴볼까요?
const { moduleRef: moduleInstance, inserted: moduleInserted } =
(await this.insertOrOverrideModule(moduleDefinition, overrides, scope)) ??
{};
우선 재정의되는 모듈인지 확인하고 모듈을 컨테이너에 등록합니다.
moduleDefinition: {
module: [class InternalCoreModule],
providers: [ [Object], [Object], [Object], [Object], [Object] ],
exports: [
[class ExternalContextCreator],
[class ModulesContainer extends Map],
[class HttpAdapterHost],
[class LazyModuleLoader],
[Function]
]
}
여기서 반환값인 moduleDefinition은 모듈에 대한 정보로, InternalCoreModule의 경우 DynamicModule 형식으로 위와 같습니다.
// scanner.ts
private async insertOrOverrideModule(
moduleDefinition: ModuleDefinition,
overrides: ModuleOverride[],
scope: Type<unknown>[],
): Promise<
| {
moduleRef: Module;
inserted: boolean;
}
| undefined
> {
// 1. 모듈 재정의 여부 확인
const overrideModule = this.getOverrideModuleByModule(moduleDefinition, overrides);
// 2. 재정의 대상이라면 override 처리
if (overrideModule !== undefined) {
return this.overrideModule(moduleDefinition, overrideModule.newModule, scope);
}
// 3. 그렇지 않다면 새 모듈 삽입
return this.insertModule(moduleDefinition, scope);
}
// scanner.ts
public async insertModule(
moduleDefinition: any,
scope: Type<unknown>[],
): Promise<
| {
moduleRef: Module;
inserted: boolean;
}
| undefined
> {
const moduleToAdd = this.isForwardReference(moduleDefinition)
? moduleDefinition.forwardRef()
: moduleDefinition;
if (
this.isInjectable(moduleToAdd) ||
this.isController(moduleToAdd) ||
this.isExceptionFilter(moduleToAdd)
) {
throw new InvalidClassModuleException(moduleDefinition, scope);
}
return this.container.addModule(moduleToAdd, scope);
}
재정의되는 모듈이라면 overrideModule 메서드를 통해서 재정의하고, 그렇지 않다면 insertModule 메서드를 통해 컨테이너의 모듈에 추가합니다.
InternalCoreModule는 재정의되는 모듈이 아니기 때문에 컨테이너 모듈에 바로 추가됩니다.
// container.ts
public async addModule(
metatype: ModuleMetatype,
scope: ModuleScope,
): Promise<
| {
moduleRef: Module;
inserted: boolean;
}
| undefined
> {
// In DependenciesScanner#scanForModules we already check for undefined or invalid modules
// We still need to catch the edge-case of `forwardRef(() => undefined)`
if (!metatype) {
throw new UndefinedForwardRefException(scope);
}
const { type, dynamicMetadata, token } =
await this.moduleCompiler.compile(metatype);
if (this.modules.has(token)) {
return {
moduleRef: this.modules.get(token),
inserted: true,
};
}
return {
moduleRef: await this.setModule(
{
token,
type,
dynamicMetadata,
},
scope,
),
inserted: true,
};
}
container.addModule 메서드는 위와 같습니다. 모듈 등록 로직에서 사용되는 메서드로, 주어진 모듈을 애플리케이션의 모듈 컨테이너에 추가하는 역할을 합니다. 해당 부분을 핵심 로직으로 볼 수 있는데요, 해당 부분은 NestJS 내부에서 모듈의 의존성을 관리하고, 중복 등록을 방지하며, 동적 모듈을 처리하는 핵심적인 부분입니다.
인자로 받은 metatype은 모듈의 정의인 moduleDefinition이고, scope은 전달하지 않았으니 디폴트값인 빈배열입니다. moduleCompiler.compile 메서드를 통해 컴파일을 진행합니다.
주어진 모듈의 type / dynamicMetadata(동적 메타데이터) / token(모듈의 고유 식별자)을 생성합니다. 값의 예시는 아래와 같습니다. type의 경우 실제 class 정보입니다.
{
token: '3406181f8ff203e0c0e65d89a38852a2e70ba4e4f3ebea16b3a9c06377af97c6',
dynamicMetadata: {
providers: [ [Object], [Object], [Object], [Object], [Object] ],
exports: [
[class ExternalContextCreator],
[class ModulesContainer extends Map],
[class HttpAdapterHost],
[class LazyModuleLoader],
[Function]
]
},
type: [class InternalCoreModule]
}
이때 이미 동일한 token으로 이미 등록된 모듈이 있으면, 중복 등록을 방지하고 기존 모듈을 반환합니다. 따라서 중복된 토큰으로 지정하게 되면 무시되기 때문에 유의해야 합니다.
새로운 모듈인 경우 this.setModule 메서드를 호출하여 실제로 모듈을 등록합니다. setModule은 모듈의 초기화 작업 및 의존성 주입 작업을 수행합니다.
// container.ts
private async setModule(
{ token, dynamicMetadata, type }: ModuleFactory,
scope: ModuleScope,
): Promise<Module | undefined> {
const moduleRef = new Module(type, this);
moduleRef.token = token;
moduleRef.initOnPreview = this.shouldInitOnPreview(type);
this.modules.set(token, moduleRef);
const updatedScope = [].concat(scope, type);
await this.addDynamicMetadata(token, dynamicMetadata, updatedScope);
if (this.isGlobalModule(type, dynamicMetadata)) {
moduleRef.isGlobal = true;
this.addGlobalModule(moduleRef);
}
return moduleRef;
}
드디어 Module 클래스를 생성합니다! Module 클래스의 인스턴스를 생성하여 모듈의 참조를 만들고 token을 설정합니다. 특정 상황에서 미리 초기화가 필요한 경우 initOnPreview 설정 여부를 판단합니다.
그리고 modules.set 메서드를 통해 모듈 컨테이너 Map에 모듈의 인스턴스, 모듈의 참조를 등록해줍니다.
// container.ts
public async addDynamicMetadata(
token: string,
dynamicModuleMetadata: Partial<DynamicModule>,
scope: Type<any>[],
) {
if (!dynamicModuleMetadata) {
return;
}
this.dynamicModulesMetadata.set(token, dynamicModuleMetadata);
const { imports } = dynamicModuleMetadata;
await this.addDynamicModules(imports, scope);
}
public async addDynamicModules(modules: any[], scope: Type<any>[]) {
if (!modules) {
return;
}
await Promise.all(modules.map(module => this.addModule(module, scope)));
}
이후 addDynamicMetadata 메서드를 거치게 되는데요, 해당 메서드를 통해 동적 모듈 메타데이터를 처리하고, 필요한 모듈(예시로, 동적으로 imports된 모듈)을 추가합니다. 여기서 동적 모듈로 추가된 imports는 추가하는데, 정적 모듈에서 명시했던 imports는 왜 지금 추가하지 않는가에 대한 의문이 생겼었습니다. 이는 동적 모듈은 런타임에서 구성이 완료되므로, 내부 imports를 미리 처리하여 의존성 준비 상태를 보장해야 하기 때문입니다.
현재 살펴보고 있는 InternalCoreModule은 DynamicModule인데요, 이에 따라서 모듈을 컴파일할 때 아래와 같이 dynamicModuleMetadata를 반환했었습니다.
{
dynamicModuleMetadata: {
providers: [ [Object], [Object], [Object], [Object], [Object] ],
exports: [
[class ExternalContextCreator],
[class ModulesContainer extends Map],
[class HttpAdapterHost],
[class LazyModuleLoader],
[Function]
]
}
}
dynamicModuleMetadata는 기존 모듈의 정보에 동적으로 추가했던 정보들입니다. 해당 메타데이터에서 imports를 뽑아와서 모두 addModule 메서드를 통해 모듈에 추가하는데요, InternalCoreModule의 경우는 imports가 없어 따로 추가하지는 않습니다.
const { moduleRef: moduleInstance, inserted: moduleInserted } =
(await this.insertOrOverrideModule(moduleDefinition, overrides, scope)) ??
{};
이제 다시 돌아와보겠습니다. 모듈을 컴파일하고 NestContainer moduled에 등록한 과정을 통해 반환된 moduleInserted, moduleRef의 값은 어떠할까요?
{ moduleInserted: true }
{
moduleInstance: Module {
_metatype: [class InternalCoreModule],
container: NestContainer {
_applicationConfig: [ApplicationConfig],
globalModules: [Set],
moduleTokenFactory: [ModuleTokenFactory],
moduleCompiler: [ModuleCompiler],
modules: [ModulesContainer [Map]],
dynamicModulesMetadata: [Map],
internalProvidersStorage: [InternalProvidersStorage],
_serializedGraph: [SerializedGraph]
},
_imports: Set(0) {},
_providers: Map(3) {
[class InternalCoreModule] => [InstanceWrapper],
[class ModuleRef extends AbstractInstanceResolver] => [InstanceWrapper],
[class ApplicationConfig] => [InstanceWrapper]
},
_injectables: Map(0) {},
_middlewares: Map(0) {},
_controllers: Map(0) {},
_entryProviderKeys: Set(0) {},
_exports: Set(0) {},
_distance: 0,
_initOnPreview: false,
_isGlobal: true,
_id: '22153c21c894fd2c361eb',
_token: 'cb378e2bcb73afe913e28e0375bf350d01480606684ec73fb3780ad1e65ef13e'
}
}
추가가 성공적으로 되었으니 moduleInserted는 true입니다. moduleInstance는 컴파일을 통해 발행한 고유식별자(token), 메타 타입(type), NestContainer가 담겨있는 것을 확인할 수 있습니다.
moduleDefinition =
this.getOverrideModuleByModule(moduleDefinition, overrides)?.newModule ??
moduleDefinition;
- 이제 이어서 아래 코드인 moduleDefinition 변수 정의 코드를 살펴보겠습니다.
// scanner.ts
private getOverrideModuleByModule(
module: ModuleDefinition,
overrides: ModuleOverride[],
): ModuleOverride | undefined {
// 1. ForwardReference 처리: 순환 참조 모듈인지 확인
if (this.isForwardReference(module)) {
return overrides.find(moduleToOverride => {
return (
moduleToOverride.moduleToReplace === module.forwardRef() ||
(
moduleToOverride.moduleToReplace as ForwardReference
).forwardRef?.() === module.forwardRef()
);
});
}
// 2. 일반 모듈인지 확인
return overrides.find(
moduleToOverride => moduleToOverride.moduleToReplace === module,
);
}
다음은 getOverrideModuleByModule 메서드를 통해서 모듈 정보를 가져옵니다.
만약에 모듈이 순환 참조 모듈이라면, getOverrideModuleByModule 메서드가 isForwardReference 메서드를 통해 분기 처리되어 순환 참조를 고려하여 작동합니다.
하지만 InternalCoreModule는 재정의되는 모듈이 아니기 때문에 그대로 사용됩니다.
{
moduleDefinition: {
module: [class InternalCoreModule],
providers: [ [Object], [Object], [Object], [Object], [Object] ],
exports: [
[class ExternalContextCreator],
[class ModulesContainer extends Map],
[class HttpAdapterHost],
[class LazyModuleLoader],
[Function]
]
}
}
moduleDefinition은 위와 같이 DynamicModule의 정보입니다.
// 모듈이 동적 모듈인지 아닌지를 판별하고, 관련 메타데이터를 통해 의존성을 반영합니다.
// import에 있는 모듈들의 메타데이터를 모두 가져옵니다.
const modules = !this.isDynamicModule(
moduleDefinition as Type<any> | DynamicModule,
)
// 동적 모듈이 아니면, 모듈의 imports에 들어가 있는 배열을 가져옵니다.
? this.reflectMetadata(
MODULE_METADATA.IMPORTS,
moduleDefinition as Type<any>,
)
: [
...this.reflectMetadata(
MODULE_METADATA.IMPORTS,
(moduleDefinition as DynamicModule).module,
),
...((moduleDefinition as DynamicModule).imports || []),
];
이제 또 이어서 scanForModules 메서드를 살펴보겠습니다. moduleDefinition은 DynamicModule인데요, 이에 따라서 modules는 아래 배열로 정의됩니다.
[
...this.reflectMetadata(
MODULE_METADATA.IMPORTS,
(moduleDefinition as DynamicModule).module,
),
...((moduleDefinition as DynamicModule).imports || []),
];
InternalCoreModule의 imports만 reflectMetadata를 통해서 가져오는 것인데요, 동적 모듈을 통해 추가된 import와 기존 모듈의 import를 모두 합칩니다.
해당 modules 변수는 사실 특정 모듈이 import한 모듈들을 모두 저장해두는 배열이라고 보시면 됩니다.
export const MODULE_METADATA = {
IMPORTS: 'imports',
PROVIDERS: 'providers',
CONTROLLERS: 'controllers',
EXPORTS: 'exports',
};
참고) MODULE_METADATA.IMPORTS는 imports이다.
{ modules: [] }
하지만 InternalCoreModule는 import가 없습니다.
return [moduleInstance].concat(registeredModuleRefs);
따라서 import를 모두 스캔하는 재귀 과정은 건너뛰게 되고 바로 scanForModules 메서드는 리턴값을 반환하고 종료됩니다.
// 최종 반환값
[
Module {
_metatype: [class InternalCoreModule],
container: [NestContainer],
_imports: Set(0) {},
_providers: [Map],
_injectables: Map(0) {},
_middlewares: Map(0) {},
_controllers: Map(0) {},
_entryProviderKeys: Set(0) {},
_exports: Set(0) {},
_distance: 0,
_initOnPreview: false,
_isGlobal: true,
_id: 'fcdd3630f21673eb9d18a',
_token: '1c4da526dc8f413e3ed4ac23049cb37c21e96e7b37a7de6a79950c56cd609104'
}
]
InternalCoreModule의 경우 scanForModules 메서드에서 바로 위와 같은 결과를 반환하고 끝냅니다. moduleInstance는 InternalCoreModule의 정보입니다. registeredModuleRefs는 없습니다. (import한 값들의 정보)
4. 다시 registerCoreModule 메서드
// scanner.ts
const [instance] = await this.scanForModules({
moduleDefinition,
overrides,
});
this.container.registerCoreModuleRef(instance);
scanForModules 메서드의 처리가 끝났으니 이제 아래 작업을 마무리합니다. instance는 위에서 명시한 Module 객체입니다.
해당 객체를 registerCoreModuleRef 매서드를 통해 NestContainer에 등록합니다. 해당 메서드를 통해 아래와 같이 internalCoreModule에 저장하고, 컨테이너에서 관리하는 모듈 목록에도 추가됩니다.
// container.ts
public registerCoreModuleRef(moduleRef: Module) {
this.internalCoreModule = moduleRef;
this.modules[InternalCoreModule.name] = moduleRef;
}
이렇게 되면 이제 InternalCoreModule가 NestContainer의 모듈로 등록된 상태가 됩니다.
5. 다시 dependenciesScanner.scan 메서드
InternalCoreModule를 등록했으니, 이제 가장 부모 모듈인 AppModule을 등록하는 과정이 남았습니다.
// scanner.ts
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();
this.addScopedEnhancersMetadata();
this.container.bindGlobalScope();
}
scanForModules 메서드는 InternalCoreModule를 등록했을 때 사용했던 메서드와 같은 메서드인데요, 동일하게 메타데이터를 분석하고, imports에 있는 모든 의존성들의 정보를 재귀적으로 가져오는 방식입니다.
이번에는 AppModule을 인자로 받은 scanForModules 메서드에 대해 한줄씩 따라가 볼까요?
const { moduleRef: moduleInstance, inserted: moduleInserted } =
(await this.insertOrOverrideModule(moduleDefinition, overrides, scope)) ??
{};
우선 재정의되는 모듈인지 확인하고 모듈을 컨테이너에 등록합니다.
// scanner.ts
private async insertOrOverrideModule(
moduleDefinition: ModuleDefinition,
overrides: ModuleOverride[],
scope: Type<unknown>[],
): Promise<
| {
moduleRef: Module;
inserted: boolean;
}
| undefined
> {
// 1. 모듈 재정의 여부 확인
const overrideModule = this.getOverrideModuleByModule(moduleDefinition, overrides);
// 2. 재정의 대상이라면 override 처리
if (overrideModule !== undefined) {
return this.overrideModule(moduleDefinition, overrideModule.newModule, scope);
}
// 3. 그렇지 않다면 새 모듈 삽입
return this.insertModule(moduleDefinition, scope);
}
public async insertModule(
moduleDefinition: any,
scope: Type<unknown>[],
): Promise<
| {
moduleRef: Module;
inserted: boolean;
}
| undefined
> {
const moduleToAdd = this.isForwardReference(moduleDefinition)
? moduleDefinition.forwardRef()
: moduleDefinition;
if (
this.isInjectable(moduleToAdd) ||
this.isController(moduleToAdd) ||
this.isExceptionFilter(moduleToAdd)
) {
throw new InvalidClassModuleException(moduleDefinition, scope);
}
return this.container.addModule(moduleToAdd, scope);
}
예시의 AppModule의 경우 재정의되는 모듈이 아니기 때문에 insertModule 메서드를 통해 컨테이너의 모듈에 추가합니다.
// container.ts
public async addModule(
metatype: ModuleMetatype,
scope: ModuleScope,
): Promise<
| {
moduleRef: Module;
inserted: boolean;
}
| undefined
> {
// In DependenciesScanner#scanForModules we already check for undefined or invalid modules
// We still need to catch the edge-case of `forwardRef(() => undefined)`
if (!metatype) {
throw new UndefinedForwardRefException(scope);
}
const { type, dynamicMetadata, token } =
await this.moduleCompiler.compile(metatype);
if (this.modules.has(token)) {
return {
moduleRef: this.modules.get(token),
inserted: true,
};
}
return {
moduleRef: await this.setModule(
{
token,
type,
dynamicMetadata,
},
scope,
),
inserted: true,
};
}
이제 container.addModule 메서드를 통해 모듈을 만들텐데요, 기존에 보았던 방식대로 인자로 받은 metatype은 모듈의 정의인 moduleDefinition이고, scope은 전달하지 않았으니 디폴트값인 빈배열입니다.
우선 this.moduleCompiler.compile 메서드를 통해 컴파일을 진행합니다. 주어진 모듈의 type / dynamicMetadata(동적 메타데이터) / token(모듈의 고유 식별자)을 생성합니다. 값의 예시는 아래와 같습니다. type의 경우 실제 class 정보입니다.
{
type: [class AppModule],
dynamicMetadata: undefined,
token: '2254555b5d685e1e5df631a435843daccb150403e09ce923828af542151f001a'
}
새로운 모듈이기 때문에 this.setModule 메서드를 호출하여 실제로 모듈을 등록합니다.
// container.ts
private async setModule(
{ token, dynamicMetadata, type }: ModuleFactory,
scope: ModuleScope,
): Promise<Module | undefined> {
const moduleRef = new Module(type, this);
moduleRef.token = token;
moduleRef.initOnPreview = this.shouldInitOnPreview(type);
this.modules.set(token, moduleRef);
const updatedScope = [].concat(scope, type);
await this.addDynamicMetadata(token, dynamicMetadata, updatedScope);
if (this.isGlobalModule(type, dynamicMetadata)) {
moduleRef.isGlobal = true;
this.addGlobalModule(moduleRef);
}
return moduleRef;
}
여기서 Module 클래스를 생성합니다. Module 클래스의 인스턴스를 생성하여 모듈의 참조를 만들고 token을 설정합니다. 그리고 modules.set 메서드를 통해 모듈 컨테이너 Map에 모듈의 인스턴스, 모듈의 참조를 등록해줍니다.
이때 AppModule의 경우 DynamicModule이 아니기 때문에 addDynamicMetadata 메서드를 실행하지 않습니다.
addDynamicMetadata를 통해 동적으로 추가된 imports가 setModule를 통해 인스턴스화되는 것을 알 수 있었습니다. 여기서 혼동하지 말아야 할 것은 Dynamic하게 설정된 imports가 그런 것이지 기존 모듈에 명시된 import의 경우 여기서 인스턴스화하지 않습니다.
const { moduleRef: moduleInstance, inserted: moduleInserted } =
(await this.insertOrOverrideModule(moduleDefinition, overrides, scope)) ??
{};
이제 또 다시 돌아와보겠습니다. 모듈을 컴파일하고 NestContainer moduled에 등록한 과정을 통해 반환된 moduleInserted, moduleRef의 값은 어떠할까요?
{
moduleInstance: Module {
_metatype: [class AppModule],
container: NestContainer {
_applicationConfig: [ApplicationConfig],
globalModules: [Set],
moduleTokenFactory: [ModuleTokenFactory],
moduleCompiler: [ModuleCompiler],
modules: [ModulesContainer [Map]],
dynamicModulesMetadata: [Map],
internalProvidersStorage: [InternalProvidersStorage],
_serializedGraph: [SerializedGraph],
internalCoreModule: [Module]
},
_imports: Set(0) {},
_providers: Map(3) {
[class AppModule] => [InstanceWrapper],
[class ModuleRef extends AbstractInstanceResolver] => [InstanceWrapper],
[class ApplicationConfig] => [InstanceWrapper]
},
_injectables: Map(0) {},
_middlewares: Map(0) {},
_controllers: Map(0) {},
_entryProviderKeys: Set(0) {},
_exports: Set(0) {},
_distance: 0,
_initOnPreview: false,
_isGlobal: false,
_id: 'a5845b8d339886ef17680',
_token: 'e521bfd8748aa84f7a6f70e230f3f65f626c3c8e97c59fe73d6af1b1df3397e2'
}
}
{ moduleInserted: true }
추가된 AppModule의 moduleInstance는 위와 같습니다.
moduleDefinition =
this.getOverrideModuleByModule(moduleDefinition, overrides)?.newModule ??
moduleDefinition;
getOverrideModuleByModule 메서드를 살펴볼까요? AppModule은 순환 참조 모듈이 아니기 때문에 그대로 사용합니다.
{ moduleDefinition: [class AppModule] }
// 모듈이 동적 모듈인지 아닌지를 판별하고, 관련 메타데이터를 통해 의존성을 반영합니다.
// import에 있는 모듈들의 메타데이터를 모두 가져옵니다.
const modules = !this.isDynamicModule(
moduleDefinition as Type<any> | DynamicModule,
)
// 동적 모듈이 아니면, 모듈의 imports에 들어가 있는 배열을 가져옵니다.
? this.reflectMetadata(
MODULE_METADATA.IMPORTS,
moduleDefinition as Type<any>,
)
: [
...this.reflectMetadata(
MODULE_METADATA.IMPORTS,
(moduleDefinition as DynamicModule).module,
),
...((moduleDefinition as DynamicModule).imports || []),
];
이어서 살펴보면 AppModule은 DynamicModule이 아닙니다. 이에 따라서 modules는 아래 값으로 정의됩니다.
this.reflectMetadata(
MODULE_METADATA.IMPORTS,
moduleDefinition as Type<any>,
)
기준코드를 살펴보면 AppModule는 imports에 TestModule을 넣어두었었죠? 따라서 reflectMetadata로 뽑아오면 아래와 같은 배열이 반환됩니다.
{ modules: [ [class TestModule] ] }
// 이미 등록된 모듈 참조들을 관리하는 배열을 초기화합니다.
let registeredModuleRefs = [];
// 각 import 된 모듈을 순회하며, 순환 의존성이나 잘못된 모듈의 경우 예외를 발생시킵니다.
for (const [index, innerModule] of modules.entries()) {
// 순환 참조시 undefined
if (innerModule === undefined) {
throw new UndefinedModuleException(moduleDefinition, index, scope);
}
if (!innerModule) {
throw new InvalidModuleException(moduleDefinition, index, scope);
}
// 이미 컨텍스트 레지스트리에 있는 모듈을 건너뜁니다.
if (ctxRegistry.includes(innerModule)) {
continue;
}
// 내부 모듈을 스캔합니다.
// 해당 메소드를 보면 재귀라는 것을 알 수 있습니다.
// scope는 현재 모듈의 부모 모듈들이 루트(AppModule)부터 순서대로 들어있습니다.
const moduleRefs = await this.scanForModules({
moduleDefinition: innerModule,
scope: [].concat(scope, moduleDefinition),
ctxRegistry,
overrides,
lazy,
});
// 재귀를 다 돌고 자식 모듈의 자식 모듈까지 모두 스캔을 완료하였다면 등록합니다.
registeredModuleRefs = registeredModuleRefs.concat(moduleRefs);
}
이제 해당 배열을 순회하며 재귀를 도는데요, imports의 imports까지 모든 요소를 돌며 관련된 요소를 추가하며 최종적으로 registeredModuleRefs 배열을 완성시킵니다.
AppModule의 경우 해당 과정을 통해 imports에 있던 TestModule까지 모두 인스턴스화되어서 registeredModuleRefs에 추가됩니다.
return [moduleInstance].concat(registeredModuleRefs);
최종적으로 moduleInstance와 관련된 요소들인 registeredModuleRefs 합쳐진 배열을 반환합니다.
AppModule의 경우 import한 testModule까지 추가하여 아래와 같은 결과로 반환됩니다. 이제 AppModule도 등록되었네요.
[
Module {
_metatype: [class AppModule],
container: [NestContainer],
_imports: Set(0) {},
_providers: [Map],
_injectables: Map(0) {},
_middlewares: Map(0) {},
_controllers: Map(0) {},
_entryProviderKeys: Set(0) {},
_exports: Set(0) {},
_distance: 0,
_initOnPreview: false,
_isGlobal: false,
_id: '45e8beb3724881ea519ac',
_token: 'd5f406c9fbe8532f6bb1d632de9e0ae2f9094741e0f5c0388ad8e9c703b73a97'
},
Module {
_metatype: [class TestModule],
container: [NestContainer],
_imports: Set(0) {},
_providers: [Map],
_injectables: Map(0) {},
_middlewares: Map(0) {},
_controllers: Map(0) {},
_entryProviderKeys: Set(0) {},
_exports: Set(0) {},
_distance: 0,
_initOnPreview: false,
_isGlobal: false,
_id: 'eb3724881ea519ac06121',
_token: 'e9b62eedf4174b4f3dc3bdaab55d6f273d06795ee4bd5f6492abd8b7eb3fb26a'
}
]
TestModule 또한 같은 과정으로 등록됩니다.
6. 정리하며
이제까지의 과정을 시퀀스 다이어그램으로 정리하고자 합니다.
- InternalCoreModuleFactory의 create 메서드를 이용해 InternalCoreModule을 먼저 생성한다.
- 이후 scanForModules 메서드가 해당 InternalCoreModule을 스캔하면서 모듈을 인스턴스화하고, 이를 NestContainer에 등록한다.
- AppModule(루트 모듈) 또한 scanForModules 메서드로 스캔되어 모듈이 인스턴스화되고, imports 정보를 모두 가져온 뒤 이 정보를 토대로 자식 모듈도 모두 인스턴스화 하며 NestContainer에 모듈을 등록한다.
7. 이어가며
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();
this.addScopedEnhancersMetadata();
this.container.bindGlobalScope();
}
scanner.scan 메서드의 registerCoreModule, scanForModules 메서드를 통해 InternalCoreModule와 선언한 모듈들을 스캔하고 인스턴스화해서 등록하는 과정을 살펴보았습니다.
다만 scanForModules 메서드는 모듈의 값과 imports에 있는 값들을 모두 재귀로 돌아서 스캔하여 최종 정보를 반환하지만, 코드상으로 사용하지 않고 있으며 해당 배열로는 각각의 계층 관계를 알 수 없습니다. 그렇다면 이렇게 인스턴스화된 각각의 모듈들의 관계는 어떻게 알고, 실제로 어떻게 사용되는 것일까요?
해당 내용에 대해 이어서 scanModulesForDependencies 메서드 부터 살펴보며 알아가보도록 하겠습니다.
