[NestJS 파헤치기] 01. NestFactory

bluewhale·2022년 11월 15일
3

NestJS

목록 보기
1/3
post-thumbnail

안녕하세요. 최근 여러가지 일들로 바쁜 나날을 보내게 되어, 오랜만에 포스팅을 하게 되었습니다.
이번에는 NestJS 파헤치기라는 주제로 한번 글을 작성해볼까 합니다.
최근 업무 상, NestJS를 새롭게 사용하게 되었습니다. 그 과정에서, NestJS의 코어와 관련된 부분의 지식이 부족하여 NestJS의 기능을 하는 것에 어려움을 느끼는 일이 잦아졌습니다. 그래서, 개인적인 공부를 위해 NestJS 프레임워크를 개괄적으로 살펴보는 시간을 가져보고자 마음 먹게 되었습니다.

이번에는 그 첫 시간으로 모든 NestJS 어플리케이션의 진입점에 해당하는 NestFactory 클래스에 대해 살펴보도록 하겠습니다.

NestFactory

Nestjs 공식문서를 살펴보면, 처음 마주하는 튜토리얼에서 다음과 같은 코드를 찾아볼 수 있습니다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}

bootstrap();

이를 통해 NestFactoryAppModule을 읽어서, 는 서버에 전달된 요청을 처리하는 모든 로직을 담고 있는 NestApplication 인스턴스를 생성하는 역할을 한다는 것을 확인할 수 있습니다. 이번 포스팅의 목표는 NestFactory.create() 안에서 과연 무슨 일들이 일어나고 있는지 이해하는 것입니다.

NestFactory.create()

NestFactory.create()를 들여다보면 다음과 같이 구현되어 있습니다. 지금부터 한줄 한줄 차근차근 자세히 살펴보도록 하겠습니다.

// packages/core/nest-factory.ts
public async create<T extends INestApplication = INestApplication>(
    module: any,
    serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
    options?: NestApplicationOptions,
  ): Promise<T> {
    const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
      ? [serverOrOptions, options]
      : [this.createHttpAdapter(), serverOrOptions];

    const applicationConfig = new ApplicationConfig();
    const container = new NestContainer(applicationConfig);
    this.setAbortOnError(serverOrOptions, options);
    this.registerLoggerConfiguration(appOptions);

    await this.initialize(module, container, applicationConfig, httpServer);

    const instance = new NestApplication(
      container,
      httpServer,
      applicationConfig,
      appOptions,
    );
    const target = this.createNestInstance(instance);
    return this.createAdapterProxy<T>(target, httpServer);
  }

HttpAdapter

처음 세 줄을 살펴보면, NestFactory는 내부적으로 주입받은 HttpAdapter를 그대로 사용하거나, 대부분의 경우(by default) HttpAdapter 인스턴스를 새롭게 생성합니다.

// packages/core/nest-factory.ts
export class NestFactory {
  public async create<T extends INestApplication = INestApplication>(
    module: any,
    serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
    options?: NestApplicationOptions,
  ): Promise<T> {
    // =======================================================
    const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
      ? [serverOrOptions, options]
      : [this.createHttpAdapter(), serverOrOptions];
    // =======================================================
      
	const applicationConfig = new ApplicationConfig();
    const container = new NestContainer(applicationConfig);  
    this.setAbortOnError(serverOrOptions, options);
    this.registerLoggerConfiguration(appOptions);

	await this.initialize(module, container, applicationConfig, httpServer);

    const instance = new NestApplication(
      container,
      httpServer,
      applicationConfig,
      appOptions,
    );
    const target = this.createNestInstance(instance);
    return this.createAdapterProxy<T>(target, httpServer);
  }
} 

HttpAdapter는 NestJS Core에서 정의하고 있는AbstractHttpAdapter 인터페이스를 구현하고 있는 클래스에 해당합니다. AbstractHttpAdapter는 http 요청을 처리하기 위한 필요한 인터페이스를 정의하고 있는 HttpServer인터페이스의 일부를 구현한 추상 클래스입니다.

// packages/core/adapters/http-adapters.ts
export abstract class AbstractHttpAdapter<
  TServer = any,
  TRequest = any,
  TResponse = any,
> implements HttpServer<TRequest, TResponse>
{
  protected httpServer: TServer;

  constructor(protected instance?: any) {}

  public use(...args: any[]) {
    return this.instance.use(...args);
  }

  public get(...args: any[]) {
    return this.instance.get(...args);
  }

  public post(...args: any[]) {
    return this.instance.post(...args);
  } 
  ...
  abstract close();
  abstract status(response: any, statusCode: number);
  abstract reply(response: any, body: any, statusCode?: number);
  abstract end(response: any, message?: string);
  abstract render(response: any, view: string, options: any);
  abstract redirect(response: any, statusCode: number, url: string);
}

