NestJS 컨테이너, DI는 어떻게 작동할까 (3)

다시 들어가며


  • 이어서 instanceLoader.createInstancesOfDependencies() 에 대해 알아보고, 의존성에 대한 인스턴스가 어떻게 만들어지고 컨테이너 초기화가 어떻게 진행되어 마무리되는지 알아보겠습니다.

1. IoC 컨테이너 부터 확인해보자


await ExceptionsZone.asyncRun(
  async () => {
    // 의존성 스캐너(dependenciesScanner)를 사용해 전달된 모듈의 모든 의존성을 스캔합니다.
    await dependenciesScanner.scan(module);

    // 인스턴스 로더(instanceLoader)를 사용해 모든 의존성의 인스턴스를 생성합니다.
    await instanceLoader.createInstancesOfDependencies(); // 확인 필요!

    // 의존성 스캐너(dependenciesScanner)를 사용해 애플리케이션의 제공자(Providers)를 적용합니다.
    dependenciesScanner.applyApplicationProviders();
  },
  // asyncRun 블록을 종료하고, 예외가 발생할 경우 teardown을 처리하고 로그 자동 플러시 여부를 설정합니다.
  teardown,
  this.autoFlushLogs
);
  • NestFactory.create 메서드에서 이번에는 instanceLoader.createInstancesOfDependencie를 살펴보려고 합니다.

2. InstanceLoader


createInstancesOfDependencies


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);
  }
  • initialize시 위에서 알아본 dependenciesScanner.scan() 이후에 해당 메소드를 호출합니다.
  • 해당 메서드는 모든 모듈의 의존성을 로드하고 인스턴스화하는 역할을 수행합니다. 기본적으로 container모든 모듈을 인수로 사용합니다.

createPrototypes


// packages/core/injector/instance-loader.ts
private createPrototypes(modules: Map<string, Module>) {
  modules.forEach(moduleRef => {
    this.createPrototypesOfProviders(moduleRef);
    this.createPrototypesOfInjectables(moduleRef);
    this.createPrototypesOfControllers(moduleRef);
  });
}

private createPrototypesOfProviders(moduleRef: Module) {
  const { providers } = moduleRef;
  providers.forEach(wrapper =>
    this.injector.loadPrototype<Injectable>(wrapper, providers),
  );
}

private createPrototypesOfControllers(moduleRef: Module) {
  const { controllers } = moduleRef;
  controllers.forEach(wrapper =>
    this.injector.loadPrototype<Controller>(wrapper, controllers),
  );
}

private createPrototypesOfInjectables(moduleRef: Module) {
  const { injectables } = moduleRef;
  injectables.forEach(wrapper =>
    this.injector.loadPrototype(wrapper, injectables),
  );
}
  • createPrototypes 메서드는 각 모듈의 모든 프로토타입을 생성합니다.
  • injector.loadPrototype을 알아보겠습니다.
// packages/core/injector/injector.ts
public loadPrototype<T>(
  { token }: InstanceWrapper<T>,
  collection: Map<InstanceToken, InstanceWrapper<T>>,
  contextId = STATIC_CONTEXT,
) {
  // contextId = STATIC_CONTEXT
  if (!collection) {
    return;
  }

  // 토큰으로 메타데이터를 불러옵니다.
  const target = collection.get(token);

  // 프로토타입을 생성합니다.
  const instance = target.createPrototype(contextId);

  // 만약 새로 생성되었다면
  if (instance) {
    // InstanceWrapper로 한 번 감쌉니다.
    const wrapper = new InstanceWrapper({
      ...target,
      instance,
    });
    // 주어진 collection에 등록합니다. ex) collection: providers | controllers |injectables
    collection.set(token, wrapper);
  }
}
  • 토큰으로 메타데이터를 불러오고, 해당 정보를 바탕으로 프로토타입을 생성한 후 컬렉션에 등록합니다.
  • 여기서 사용하는 createPrototype 메서드에서도 더 알아보겠습니다.
// packages/core/injector/instance-wrapper.ts
public createPrototype(contextId: ContextId) {
  const host = this.getInstanceByContextId(contextId);
  // 인스턴스가 만들어졌다면 반환하지 않습니다.
  if (!this.isNewable() || host.isResolved) {
    return;
  }

  // 새로 만들어서 반환한다.
  return Object.create(this.metatype.prototype);
}

