실전: 기준 코드를 분석해봅시다.

러리·2022년 11월 7일
3

Nest.js 삽질기

목록 보기
2/4
post-thumbnail

앞서 올렸던 Nest.js는 실제로 어떻게 의존성을 주입해줄까?라는 글에서, 먼저 기준이 되는 코드를 정의하고 삽질을 들어갔습니다.

그런데, 글을 다시 읽어보니 글 마지막 쯤 갔을 때에는 기준이 되는 코드는 이미 잊혀졌던 것이었습니다. 그래서 삽질한 내용을 기반으로 기준이 되는 코드를 해석해보려 합니다.

해당 글에 수정해서 붙이기엔 글이 너무 길어서, 따로 부록으로 빼내어보려 합니다.

자, 그럼 시작해봅시다!

하나씩 뜯어보기

하나씩 뜯어보도록 할게요!

먼저 app.controller.tsapp.service.ts를 보고, 그 다음 app.module.ts, 마지막으로 main.ts 파일을 살펴볼께요.

app.controller.ts

// 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();
  }
}

AppService를 주입 받아서, 서비스의 getHello() 메서드를 호출하는 것 밖에 없어요.

따라서, 위의 컨트롤러 코드는 사실 큰 의미는 없습니다.

네스트의 의존성 주입과 관련된 건 '사실 AppService를 어떻게 주입 받느냐'지, '주입 받은 AppService를 어떻게 쓸 것인가?' 가 아니니깐요.

그 외에는 @Controller 데코레이터가 보이는데, 해당 데코레이터는 여기서 다뤘습니다. 크게 의미는 없다는 결론을 내렸었지만, 궁금하신 분들도 있을 것 같아 다시 코드를 들고와보겠습니다.

// packages/common/decorators/core/controller.decorator.ts
export function Controller(
  prefixOrOptions?: string | string[] | ControllerOptions,
): ClassDecorator {
  // ...
  return (target: object) => {
    Reflect.defineMetadata(CONTROLLER_WATERMARK, true, target);
    Reflect.defineMetadata(PATH_METADATA, path, target);
    Reflect.defineMetadata(HOST_METADATA, host, target);
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, scopeOptions, target);
    Reflect.defineMetadata(VERSION_METADATA, versionOptions, target);
  };
}

필요 없는 부분은 모두 날렸어요! 사실 데코레이터에서는 결국 메타데이터를 등록하기만 해서, 크게 볼 필요 없다고 생각해요. 저번 글의 삽질에서도 위의 메타데이터들을 살펴보지 않았었구요.

이번에도 넘어가보도록 할게요.

app.service.ts

// app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

앞서 살펴본 컨트롤러 코드보다 더 살펴볼 필요가 없어요. 심지어 주입 받지도 않고 있거든요!

@Injectable 데코레이터만 다시 살펴보고 넘어갈게요.

// packages/common/decorators/core/injectable.decorator.ts
export function Injectable(options?: InjectableOptions): ClassDecorator {
  return (target: object) => {
    Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
  };
}

위 코드는 생략한 부분도 없습니다. 이게 끝이에요! 정말 메타데이터만 등록하고 끝나요.
다음으로 넘어가볼게요.

사실 다음 2개의 파일이 본론이라고 볼 수 있어요.
저번 글을 보신 분들은 아시겠지만, 실제로 관계를 정의하고 의존성을 주입해주는 부분이거든요!

app.module.ts

// 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 {}

네스트를 경험해보신 분들이라면 친근한 코드일거에요.

@Module() 데코레이터를 사용해서, 해당 모듈 내에 무슨 컨트롤러가 있고, 무슨 프로바이더가 등록되어 있는지 나타낼 수 있어요. 그렇다면 @Module() 데코레이터가 어떻게 생겼는지 안 살펴볼 수 없겠죠?

// packages/common/decorators/modules/module.decorator.ts
export function Module(metadata: ModuleMetadata): ClassDecorator {
  const propsKeys = Object.keys(metadata);
  validateModuleKeys(propsKeys);

  return (target: Function) => {
    for (const property in metadata) {
      if (metadata.hasOwnProperty(property)) {
        Reflect.defineMetadata(property, (metadata as any)[property], target);
      }
    }
  };
}