한 가지 재미있는 점은, AbstractHttpAdapter가 일부 메서드(ex, use, get, post) 메서드의 구현을 any 타입으로 주입받은 instance에 의존하고 있다는 점입니다. 어떤 분들은 메서드의 이름들이 익숙하다는 것을 이미 눈치채셨을 것 같습니다.

그렇습니다. 해당 메서드는 express에서 미들웨어나 핸들러를 등록할 때 사용하는 메서드 명과 일치합니다. 실제로 NestFactoryexpress에 의존하여 AbstractHttpAdapter 인터페이스를 구현한 ExpressAdapter를 기본값으로 사용하고 있습니다. 이러한 관점에서, NestJS는 일종의 express-wrapper 프레임워크라고도 볼 수 있습니다. express와 익숙하신 분들이라면 정말 반가운 일이 아닐 수 없습니다.

// packages/platform-express/adapters/express-adapter.ts
export class ExpressAdapter extends AbstractHttpAdapter {
  ...
  constructor(instance?: any) {
    super(instance || express()); // << express!!
  }
}
// packages/core/nest-factory.ts
export class NestFactoryStatic {
  ...
  private createHttpAdapter<T = any>(httpServer?: T): AbstractHttpAdapter {
    const { ExpressAdapter } = loadAdapter(
      '@nestjs/platform-express',
      'HTTP',
      () => require('@nestjs/platform-express'),
    );
    return new ExpressAdapter(httpServer);
  }
}

ApplicationConfig

이제 다음 줄로 넘어가보도록 하겠습니다. NestFactory는 이후 ApplicationConfig 인스턴스를 생성합니다.

// packages/core/nest-factory.ts
export class NestFactory {
  public async create<T extends INestApplication = INestApplication>(
    module: any,
    serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
    options?: NestApplicationOptions,
  ): Promise<T> {
    const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
      ? [serverOrOptions, options]
      : [this.createHttpAdapter(), serverOrOptions];
      
	// =======================================================
    const applicationConfig = new ApplicationConfig();
    // =======================================================

    const container = new NestContainer(applicationConfig);  
    this.setAbortOnError(serverOrOptions, options);
    this.registerLoggerConfiguration(appOptions);

	await this.initialize(module, container, applicationConfig, httpServer);

    const instance = new NestApplication(
      container,
      httpServer,
      applicationConfig,
      appOptions,
    );
    const target = this.createNestInstance(instance);
    return this.createAdapterProxy<T>(target, httpServer);
  }
} 

ApplicationConfigNestJS에서 사용되는 글로벌 미들웨어들을 저장하고 있는 비교적 단순한 데이터 객체에 해당합니다. 최초에는 대부분 빈 값으로 초기화가 되어 있지만, 우리가 NestApplication 생성하는 과정에서 모든 글로벌 미들웨어들은 내부적으로 ApplicationConfig에 등록됩니다.

// packages/core/application-config.ts
export class ApplicationConfig {
  private globalPrefix = '';
  private globalPrefixOptions: GlobalPrefixOptions<ExcludeRouteMetadata> = {};
  private globalPipes: Array<PipeTransform> = [];
  private globalFilters: Array<ExceptionFilter> = [];
  private globalInterceptors: Array<NestInterceptor> = [];
  private globalGuards: Array<CanActivate> = [];
  private versioningOptions: VersioningOptions;
  ...
}

NestContainer

다음으로 NestFactoryNestJS의 의존성 주입(DI)에서 핵심적인 역할을 수행하는 NestContainer 객체를 생성합니다. NestContainerNestApplication이 동작하기 위해 필수적인 모듈(Module)을 저장하고 있는 객체입니다.

// packages/core/nest-factory.ts
export class NestFactory {
  public async create<T extends INestApplication = INestApplication>(
    module: any,
    serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
    options?: NestApplicationOptions,
  ): Promise<T> {
    const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
      ? [serverOrOptions, options]
      : [this.createHttpAdapter(), serverOrOptions];

    const applicationConfig = new ApplicationConfig();
      
	// =======================================================
    const container = new NestContainer(applicationConfig);
	// =======================================================
      
    this.setAbortOnError(serverOrOptions, options);
    this.registerLoggerConfiguration(appOptions);

	await this.initialize(module, container, applicationConfig, httpServer);

    const instance = new NestApplication(
      container,
      httpServer,
      applicationConfig,
      appOptions,
    );
    const target = this.createNestInstance(instance);
    return this.createAdapterProxy<T>(target, httpServer);
  }
} 