public getInstanceByContextId(
  contextId: ContextId,
  inquirerId?: string,
): InstancePerContext<T> {
  // 프로바이더 스코프가 TRANSIENT && inquirerId 존재
  if (this.scope === Scope.TRANSIENT && inquirerId) {
    return this.getInstanceByInquirerId(contextId, inquirerId);
  }

  // 해당 맥락에서의 인스턴스를 가져옵니다.
  const instancePerContext = this.values.get(contextId);
  return instancePerContext
    // 이미 해당 맥락 내에서 인스턴스가 존재한다면 그대로 반환합니다.
    ? instancePerContext
    // 존재하지 않으면, 정적 인스턴스로부터 복사해서 만들어냅니다.
    : this.cloneStaticInstance(contextId);
}
  • InstanceLoader는 의존성 주입에 사용될 인스턴스들을 load하는 클래스입니다.
  • 해당 instanceLoader를 사용해 모든 의존성의 인스턴스를 생성하는데 호출됩니다. (instanceLoader.createInstancesOfDependencies())
  • 모든 모듈의 프로토타입 / 인스턴스를 생성합니다.
    • 각 모듈의 provider, injectable, controller 인스턴스를 생성합니다.

참고) 프로토타입은 왜 생성할까?


  • NestJS에서 프로토타입을 생성하는 것은 의존성 주입(DI)에서 성능과 효율성을 높이기 위한 방법 중 하나입니다.
  • 특정 클래스의 인스턴스를 생성하는 대신 클래스의 프로토타입만 생성하고, 이를 사용해 나중에 인스턴스 생성에 활용합니다.
  • 역할
    • 기본 속성 제공: 프로토타입은 객체가 상속받을 기본 속성들을 정의하는 역할을 합니다. 새 인스턴스를 생성할 때 클래스의 프로토타입을 복사하여 상속 구조를 유지합니다.
    • 메서드 재사용: 프로토타입을 통해 모든 인스턴스가 동일한 메서드를 공유하게 되어 메모리를 절약하고, 메서드 호출 시 프로토타입 체인을 활용해 성능을 최적화합니다.
    • 메모리 절약: 프로토타입을 미리 만들어두고 인스턴스 생성 시 이를 활용하면, 새로운 인스턴스를 만들 때마다 클래스 메타타입의 메서드와 속성을 복사할 필요가 없어 메모리 사용량을 줄일 수 있습니다.
    • 성능 향상: 프로토타입을 통해 인스턴스가 필요한 메서드를 모두 공유하게 하므로 중복된 메서드를 인스턴스마다 생성할 필요가 없어 성능을 향상시킵니다.
    • DI 컨테이너 관리: 의존성 주입 컨테이너가 각 인스턴스에 대한 프로토타입을 미리 생성하고 관리함으로써, 인스턴스 간의 복잡한 의존 관계를 보다 효율적으로 다룰 수 있습니다.

createInstances


 // 모든 모듈의 인스턴스를 생성하는 비동기 메서드
  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 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 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 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;
  }
  • createInstances 메서드는 각 모듈 별 프로바이더, injectable, controller를 불러오고 인스턴스화 시킵니다.
  • 우선 대표적으로 불러오는 부분인 injector.loadProvider을 알아보겠습니다.
 // packages/core/injector/injector.ts
 public async loadProvider(
    wrapper: InstanceWrapper<Injectable>,
    moduleRef: Module,
    contextId = STATIC_CONTEXT,
    inquirer?: InstanceWrapper,
  ) {
    const providers = moduleRef.providers;
    await this.loadInstance<Injectable>(
      wrapper,
      providers,
      moduleRef,
      contextId,
      inquirer,
    );
    await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
  }

  • 프로바이더를 뽑아서 loadInstance 메서드로 인스턴스를 로드합니다.
  • loadInstance를 조금 더 상세하게 보겠습니다.
 // packages/core/injector/injector.ts