위 로직에 따라, AppModule 클래스에 아래 세 가지의 메타데이터가 만들어져요.

Key: 'imports', Value: []
Key: 'controllers', Value: [AppController]
Key: 'providers', Value: [AppService]

이걸 꼭 기억해주세요!

main.ts

자 이제 본론입니다. 사실상 이 파일이 이전 글 내용의 70% ~ 80%를 차지했었죠.

// 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();

main.ts 자체는 크게 특별한 점 없는 코드입니다. 의존성을 어떻게 주입해주는지에 대해서만 살펴봤기 때문에, listen 메서드는 건너띄고 NestFactory.create만 볼게요.

// packages/core/nest-factory.ts
public async create<T extends INestApplication = INestApplication>(
  module: any,
  // ...
): Promise<T> {
  // ...
  await this.initialize(module, container, applicationConfig, httpServer);
  // ...
}

NestFactory.create 메서드는 this.initialize 메서드를 호출해요. 여기서, 매개변수 module으로 AppModule 이 들어와요.

// packages/core/nest-factory.ts
private async initialize(
  module: any,
  // ...
) {
  // ...
  await dependenciesScanner.scan(module);
  await instanceLoader.createInstancesOfDependencies();
  dependenciesScanner.applyApplicationProviders();
  // ...
}

우리의 AppModuleDependenciesScanner#scan 속으로 들어갔어요. 같이 따라 들어가봐요!

DependenciesScanner#scan

// packages/core/scanner.ts
public async scan(module: Type<any>) {
  // ...
  await this.scanForModules(module);
  await this.scanModulesForDependencies();
  // ...
}

따라들어왔더니, module 매개변수가 Type<any> 타입을 가지게 되었어요. 해당 타입은 네스트 내에서 정의한 인터페이스인데요. '생성자를 가진다'를 나타냅니다.

export interface Type<T = any> extends Function {
  new (...args: any[]): T;
}

실제로는 위와 같이 생겼고, 여기서 볼 수 있어요.

DependenciesScanner#scanForModules

// packages/core/scanner.ts
public async scanForModules(
  moduleDefinition:
    | ForwardReference
    | Type<unknown>
    | DynamicModule
    | Promise<DynamicModule>,
  // ...
): Promise<Module[]> {
  const moduleInstance = await this.insertModule(moduleDefinition, scope);
  // 1. moduleDefinition이 ForwardRef 라면 .forwardRef 메서드 호출
  // 2. 모듈이 Injectable이거나, 컨트롤러거나, 예외 필터면 경고 로그 출력
  // 3. 모듈 컴파일 -> 타입, 동적 메타데이터, 토큰을 가져옴
  // 4. 이를 기반으로 새로운 모듈 객체 생성
  // 5. 컨테이너의 modules(ModulesContainer)에 토큰과 모듈을 등록
  // 6. 만들어진 모듈 객체를 반환

  // ...

  const modules = !this.isDynamicModule(
    moduleDefinition as Type<any> | DynamicModule,
  )
    ? this.reflectMetadata(
        moduleDefinition as Type<any>,
        MODULE_METADATA.IMPORTS, // = imports
      )
    : // ...

  let registeredModuleRefs = [];
  // 등록된 모듈들의 배열

  for (const [index, innerModule] of modules.entries()) {
    // ...
  }
  // ...

  return [moduleInstance].concat(registeredModuleRefs);
  // 현재 모듈과 등록된 자식 모듈들의 배열을 반환합니다.
}

이전에는 insertModule 메서드를 뛰어넘었는데, 이번엔 한 번 봐야할 필요가 있어보여요.

// packages/core/scanner.ts
public async insertModule(
  moduleDefinition: any, // = AppModule
  scope: Type<unknown>[],
): Promise<Module | undefined> {
  const moduleToAdd = this.isForwardReference(moduleDefinition)
    ? moduleDefinition.forwardRef()
    : moduleDefinition; // @1

  if (
    this.isInjectable(moduleToAdd) || // @2
    this.isController(moduleToAdd) || // @3
    this.isExceptionFilter(moduleToAdd) // @4
  ) {
    throw new InvalidClassModuleException(moduleDefinition, scope);
  }

  return this.container.addModule(moduleToAdd, scope);
}

