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.tscreate 메서드로 돌아와서 살펴보겠습니다.
  • 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 되는지에 대해 알아보겠습니다.