public async loadInstance<T>(
    wrapper: InstanceWrapper<T>, // provider
    collection: Map<InjectionToken, InstanceWrapper>, // metadatas
    moduleRef: Module, // module
    contextId = STATIC_CONTEXT,
    inquirer?: InstanceWrapper,
  ) {
    // inquirer에 대한 고유 식별자를 가져옵니다.
    const inquirerId = this.getInquirerId(inquirer);

    // contextId와 inquirerId를 이용하여 현재 컨텍스트의 인스턴스 호스트를 가져옵니다.
    const instanceHost = wrapper.getInstanceByContextId(
      this.getContextId(contextId, wrapper),
      inquirerId,
    );

    // 해당 인스턴스 호스트가 아직 초기화 중인지 확인합니다.
    if (instanceHost.isPending) {
      // wrapper에 있는 정착 신호(settlementSignal)를 가져옵니다.
      const settlementSignal = wrapper.settlementSignal;

      // inquirer가 존재하고, 해당 신호가 순환 의존성에 속한다면
      if (inquirer && settlementSignal?.isCycle(inquirer.id)) {
        // "${wrapper.name}"로 표시되는 순환 의존성 예외를 발생시킵니다.
        throw new CircularDependencyException(`"${wrapper.name}"`);
      }

      // 초기화가 진행 중인 경우, 초기화 완료 후에 donePromise로 오류를 확인합니다.
      return instanceHost.donePromise.then((err?: unknown) => {
        // 오류가 있다면 오류를 발생합니다.
        if (err) {
          throw err;
        }
      });
    }

    // 해당 인스턴스 호스트와 wrapper에 정착 신호를 적용합니다.
    const settlementSignal = this.applySettlementSignal(instanceHost, wrapper);
    // wrapper의 토큰 또는 이름을 token으로 가져옵니다.
    const token = wrapper.token || wrapper.name;

    // wrapper의 inject 속성을 추출합니다.
    const { inject } = wrapper;

    // 전달된 토큰으로부터 해당 targetWrapper를 collection 맵에서 가져옵니다.
    const targetWrapper = collection.get(token);

    // 대상 wrapper가 정의되어 있지 않으면 에러 발생
    if (isUndefined(targetWrapper)) {
      throw new RuntimeException();
    }

    // 인스턴스 호스트가 이미 resolve되었다면 정착 신호를 완료로 설정하고 반환합니다.
    if (instanceHost.isResolved) {
      return settlementSignal.complete();
    }
    try {
      // 현재 시간을 t0로 가져옵니다.
      const t0 = this.getNowTimestamp();
      // 인스턴스 배열을 매개변수로 하는 비동기 콜백을 정의합니다.
      const callback = async (instances: unknown[]) => {
        // 주입할 프로퍼티를 비동기적으로 확인합니다.
        const properties = await this.resolveProperties(
          wrapper,
          moduleRef,
          inject as InjectionToken[],
          contextId,
          wrapper,
          inquirer,
        );
        // **클래스 인스턴스를 생성합니다.**
        const instance = await this.instantiateClass(
          instances,
          wrapper,
          targetWrapper,
          contextId,
          inquirer,
        );

        // 생성된 인스턴스에 확인된 프로퍼티를 적용합니다.
        this.applyProperties(instance, properties);
        // 초기화 시간을 계산하여 wrapper의 initTime 속성에 저장합니다.
        wrapper.initTime = this.getNowTimestamp() - t0;
        // 정착 신호를 완료로 설정합니다.
        settlementSignal.complete();
      };
      // 생성자의 매개변수에 대한 의존성을 처리합니다. 콜백 함수를 호출합니다.
      await this.resolveConstructorParams<T>(
        wrapper,
        moduleRef,
        inject as InjectionToken[],
        callback,
        contextId,
        wrapper,
        inquirer,
      );
    } catch (err) {
      // 에러가 발생하면 정착 신호를 오류로 설정합니다.
      settlementSignal.error(err);
      throw err;
    }
  }

  • 전달받은 wrapper의 인스턴스가 초기화 중인지 확인하고 초기화 중이 아니라면 초기화를 진행합니다.
  • 생성자의 매개변수에 대한 의존성을 처리하고, 콜백 함수를 통해 인스턴스를 만들어 반환합니다.

주어진 생성자 매개변수의 의존성을 찾고 인스턴스화 하는 과정


flowchart TD K[resolveConstructorParams] --> J(resolveSingleParam) J[resolveSingleParam] --> A(resolveComponentInstance) A[resolveComponentInstance] --> B(lookupComponent) B --> C{모듈의 provider에 있는가?} C -->|yes| D[instanceWrapper 반환] C -->|no| E(lookupComponentInParentModules) E --> F(lookupComponentInImports) F --> G{모듈의 provider, exports에 있는가?} G -->|yes| H[instanceWrapper 반환] G -->|no, 찾을때까지 DFS| F[lookupComponentInImports]
  • 상세적으로 코드로 확인하겠지만, 하나의 생성자 의존성을 찾고 인스턴스화 하기까지 다음과 같은 과정을 거칩니다.
  • 재귀를 돌며(DFS) 같은 토큰을 가진 모듈을 찾을 때까지 반복한다는 점이 흥미롭습니다!

resolveConstructorParams