여러 조건들이 보여요. 각각 확인해봐요.

  1. 주어진 모듈 객체가 Forward-Ref인지를 확인해요. 우리는 일반적인 모듈을 넘겨줬기 때문에, 해당 객체를 그대로 쓰게 됩니다.
  2. 주어진 모듈 객체가 Injectable 객체인지 확인해요. 아니니까 패스!
  3. 마찬가지로, 모듈 객체가 Controller 객체인지 확인해요. 이것도 패스!
  4. ExceptionFilter인지도 확인해요. 이것도 아니죠? 패스!

따라서, insertModule 메서드를 정리하면 아래와 같아요.

// packages/core/scanner.ts
public async insertModule(
  moduleDefinition: any, // = AppModule
  scope: Type<unknown>[],
): Promise<Module | undefined> {
  const moduleToAdd = moduleDefinition;
  return this.container.addModule(moduleToAdd, scope);
}

즉, NestContainer에 모듈을 추가하는 게 끝이에요. 다시 돌아가볼게요.

scanForModules도 현재의 조건에 맞게 코드를 조금 제거해볼텐데요.

// packages/core/scanner.ts
public async scanForModules(
  moduleDefinition:
    | ForwardReference
    | Type<unknown>
    | DynamicModule
    | Promise<DynamicModule>,
  // ...
): Promise<Module[]> {
  const moduleInstance = await this.container.addModule(moduleDefinition, scope);

  const modules = this.reflectMetadata(
    moduleDefinition as Type<any>,
    MODULE_METADATA.IMPORTS, // = imports
  )

  let registeredModuleRefs = [];
  // 등록된 모듈들의 배열

  for (const [index, innerModule] of modules.entries()) {
    // ...
  }
  // ...

  return [moduleInstance].concat(registeredModuleRefs);
  // 현재 모듈과 등록된 자식 모듈들의 배열을 반환합니다.
}

모두 제거하면 위와 같아요. 이때, modules 에는 빈 배열이 들어오게 됩니다.

그 이유는 위에서 살펴본 @Module 데코레이터에 있는데요, 아까 해당 데코레이터 안에서 imports 메타데이터에 빈 배열을 저장했었습니다. 기억 안 난다면 다시 보고 와주세요!

따라서, modules.entries()에 대한 반복문은 실행되지 않고, registeredModuleRefs에도 그대로 빈 배열만 남게 됩니다.

그럼, 결론적으로 scanForModules가 반환하는 값은 [AppModule] 배열이겠죠!

원래 위 반복문에서 현재 모듈이 의존성을 갖는, 그러니까 imports 배열 안에 있는 모듈들이 재귀를 통해 초기화되는데, 우리는 지금 AppModule 하나만 갖고 있으니 실행되지 않아요.

이제 scanModulesForDependencies를 보러 가봅시다.

DependenciesScanner#scanModulesForDependencies

// packages/core/scanner.ts
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);
  }
}

scan 메서드에서 매개변수를 하나도 넘겨주지 않았기 때문에, modules에는 자동적으로 현재 NestContainer의 모든 모듈을 불러오게 됩니다.

그리고 각 모듈에 대해서, reflectImports, reflectProviders, reflectControllers, reflectExports를 실행시키는데요! 어짜피 모듈은 AppModule 하나 뿐이니 사실 한 번만 반복하긴 합니다.

각각 러프하게 살펴볼게요.

DependenciesScanner#reflectImports

// packages/core/scanner.ts
public async reflectImports(
  module: Type<unknown>,
  token: string,
  context: string,
) {
  const modules = [
    ...this.reflectMetadata(MODULE_METADATA.IMPORTS, module),
    ...this.container.getDynamicMetadataByToken(
      token,
      MODULE_METADATA.IMPORTS as 'imports',
    ),
  ];
  for (const related of modules) {
    await this.insertImport(related, token, context);
  }
}

위에서 보시다시피, 모듈과 토큰에서 imports 배열들을 가져와, 각각에 대하여 반복문을 돌리는데요. 앞서 살펴봤듯이, imports에는 빈 배열 밖에 없기 때문에 해당 메서드는 아무것도 실행시키지 않습니다.