NestContainer는 내부적으로는 Module, ModuleTokenFactory, ModuleCompiler, ModulesContainer, 과 같은 클래스들을 가지고 있습니다. NestContainer에는 이후 DependenciesScanner를 통해 모듈이 등록됩니다. 이와 관련된 보다 자세한 내용은 뒤에 이어서 다루도록 하겠습니다.

// packages/core/injector/factory.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>>();
  ...
  
  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);
  }
}

Module

Module 클래스는 하나의 모듈에 포함되어 있는 모든 의존성(ex, Provider, Controller)을 정의하고 있는 핵심 객체입니다. 우리가 일반적으로 @Module 데코레이터 혹은 forRoot() 메서드를 통해 DynamicModule 형태로 등록한 모듈들은 NestJS 프레임워크 안에서는 Module 클래스로 관리됩니다.

// packages/core/injector/module.ts
export class Module {
  private readonly _id: string;
  private readonly _imports = new Set<Module>();
  private readonly _providers = new Map<
    InstanceToken,
    InstanceWrapper<Injectable>
  >();
  private readonly _injectables = new Map<
    InstanceToken,
    InstanceWrapper<Injectable>
  >();
  private readonly _middlewares = new Map<
    InstanceToken,
    InstanceWrapper<Injectable>
  >();
  private readonly _controllers = new Map<
    InstanceToken,
    InstanceWrapper<Controller>
  >();
  ...
}

ModuleTokenFactory

ModuleTokenFactory는 간단히 말하면, 모듈들을 등록/관리 할 때 키로 사용되는 토큰(hash)을 생성하는 클래스입니다.

// packages/core/injector/module-token-factory.ts
export class ModuleTokenFactory {
  public create(
    metatype: Type<unknown>,
    dynamicModuleMetadata?: Partial<DynamicModule> | undefined,
  ): string {
    const moduleId = this.getModuleId(metatype);
    const opaqueToken = {
      id: moduleId,
      module: this.getModuleName(metatype),
      dynamic: this.getDynamicMetadataToken(dynamicModuleMetadata),
    };
    return hash(opaqueToken, { ignoreUnknown: true });
  }
  ...
}

ModuleCompiler

NestContainer에서는 DynamicModule의 의존성 관련 정보를 따로 분리하여 관리합니다. ModuleCompiler는 이를 위해, Module의 정보를 적절히 파싱하는 기능을 수행하는 일종의 유틸리티 클래스에 해당한다고 볼수 있습니다.

// packages/core/injector/compiler.ts
export class ModuleCompiler {
  public async compile(
    metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
  ): Promise<ModuleFactory> {
    const { type, dynamicMetadata } = this.extractMetadata(await metatype);
    const token = this.moduleTokenFactory.create(type, dynamicMetadata);
    return { type, dynamicMetadata, token };
  }
  ...
}

ModulesContainer

ModulesContainer는 모든 Module 정보가 저장되는 비교적 단순한 Map<string, Module> 타입의 클래스입니다. 키 값으로는 ModuleTokenFactory에서 생성된 토큰을 사용하게 됩니다.

// packages/core/injector/modules-container.ts
export class ModulesContainer extends Map<string, Module> {
  private readonly _applicationId = uuid();

  get applicationId(): string {
    return this._applicationId;
  }
}

initialize()

다음으로 NestFactory는 간단한 설정을 마친 후, initialize() 메서드를 호출합니다. initalize() 매서드의 핵심 역할은 크게 DependenciesScanner에 의한 모듈 등록 과정과 InstanceLoader에 의한 의존성 객체 생성 과정으로 나눌 수 있습니다.

// packages/core/nest-factory.ts
export class NestFactory{
  ...
  private async initialize(
    module: any,
    container: NestContainer,
    config = new ApplicationConfig(),
    httpServer: HttpServer = null,
      ) {
        const instanceLoader = new InstanceLoader(container);
        const metadataScanner = new MetadataScanner();
        const dependenciesScanner = new DependenciesScanner(
          container,
          metadataScanner,
          config,
        );
        container.setHttpAdapter(httpServer);

        const teardown = this.abortOnError === false ? rethrow : undefined;
        await httpServer?.init();
        try {
          this.logger.log(MESSAGES.APPLICATION_START);

          await ExceptionsZone.asyncRun(
            async () => {
              await dependenciesScanner.scan(module);
              await instanceLoader.createInstancesOfDependencies();
              dependenciesScanner.applyApplicationProviders();
            },
            teardown,
            this.autoFlushLogs,
          );
        } catch (e) {
          this.handleInitializationError(e);
        }
    }
}