// packages/core/injector/injector.ts
public async resolveConstructorParams<T>(
    wrapper: InstanceWrapper<T>,
    moduleRef: Module,
    inject: InjectorDependency[],
    callback: (args: unknown[]) => void | Promise<void>,
    contextId = STATIC_CONTEXT,
    inquirer?: InstanceWrapper,
    parentInquirer?: InstanceWrapper,
  ) {
    let inquirerId = this.getInquirerId(inquirer);

    // wrapper로부터 생성자 메타데이터를 가져옵니다.
    const metadata = wrapper.getCtorMetadata();

    // 메타데이터가 존재하고 컨텍스트가 정적(STATIC_CONTEXT)이 아닌 경우에 대해 조건문을 시작합니다.
    if (metadata && contextId !== STATIC_CONTEXT) {
      // 인스턴스를 새로 불러와서 콜백을 호출합니다.
      // 생성자 메타데이터를 기반으로 의존성을 비동기적으로 불러옵니다.
      const deps = await this.loadCtorMetadata(
        metadata,
        contextId,
        inquirer,
        parentInquirer,
      );
      // 가져온 의존성 목록을 callback 함수에 전달하고 메서드를 종료합니다.
      return callback(deps);
    }

    // 팩토리 제공자일 경우 팩토리 의존성을, 그렇지 않은 경우 클래스 의존성을 가져옵니다.
    // 각각의 위치에 맞는 토큰이나 타입을 반환해서 저장한다.
    // 전자의 경우 PARAMTYPES_METADATA 메타데이터를 SELF_DECLARED_DEPS_METADATA 메타데이터로 덮어씌운 값으로 지정된다
    // PARAMTYPES_METADATA -> 데코레이터가 달린 클래스의 생성자 매개변수의 타입을 알 수 있음
    // nest가 아닌 reflect-metadata로 인해 알 수 있는 것
    // SELF_DECLARED_DEPS_METADATA nest에서 찾아온 값이 아니고 명시적으로 Inject() 했을 때 저장됨
    const isFactoryProvider = !isNil(inject);
    const [dependencies, optionalDependenciesIds] = isFactoryProvider
      ? this.getFactoryProviderDependencies(wrapper)
      : this.getClassDependencies(wrapper);

    let isResolved = true;
    const resolveParam = async (param: unknown, index: number) => {
      try {
        // 매개변수가 parentInquirer와 일치하는지 확인하고
        // 그렇다면 parentInquirer의 인스턴스를 반환합니다.
        if (this.isInquirer(param, parentInquirer)) {
          return parentInquirer && parentInquirer.instance;
        }
        if (inquirer?.isTransient && parentInquirer) {
          inquirer = parentInquirer;
          inquirerId = this.getInquirerId(parentInquirer);
        }

        // 매개변수 하나의 의존성을 해결합니다.
        // 매개변수 래퍼를 반환합니다.
        const paramWrapper = await this.resolveSingleParam<T>(
          wrapper,
          param,
          { index, dependencies },
          moduleRef,
          contextId,
          inquirer,
          index,
        );

        // 매개변수 래퍼의 현재 맥락의 인스턴스를 가져옵니다.
        const instanceHost = paramWrapper.getInstanceByContextId(
          this.getContextId(contextId, paramWrapper),
          inquirerId,
        );

        if (!instanceHost.isResolved && !paramWrapper.forwardRef) {
          isResolved = false;
        }

        // 의존성 처리가 완료되었다면 의존성의 인스턴스를 반환합니다.
        return instanceHost?.instance;
      } catch (err) {
        const isOptional = optionalDependenciesIds.includes(index);
        if (!isOptional) {
          throw err;
        }
        return undefined;
      }
    };
    // 생성자의 의존성을 모두 돌며 resolveParam, 의존성 처리를 하고 인스턴스를 반환
    const instances = await Promise.all(dependencies.map(resolveParam));
    // 정상적으로 처리되었다면 생성자의 의존성 인스턴스들을 담아 콜백을 호출합니다.
    isResolved && (await callback(instances));
  }
  • 생성자 매개변수의 모든 메타데이터를 가져오고, 모든 의존성을 돌면서 의존성을 처리하고 인스턴스를 반환합니다.

resolveSingleParam


// packages/core/injector/injector.ts
public async resolveSingleParam<T>(
    wrapper: InstanceWrapper<T>,
    // 처리할 의존성 정보 (토큰이나 타입)
    param: Type<any> | string | symbol | any,
    // 현재 처리할 의존성의 인덱스와, 생성자 내의 모든 의존성 정보를 갖는 배열
    dependencyContext: InjectorDependencyContext,
    moduleRef: Module,
    contextId = STATIC_CONTEXT,
    inquirer?: InstanceWrapper,
    keyOrIndex?: symbol | string | number,
  ) {
    if (isUndefined(param)) {
      this.logger.log(
        'Nest encountered an undefined dependency. This may be due to a circular import or a missing dependency declaration.',
      );
      throw new UndefinedDependencyException(
        wrapper.name,
        dependencyContext,
        moduleRef,
      );
    }

    // 의존성 정보(토큰)를 가져와서
    const token = this.resolveParamToken(wrapper, param);

    // 의존성을 처리한다.
    return this.resolveComponentInstance<T>(
      moduleRef,
      token,
      dependencyContext,
      wrapper,
      contextId,
      inquirer,
      keyOrIndex,
    );
  }
  • 크게 특별한 코드는 없으며, resolveParamToken / resolveComponentInstance 메소드를 살펴보겠습니다.