DependenciesScanner#reflectProviders

// packages/core/scanner.ts
public reflectProviders(module: Type<any>, token: string) {
  const providers = [
    ...this.reflectMetadata(MODULE_METADATA.PROVIDERS, module),
    ...this.container.getDynamicMetadataByToken(
      token,
      MODULE_METADATA.PROVIDERS as 'providers',
    ),
  ];
  providers.forEach(provider => {
    this.insertProvider(provider, token);
    this.reflectDynamicMetadata(provider, token);
  });
}

거의 비슷하지만 providers 메타데이터에 대해서 배열을 가져오는데요. 해당 메타데이터에는 @Module 데코레이터에 의해 [AppService]라는 배열이 들어있습니다. 우리가 모듈에다 넣어준 단 하나의 서비스죠.

AppService 프로바이더에 대해서, insertProviderreflectDynamicMetadata를 호출하고 있습니다.

DependenciesScanner#insertProvider

// packages/core/scanner.ts
public insertProvider(provider: Provider, token: string) {
  const isCustomProvider = this.isCustomProvider(provider);
  if (!isCustomProvider) {
    return this.container.addProvider(provider as Type<any>, token);
  }
  // ...
}

아래에 아주 많은 내용이 있으나, AppService는 커스텀 프로바이더가 아니기 때문에 모두 넘어가고, NestContainer에 토큰을 통해 프로바이더를 추가합니다.

NestContainer#addProvider

// packages/core/injector/container.ts
public addProvider(
  provider: Provider,
  token: string,
): string | symbol | Function {
  const moduleRef = this.modules.get(token);
  if (!provider) {
    throw new CircularDependencyException(moduleRef?.metatype.name);
  }
  if (!moduleRef) {
    throw new UnknownModuleException();
  }
  return moduleRef.addProvider(provider);
}

그럼 해당 토큰을 갖고 있는 모듈을 찾아보고, 해당 모듈에 프로바이더를 추가해줘요.

NestContainer#reflectDynamicMetadata는 넘어갈게요. 현재 프로바이더가 가져야 하는 가드, 예외 필터 등의 정보를 저장하는 과정이라 보시면 됩니다.

DependenciesScanner#reflectControllers

// packages/core/scanner.ts
public reflectControllers(module: Type<any>, token: string) {
  const controllers = [
    ...this.reflectMetadata(MODULE_METADATA.CONTROLLERS, module),
    ...this.container.getDynamicMetadataByToken(
      token,
      MODULE_METADATA.CONTROLLERS as 'controllers',
    ),
  ];
  controllers.forEach(item => {
    this.insertController(item, token);
    this.reflectDynamicMetadata(item, token);
  });
}

public insertController(controller: Type<Controller>, token: string) {
  this.container.addController(controller, token);
}

사실상 reflectProviders랑 완전히 동일해요. 심지어 컨트롤러는 커스텀 프로바이더 같은 것이 없기 때문에, insertController는 훨씬 간단해져요.

controllers 배열로 @Module 데코레이터에서 등록한 [AppContorller]이 들어오고, 해당 컨트롤러에 대해서 insertControllerreflectDynamicMetadata 메서드를 호출하고 있어요.

또, addController는 위의 addProvider과 동작이 비슷하기 때문에 넘어가보도록 할게요.

DependenciesScanner#reflectExports

// packages/core/scanner.ts
public reflectExports(module: Type<unknown>, token: string) {
  const exports = [
    ...this.reflectMetadata(MODULE_METADATA.EXPORTS, module),
    ...this.container.getDynamicMetadataByToken(
      token,
      MODULE_METADATA.EXPORTS as 'exports',
    ),
  ];
  exports.forEach(exportedProvider =>
    this.insertExportedProvider(exportedProvider, token),
  );
}

public insertExportedProvider(
  exportedProvider: Type<Injectable>,
  token: string,
) {
  this.container.addExportedProvider(exportedProvider, token);
}

여기는 더 간단해졌어요. exports에 대한 처리인데, 커스텀 exports 같은 것도 없을 뿐더러 가드나 예외 필터 등을 적용시키지도 않기 때문에 reflectDynamicMetadata도 사용하지 않아요.