DependenciesScanner

DependenciesScanner의 가장 중요한 역할은 모듈 트리를 순회하며 NestContainer에 모듈들을 등록하는 것입니다. 뿐만 아니라, DependenciesScanner는 모듈간의 연결관계(edge)와 관련된 정보를 등록하고, 모듈에서 관리하는 의존성(ex, providers) 등의 메타정보를 관리하고, 이후 NestApplication에서 모듈을 순회하는(ex, Module Lifecycle Hooks) 과정에서 필요한 위상정렬을 위해, 모듈의 깊이를 계산하는 등의 다양한 기능을 추가적으로 수행합니다.

// packages/core/scanner.ts
export class DependenciesScanner {
  ...
  public async scan(module: Type<any>) {
    await this.registerCoreModule(); // NestContainer에 InternalCoreModule 등록
    await this.scanForModules(module); // Module Tree를 재귀적으로 탐색하여 모든 모듈을 NestContainer에 등록 
    await this.scanModulesForDependencies(); // 메타데이터 파싱을 통해 등록된 모듈의 의존성(imports, providers, controllers, exports) 관련 정보를 추가 
    this.calculateModulesDistance(); // Module Tree를 순회하며 모듈을 위상 정렬(Topological Sort)하기 위해 필요한 depth 정보를 등록

    this.addScopedEnhancersMetadata(); // Provider의 Scope 관련 메타데이터를 Controller에 추가 등록
    this.container.bindGlobalScope(); // Global Module을 import 하고 있는 모듈들에 연결 정보 추가
  }

InstanceLoader

InstanceLoaderNestContainer에 등록된 모듈의 Provider, Injectable, Controller의 프로토타입 객체와 인스턴스를 순서대로 생성하는 역할을 합니다. 프로토타입을 생성하는 내부 과정은 단순하지만, 복잡한 의존성 주입 관계를 갖고 있는 인스턴스를 생성하는 과정은 비교적 쉽지 않습니다. 해당 기능은 Injector라 불리는 내부 클래스에 의해 실행됩니다. Injector는 의존성 객체의 위계를 파악하고 콜백을 활용하여 순서를 조율하고, 이를 적절히 주입하여 모든 의존성 객체들의 인스턴스를 생성하는 등, DI에서 핵심적인 역할을 수행합니다. 이번 포스팅에서는 다루기에는 내용이 많은 관계로 Injector는 추후 다른 포스팅에서 자세히 알아보도록 하겠습니다.

// packages/core/injector/instance-loader.ts
export class InstanceLoader {
  protected readonly injector = new Injector();

  public async createInstancesOfDependencies(
    modules: Map<string, Module> = this.container.getModules(),
  ) {
    this.createPrototypes(modules); // 의존성 객체의 prototype 생성
    await this.createInstances(modules); // 의존성 객체의 instance 생성
  }
  ...
}

NestApplication

드디어 마지막 과정입니다. NestFactory는 모듈과 의존성 객체 등록 과정을 모두 마친 후, NestApplication 인스턴스를 생성합니다. 이후, 에러 핸들링, 메서드 체이닝, fallback 등 일부 기능을 위해 Proxy 객체로 감싸는 것을 끝으로, NestFactory의 역할이 끝나게 됩니다.

// packages/core/nest-factory.ts
export class NestFactory {
  public async create<T extends INestApplication = INestApplication>(
    module: any,
    serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
    options?: NestApplicationOptions,
  ): Promise<T> {
    const [httpServer, appOptions] = this.isHttpServer(serverOrOptions)
      ? [serverOrOptions, options]
      : [this.createHttpAdapter(), serverOrOptions];

    const applicationConfig = new ApplicationConfig();
    const container = new NestContainer(applicationConfig);
    this.setAbortOnError(serverOrOptions, options);
    this.registerLoggerConfiguration(appOptions);

	await this.initialize(module, container, applicationConfig, httpServer);

	// =======================================================
    const instance = new NestApplication(
      container,
      httpServer,
      applicationConfig,
      appOptions,
    );
    const target = this.createNestInstance(instance);
    return this.createAdapterProxy<T>(target, httpServer);
    // =======================================================
  }
} 

마무리

이번 포스팅에서는 NestFactory를 통해 모든 서비스 로직을 담고 있는 NestApplication이 생성되는 과정을 살펴보았습니다. 다음 포스팅에서는 NestApplication이 어떻게 서버에 전달된 요청을 처리하는지와 관련된 내용을 다루어 보도록 하겠습니다. 감사합니다.

참고자료

profile
안녕하세요

0개의 댓글