resolveParamToken


// packages/core/injector/injector.ts
 public resolveParamToken<T>(
    wrapper: InstanceWrapper<T>,
    param: Type<any> | string | symbol | any,
  ) {
    // ForwardRef 상태가 아니라면 그대로 반환합니다.
    if (!param.forwardRef) {
      return param;
    }

    // 매개변수가 ForwardRef 상태이면,
    // 해당 매개변수가 포함된 생성자의 클래스도 ForwardRef 상태로 바꿉니다.
    wrapper.forwardRef = true;

    // forwardRef 메서드를 통해 실제 값을 가져옵니다.
    return param.forwardRef();
  }

resolveComponentInstance


// packages/core/injector/injector.ts

public async resolveComponentInstance<T>(
  moduleRef: Module,
  token: InstanceToken,
  dependencyContext: InjectorDependencyContext,
  wrapper: InstanceWrapper<T>,
  contextId = STATIC_CONTEXT,
  inquirer?: InstanceWrapper,
  keyOrIndex?: symbol | string | number,
): Promise<InstanceWrapper> {
  this.printResolvingDependenciesLog(token, inquirer);
  this.printLookingForProviderLog(token, moduleRef);

  // 모듈의 모든 프로바이더들을 가져와서
  const providers = moduleRef.providers;

  // 의존성을 찾는다.
  // 1. 현재 모듈의 프로바이더들 중에, 현재 처리할 의존성의 토큰과 같은 토큰을 갖는
  //    프로바이더가 있다면, 해당 정보를 가져와 저장하고 반환.
  // 2. 없다면 부모 모듈, 즉 imports 배열에 있는 모듈들에서 찾아보고, 찾을 때까지
  //    재귀적으로 반복.
  const instanceWrapper = await this.lookupComponent(
    providers,
    moduleRef,
    { ...dependencyContext, name: token },
    wrapper,
    contextId,
    inquirer,
    keyOrIndex,
  );

  // 의존성 처리 완료, instanceWrapper 반환
  return this.resolveComponentHost(
    moduleRef,
    instanceWrapper,
    contextId,
    inquirer,
  );
}
  • 의존성을 lookupComponent를 통해 탐색합니다.

lookupComponent


  public async lookupComponent<T = any>(
    providers: Map<Function | string | symbol, InstanceWrapper>,
    moduleRef: Module,
    dependencyContext: InjectorDependencyContext,
    wrapper: InstanceWrapper<T>,
    contextId = STATIC_CONTEXT,
    inquirer?: InstanceWrapper,
    keyOrIndex?: symbol | string | number,
  ): Promise<InstanceWrapper<T>> {
    const token = wrapper.token || wrapper.name;
    const { name } = dependencyContext;
    if (wrapper && token === name) {
      throw new UnknownDependenciesException(
        wrapper.name,
        dependencyContext,
        moduleRef,
        { id: wrapper.id },
      );
    }
    // provider 들에서 찾는 이름이 있으면
    if (providers.has(name)) {
      const instanceWrapper = providers.get(name);
      //provider에서 하나 꺼내옵니다.
      this.printFoundInModuleLog(name, moduleRef);
      //의존성 메타데이터를 집어 넣습니다.
      this.addDependencyMetadata(keyOrIndex, wrapper, instanceWrapper);

      // 찾았으니 반환합니다.
      return instanceWrapper;
    }

    // provider들에서 찾는 이름이 없으면 부모 모듈에서 찾습니다.
    return this.lookupComponentInParentModules(
      dependencyContext,
      moduleRef,
      wrapper,
      contextId,
      inquirer,
      keyOrIndex,
    );
  }
  • provider에서 찾으면 반환합니다.
  • 만약 provider에 없다면 부모 모듈에서 찾습니다.