여기까지 DependenciesScanner#scanModulesForDependencies의 호출 결과였습니다.

정리해보면, importsexports가 존재하지 않기 때문에, reflectImportsreflectExports는 아무것도 실행시키지 않고, reflectProvidersreflectControllers에서 우리가 등록한 [AppService][AppController]의 정보를 등록하는 과정을 거쳤어요.

여기서 중요한 점은, 모듈과 프로바이더 및 컨트롤러의 의존성 정보를 등록한 것이지, 실제로 인스턴스화를 하지는 않았다는 건데요.

이제 다음 목적지를 정하기 위해, 쭉 돌아가서 NestFactory#initialize 메서드를 다시 볼게요.

// packages/core/nest-factory.ts
private async initialize(
  module: any,
  // ...
) {
  // ...
  await dependenciesScanner.scan(module);
  await instanceLoader.createInstancesOfDependencies();
  dependenciesScanner.applyApplicationProviders();
  // ...
}

우리는 이제 DependenciesScanner#scan 하나만 봤어요. 이 정도면 부록이 아니라 새로운 글 하나가 더 탄생한 느낌인데요, 불완전한 글로 남는 것보단 나으니까요! 계속 가봅시다.

InstanceLoader#createInstancesOfDependencies

저번 글에 따르면, 이 메서드에서 실제로 프로바이더들과 컨트롤러들을 인스턴스화 시켜줬어요.

한 번 똑같이 따라가볼께요.

// packages/core/injector/instance-loader.ts
public async createInstancesOfDependencies(
  modules: Map<string, Module> = this.container.getModules(),
) {
  this.createPrototypes(modules);
  await this.createInstances(modules);
}

매개변수를 넘겨주지 않았기 때문에, 현재 NestContainer의 모든 모듈들을 불러옵니다. 당연히 AppModule 밖에 없겠죠? 물론, 기본적으로 들어가는 코어 모듈들이 있습니다만 이번 글에서 해당 모듈들은 고려하지 않는 방향으로 가겠습니다.

InstanceLoader#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);
  });
}

각 모듈에 대하여 세 메서드를 호출하고 있습니다. 각각에 넣어주는 매개변수인 moduleRefAppModule이라고 생각하면서 진행하면 될 거 같아요.

// packages/core/injector/instance-loader.ts
private createPrototypesOfProviders(moduleRef: Module) {
  const { providers } = moduleRef;
  providers.forEach(wrapper =>
    this.injector.loadPrototype<Injectable>(wrapper, providers),
  );
}

private createPrototypesOfInjectables(moduleRef: Module) {
  const { injectables } = moduleRef;
  injectables.forEach(wrapper =>
    this.injector.loadPrototype(wrapper, injectables),
  );
}

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

앞서 addProvider 메서드를 호출하여 모듈에 추가해준 프로바이더들을 모두 들고와서, 각각에 대하여 loadPrototype을 호출합니다. 그 외에 다른 두 메서드도 제너릭의 타입이 다르다는 것 외엔 크게 다른 점이 없어보여요.

Injector#loadPrototype

// packages/core/injector/injector.ts
public loadPrototype<T>(
  { token }: InstanceWrapper<T>,
  collection: Map<InstanceToken, InstanceWrapper<T>>,
  contextId = STATIC_CONTEXT,
) {
  if (!collection) {
    return;
  }
  const target = collection.get(token);
  const instance = target.createPrototype(contextId);
  if (instance) {
    const wrapper = new InstanceWrapper({
      ...target,
      instance,
    });
    collection.set(token, wrapper);
  }
}

인스턴스의 토큰을 이용해서 주어진 컬렉션(providers, injectables, controllers) 내에 있는 인스턴스 래퍼를 가져와서, createPrototype을 호출하고, 그 결과를 포함해서 새로운 인스턴스 래퍼를 만들고, 기존의 인스턴스 래퍼를 대체시키는 모습을 볼 수 있습니다.