lookupComponentInParentModules


 public async lookupComponentInParentModules<T = any>(
    dependencyContext: InjectorDependencyContext,
    moduleRef: Module,
    wrapper: InstanceWrapper<T>,
    contextId = STATIC_CONTEXT,
    inquirer?: InstanceWrapper,
    keyOrIndex?: symbol | string | number,
  ) {
    // imports에서 찾습니다.
    const instanceWrapper = await this.lookupComponentInImports(
      moduleRef,
      dependencyContext.name,
      wrapper,
      [],
      contextId,
      inquirer,
      keyOrIndex,
    );
    if (isNil(instanceWrapper)) {
      throw new UnknownDependenciesException(
        wrapper.name,
        dependencyContext,
        moduleRef,
        { id: wrapper.id },
      );
    }
    return instanceWrapper;
  }
  • 모듈의 imports에서 탐색을 시작해봅니다.

lookupComponentInParentModules


// 재귀는 여기서 발생합니다.
public async lookupComponentInImports(
    moduleRef: Module,
    name: InjectionToken,
    wrapper: InstanceWrapper,
    moduleRegistry: any[] = [],
    contextId = STATIC_CONTEXT,
    inquirer?: InstanceWrapper,
    keyOrIndex?: symbol | string | number,
    isTraversing?: boolean,
  ): Promise<any> {
    let instanceWrapperRef: InstanceWrapper = null;
    // imports를 가져온다
    const imports = moduleRef.imports || new Set<Module>();
    const identity = (item: any) => item;

    // import의 value들을 가져옵니다
    let children = [...imports.values()].filter(identity);

    // 재귀라면,
    if (isTraversing) {
      const contextModuleExports = moduleRef.exports;
      children = children.filter(child =>
        contextModuleExports.has(child.metatype),
      );
    }
    for (const relatedModule of children) {
      // moduleRegistry 이미 돌았던 것들에 리스트에 있으면 건너뜁니다.
      if (moduleRegistry.includes(relatedModule.id)) {
        continue;
      }
      this.printLookingForProviderLog(name, relatedModule);
      // 한번 돌았으니 push한다.
      moduleRegistry.push(relatedModule.id);

      // import한 모듈의 providers, exports를 가져온다
      const { providers, exports } = relatedModule;
      // exports나 provider에 없으면 다시 재귀로 import 모듈의 import에서 찾는다 (재귀)
      if (!exports.has(name) || !providers.has(name)) {
        const instanceRef = await this.lookupComponentInImports(
          relatedModule,
          name,
          wrapper,
          moduleRegistry,
          contextId,
          inquirer,
          keyOrIndex,
          true,
        );
        // 여기서 찾으면 끝낸다.
        if (instanceRef) {
          this.addDependencyMetadata(keyOrIndex, wrapper, instanceRef);
          return instanceRef;
        }
        // 못찾으면 한번 더 돈다. (해당 모듈의 다음 import)
        continue;
      }

      // 여기까지 오면 exports나 provider에 있는 것이다.
      this.printFoundInModuleLog(name, relatedModule);
      // providers에서 가져온다.
      instanceWrapperRef = providers.get(name);
      // 넣는다
      this.addDependencyMetadata(keyOrIndex, wrapper, instanceWrapperRef);

      const inquirerId = this.getInquirerId(inquirer);
      const instanceHost = instanceWrapperRef.getInstanceByContextId(
        this.getContextId(contextId, instanceWrapperRef),
        inquirerId,
      );
      if (!instanceHost.isResolved && !instanceWrapperRef.forwardRef) {
        wrapper.settlementSignal?.insertRef(instanceWrapperRef.id);

        await this.loadProvider(
          instanceWrapperRef,
          relatedModule,
          contextId,
          wrapper,
        );
        break;
      }
    }
    return instanceWrapperRef;
  }
  • import를 순회합니다. 해당 모듈들의 exports, provider에서 탐색하고, 탐색하지 못하면 다음 import한 모듈로 넘어가서 탐색합니다.
  • import를 모두 순회해도 없다면 본래 모듈의 exports, provider에서 탐색합니다.
  • 이렇게 깊이 우선으로 탐색한 의존성을 반환합니다.

다시 resolveConstructorParams부터 인스턴스 생성까지