여기서 인스턴스 래퍼는, 각 프로바이더나 컨트롤러 하나하나에 할당되는 개념이라고 보시면 됩니다. 여기에는 해당 인스턴스의 이름과 토큰, 비동기적인지, 해당 인스턴스를 갖고 있는 모듈이 무엇인지, 스코프는 어떤지, 인스턴스의 타입은 무엇인지, 각 컨텍스트 식별자에 대하여 객체를 저장하는 맵 객체 등이 포함됩니다.

그리고, createPrototype은 거기서 인스턴스 타입을 통해 프로토타입의 객체를 생성합니다.

public createPrototype(contextId: ContextId) {
  const host = this.getInstanceByContextId(contextId);
  if (!this.isNewable() || host.isResolved) {
    return;
  }
  return Object.create(this.metatype.prototype);
}

현재 컨텍스트 식별자에 해당 인스턴스가 이미 존재하는지를 확인하고, 없다면 해당 인스턴스의 프로토타입에 대한 객체를 생성합니다.

이렇게 createPrototypes의 동작을 살펴봤어요.

정리하면, 프로바이더, injectable들, 컨트롤러들의 각 프로토타입을 생성하여 새로운 인스턴스 래퍼로 대체시키는 역할을 해요.

InstanceLoader#createInstsances

// packages/core/injector/instance-loader.ts
private async createInstances(modules: Map<string, Module>) {
  await Promise.all(
    [...modules.values()].map(async moduleRef => {
      await this.createInstancesOfProviders(moduleRef);
      await this.createInstancesOfInjectables(moduleRef);
      await this.createInstancesOfControllers(moduleRef);

      const { name } = moduleRef.metatype;
      this.isModuleWhitelisted(name) &&
        this.logger.log(MODULE_INIT_MESSAGE`${name}`);
    }),
  );
}

private isModuleWhitelisted(name: string): boolean {
  return name !== InternalCoreModule.name;
}

이제 본격적으로 인스턴스 생성을 시작합니다. 모든 모듈에 대하여 세 메서드를 호출하고, 모듈이 내부 코어 모듈이 아닌 경우, 모듈의 초기화가 완료되었을 때 로그를 출력해요. 우리가 네스트 서버를 켰을 때 우루루 로그가 뜨는 게 바로 여기서 일어나는 거에요!

이제 세 메서드를 살펴봐야 하는데, 이 부분은 이전 글에서 이미 다뤘어요. 이전 글에서, 결과만 가져올게요.

각각의 메서드는 모듈(AppModule)에서 프로바이더, Injectable, 컨트롤러들을 가져옵니다.
이때 대표적으로 프로바이더의 경우 Injector#loadProvider라는 메서드를 사용하는데요. 해당 메서드는 다시 Injector#loadInstance 메서드를 호출합니다. 요 메서드는 아래와 같이 생겼는데요.

Injector#loadInstance

// packages/core/injector/injector.ts
public async loadInstance<T>(
  wrapper: InstanceWrapper<T>, // 각각의 Provider, 여기선 AppService
  collection: Map<InstanceToken, InstanceWrapper>, // = providers
  moduleRef: Module, // = AppModule
  contextId = STATIC_CONTEXT,
  inquirer?: InstanceWrapper, // = undefined
) {
  // ...
  
  const callback = async (instances: unknown[]) => {
    // ...
    const instance = await this.instantiateClass(
      instances,
      wrapper,
      targetWrapper,
      contextId,
      inquirer,
    );
    // ...
  };
  // ...
}

이름만 봐도 인스턴스화를 진행할 듯한 메서드를 호출하는 콜백을 만든 뒤, Injector#resolveConstructorParams 메서드에 해당 콜백을 넘겨줍니다. 요 메서드도 한 번 살펴볼게요.

Injector#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, // = undefined
  parentInquirer?: InstanceWrapper, // = undefined
) {
  // ...
  const [dependencies, optionalDependenciesIds] = this.getClassDependencies(wrapper);

  let isResolved = true;
  const resolveParam = async (param: unknown, index: number) => {
    // 의존성 처리가 제대로 완료되었다면, 해당 의존성의 인스턴스를 반환합니다.
  };
  const instances = await Promise.all(dependencies.map(resolveParam));
  isResolved && (await callback(instances));
  // 지정된 의존성들의 인스턴스를 만들고, 정상적으로 처리되었다면 콜백을 호출합니다.
}