// packages/core/injector/injector.ts
public async resolveConstructorParams<T>(
    wrapper: InstanceWrapper<T>,
    moduleRef: Module,
    inject: InjectorDependency[],
    callback: (args: unknown[]) => void | Promise<void>,
    contextId = STATIC_CONTEXT,
    inquirer?: InstanceWrapper,
    parentInquirer?: InstanceWrapper,
  ) {
    ...

    let isResolved = true;
    const resolveParam = async (param: unknown, index: number) => {
      try {
        ...
        // 이제 잘 반환된 의존성을 사용합니다!
        const paramWrapper = await this.resolveSingleParam<T>(
          wrapper,
          param,
          { index, dependencies },
          moduleRef,
          contextId,
          inquirer,
          index,
        );

        // 매개변수 래퍼의 현재 맥락의 인스턴스를 가져옵니다.
        const instanceHost = paramWrapper.getInstanceByContextId(
          this.getContextId(contextId, paramWrapper),
          inquirerId,
        );

        if (!instanceHost.isResolved && !paramWrapper.forwardRef) {
          isResolved = false;
        }

        // 의존성 처리가 잘 되었으니 완료되었다면 의존성의 인스턴스를 반환합니다.
        return instanceHost?.instance;
      } catch (err) {
        const isOptional = optionalDependenciesIds.includes(index);
        if (!isOptional) {
          throw err;
        }
        return undefined;
      }
    };

    const instances = await Promise.all(dependencies.map(resolveParam));
    // 정상적으로 처리되었다면 생성자의 의존성 인스턴스들을 담아 콜백을 호출합니다.
    isResolved && (await callback(instances));
  }
  • 이제 위에서 처리된 의존성이 리턴되는 것을 알 수 있습니다. (resolveSingleParam)
  • 해당 의존성을 instanceHost?.instance 를 통해 인스턴스를 반환합니다.
  • 이제 이렇게 모여진 생성자의 인스턴스들을 콜백에 넘깁니다!

callback


const callback = async (instances: unknown[]) => {
  // 처리된 의존성들의 배열이 전달됩니다.
  // 주입할 프로퍼티를 비동기적으로 확인합니다.
  const properties = await this.resolveProperties(wrapper, moduleRef, inject as InjectionToken[], contextId, wrapper, inquirer);
  // **클래스 인스턴스를 생성합니다. 인스턴스 생성!**
  const instance = await this.instantiateClass(instances, wrapper, targetWrapper, contextId, inquirer);

  // 생성된 인스턴스에 확인된 프로퍼티를 적용합니다.
  // 위에서 처리한 의존성을 만들어진 인스턴스에 적용하는 메서드
  this.applyProperties(instance, properties);
  // 초기화 시간을 계산하여 wrapper의 initTime 속성에 저장합니다.
  wrapper.initTime = this.getNowTimestamp() - t0;
  // 정착 신호를 완료로 설정합니다.
  settlementSignal.complete();
};
  • 아래와 같이 AppModule에 있는 AppController가 AppService를 생성자를 통해 의존성을 주입해주고 있다고 가정하겠습니다.
 import { Controller, Get, Post } from "@nestjs/common";
 import { AppService } from "./app.service";
 import { TestService } from "./test.service";

 @Controller()
 export class AppController {
   constructor(
     private readonly appService: AppService,
     private readonly testService: TestService,
   ) { }
   ...
 }
  • 콜백 함수에 있는 targetWrapper / instances / instance 변수는 아래와 같습니다.
// 대상이 될 AppController
{
targetWrapper: InstanceWrapper {
    isAlias: false,
    scope: undefined,
    values: WeakMap { <items unknown> },
    token: [class AppController],
    name: 'AppController',
    metatype: [class AppController],
    durable: undefined,
    host: Module {
      _metatype: [class AppModule],
      container: [NestContainer],
      _imports: [Set],
      _providers: [Map],
      _injectables: Map(0) {},
      _middlewares: Map(0) {},
      _controllers: [Map],
      _entryProviderKeys: Set(0) {},
      _exports: Set(0) {},
      _distance: 0,
      _initOnPreview: false,
      _isGlobal: false,
      _id: '0f80f9e022edd38efc8a1',
      _token: '024ef4b961c95d3555c3f123e928e8434bab1f0f3746b0cd9e60594269b97899'
    },
    settlementSignal: SettlementSignal {
      _refs: Set(0) {},
      completed: true,
      settleFn: [Function (anonymous)],
      settledPromise: [Promise]
    },
    isTreeStatic: true,
    initTime: 1.9916669987142086,
    [Symbol(instance_metadata:cache)]: { dependencies: [Array] },
    [Symbol(instance_metadata:id)]: 'c8a1271e1594d66877780'
  }
}

// constructor params
{ instances: [ AppService {}, TestService {} ] }

// resolve 이후 결과
{
  instance: AppController {
    appService: AppService {},
    testService: TestService {}
  }
}

instantiateClass