위 코드에서 dependencies 변수에는 해당 프로바이더 클래스의 생성자에서 갖는 의존성의 타입을 나타냅니다. 위 코드의 경우 컨트롤러에서도 동일하게 적용되니, AppController를 기준으로 보자면 해당 변수에는 dependencies = [AppService]가 되는 것입니다.

또한, resolveParam 콜백은 본격적으로 프로바이더를 인스턴스화하기 전에, 해당 프로바이더의 의존성들에 문제는 없는지 확인하고, 각각을 인스턴스화하는 과정이라고 보시면 될 거 같습니다.

즉, AppController를 인스턴스화하기 위해서는 AppService의 인스턴스가 필요하니, AppService의 인스턴스를 먼저 만드는 과정입니다.

대상 프로바이더가 의존하는 모든 의존성들을 정상적으로 인스턴스화 했다면, 이제 대상 프로바이더를 인스턴스화할 준비가 완료되었다는 뜻입니다.

콜백에서는 위에서 만들어진 의존성들의 인스턴스들을 Injector#instantiateClass로 넘겨주고 있는데요, 요 메서드를 마지막으로 살펴보도록 하겠습니다.

Injector#instantiateClass

// packages/core/injector/injector.ts
public async instantiateClass<T = any>(
  instances: any[], // 처리된 인스턴스들
  wrapper: InstanceWrapper, // 토큰 정보 등을 담고 있음
  targetMetatype: InstanceWrapper, // 만들어낼 인스턴스
  contextId = STATIC_CONTEXT,
  inquirer?: InstanceWrapper, // = undefined
): Promise<T> {
  const { metatype, inject } = wrapper;
  // ...
  instanceHost.instance = new (metatype as Type<any>)(...instances);
  instanceHost.isResolved = true;

  // 만들어진 인스턴스를 반환합니다.
  return instanceHost.instance;
}

실제로는 if문도 있고, 다른 많은 코드가 있으나 현재 조건에 맞췄을 때 요약하면 위와 같습니다. 이를 AppController에 맞춰서 살펴보면, 아래와 같습니다.

// packages/core/injector/injector.ts
public async instantiateClass<T = any>(
  instances: any[], // [Instance of AppService]
  wrapper: InstanceWrapper, // AppController의 인스턴스 래퍼
  targetMetatype: InstanceWrapper, // 토큰으로 받아온 AppController의 인스턴스 래퍼
  contextId = STATIC_CONTEXT,
  inquirer?: InstanceWrapper, // = undefined
): Promise<T> {
  const { metatype, inject } = wrapper;
  // metatype = AppController
  // ...
  instanceHost.instance = new (metatype as Type<any>)(...instances);
  instanceHost.isResolved = true;

  // 만들어진 인스턴스를 반환합니다.
  return instanceHost.instance;
}

이렇게 AppController 하나가 만들어졌습니다. AppService 등의 프로바이더들도 마찬가지로 동작하며, 그렇게 모든 의존성이 처리가 완료되면 네스트 서버가 켜지기 시작합니다.

끝.

생각보다 하는 일이 엄청나게 많은데, 의외로 서버가 켜지는 데에 크게 시간이 걸린다고 느껴지진 않습니다. Promise.all 처럼 동시에 처리하는 부분이 상당히 많고, 중복 처리(isResolved) 및 이미 만들어진 인스턴스에 대한 캐싱도 처리하는 등, 속도에 상당히 신경 썼다는 게 느껴지네요.

이렇게 부록도 끝이 났습니다. 기본적으로 만들어지는 총 100줄도 안되는 코드들이 위와 같은 수많은 코드들을 실행하고, 서버가 시작되는 걸 직접 보니 감회가 새롭네요.

다음 글은 언제가 될진 모르겠으나.. 요청이 들어왔을 때, 어떻게 알맞은 컨트롤러의 메서드를 호출해주는지 알아보도록 하겠습니다. 이번에 네스트 버전이 v9로 올라갔는데, 공부할 게 또 늘었네요.

긴 글 읽어주셔서 감사합니다.

profile
하고 싶은걸 합니다.

0개의 댓글