public async instantiateClass<T = any>(
    instances: any[], // 처리된 인스턴스들
    wrapper: InstanceWrapper, // 토큰 정보 등
    targetMetatype: InstanceWrapper,
    contextId = STATIC_CONTEXT,
    inquirer?: InstanceWrapper,
  ): Promise<T> {
    const { metatype, inject } = wrapper;
    const inquirerId = this.getInquirerId(inquirer);

    // 현재 맥락에서 필요한 인스턴스
    const instanceHost = targetMetatype.getInstanceByContextId(
      this.getContextId(contextId, targetMetatype),
      inquirerId,
    );
    const isInContext =
      wrapper.isStatic(contextId, inquirer) ||
      wrapper.isInRequestScope(contextId, inquirer) ||
      wrapper.isLazyTransient(contextId, inquirer) ||
      wrapper.isExplicitlyRequested(contextId, inquirer);

    if (this.options?.preview && !wrapper.host?.initOnPreview) {
      instanceHost.isResolved = true;
      return instanceHost.instance;
    }

    if (isNil(inject) && isInContext) {
      // 팩토리 프로바이더가 아니라면,
      // new로 인스턴스를 생성해서 저장!!
      instanceHost.instance = wrapper.forwardRef
        ? Object.assign(
            instanceHost.instance,
            new (metatype as Type<any>)(...instances),
          )
        // 인스턴스를 만들어서 저장합니다.
        : new (metatype as Type<any>)(...instances);
    } else if (isInContext) {
      const factoryReturnValue = (targetMetatype.metatype as any as Function)(
        ...instances,
      );
      instanceHost.instance = await factoryReturnValue;
    }

    // 해당 의존성이 처리되었다고 명시
    instanceHost.isResolved = true;

    // 인스턴스 반환
    return instanceHost.instance;
  }
  • 위 콜백에서 제시하였던 AppController를 기준으로 보았을 때 팩토리 프로바이더가 아니기 때문에 아래 분기를 타게 됩니다.
if (isNil(inject) && isInContext) {
      // 팩토리 프로바이더가 아니라면,
      // new로 인스턴스를 생성해서 저장!!
      instanceHost.instance = wrapper.forwardRef
        ? Object.assign(
            instanceHost.instance,
            new (metatype as Type<any>)(...instances),
          )
        // 인스턴스를 만들어서 저장합니다.
        : new (metatype as Type<any>)(...instances);
    } 
  • 이때 새로 instances(생성자에 주입한 요소들)과 함께 인스턴스화하여 instanceHost.instance에 저장하는 것을 알 수 있습니다.
  • 이후 이 instance를 반환하고 메서드를 종료합니다.

4. 다시 컨테이너 initialize로


await ExceptionsZone.asyncRun(
  async () => {
    // 의존성 스캐너(dependenciesScanner)를 사용해 전달된 모듈의 모든 의존성을 스캔합니다.
    await dependenciesScanner.scan(module);

    // 인스턴스 로더(instanceLoader)를 사용해 모든 의존성의 인스턴스를 생성합니다.
    await instanceLoader.createInstancesOfDependencies();

    // 의존성 스캐너(dependenciesScanner)를 사용해 애플리케이션의 제공자(Providers)를 적용합니다.
    dependenciesScanner.applyApplicationProviders();
  },
  // asyncRun 블록을 종료하고, 예외가 발생할 경우 teardown을 처리하고 로그 자동 플러시 여부를 설정합니다.
  teardown,
  this.autoFlushLogs
);
  • 이제 다시 initialize로 돌아왔습니다. 위의 과정들을 통해 createInstancesOfDependencies를 통해 인스턴스들을 생성하고, 저장하였습니다.
  • 이제 모듈들이 모두 NestContainer에 등록되고 요소들을 포함하여 초기화가 되었고, provider / controller / injectable 모두 생성자까지 포함되어서 인스턴스화 되었습니다.
  • 이렇게 저장해둔 인스턴스들과 정보들을 바탕으로 IoC 컨테이너가 필요에 맞게 골라서 사용합니다.

5. 마치며


  • NestJS가 어떻게 초기화되고 컨테이너가 DI를 관리하는지에 대해 알아보았습니다.
  • 생각보다 코드를 모두 까보는게 쉽지만은 않은 것 같습니다. 코드량이 많고 이해하지 못한 부분들이 아직 너무 많습니다.
    • 두번 세번 더 살펴보면서 더 깊고 확실하게 계속 이해해보려고 합니다.
  • 프레임워크를 사용할 때 비즈니스 로직을 잘 짜는것도 중요하지만 어떻게 프레임워크가 작동하는지 알아야 더 좋은 구조를 짤 수 있고 더 나은 프로덕트를 만들 수 있다고 생각해서 좋은 학습이였다고 생각합니다. 이외에도 시간이 있을 때마다 다른 core 기능들에 대해 더 깊게 파보려고 합니다!