[Nest JS] @Global 데코레이터 톺아보기

toto9602·2024년 3월 29일
0

NestJS 공부하기

목록 보기
3/4

참고 자료

Nest JS Docs : global modules

Nest.js는 실제로 어떻게 의존성을 주입해줄까?

1. Nest JS의 @Global 데코레이터란?

Nest JS는 애플리케이션 구조의 설계를 위해 @Module 데코레이터를 제공하여, Provider들을 모듈로 묶어 구조화하는 방식을 많이 사용합니다 :)

Nest가 강하게 영향을 받았다는 Angular의 구조와 조금 다르게, Nest JS의 Provider들은 모듈 scope 안에 캡슐화되기 때문에, 기본적으로 다른 모듈에서 사용이 불가능한데요.

특정 모듈에 등록된 Provider를 exports 하여 사용할 수도 있지만,
Nest는 여러 번 반복 사용되는 모듈을 매번 imports 할 필요가 없도록, 특정 모듈을 전역에서 사용 가능하다록 하는 @Global 데코레이터를 제공합니다!

오늘 포스팅에서는, @Global 데코레이터가 어떤 식으로 모듈을 전역으로 주입해 주는지 알아보고자 합니다!!

P.S

Nest 공식 문서 설명에 따르면, 모든 모듈을 Global 하게 만들어 버리는 것은 좋은 설계 방식이 아니라고 합니다.
되도록 imports 문을 사용하여 모듈을 사용하고, Global 인 필수적인 부분에 사용하는 것이 좋을 것 같습니다! :D

2. @Global 데코레이터 코드 구경하기

 * @publicApi
 */
export function Global(): ClassDecorator {
  return (target: Function) => {
    Reflect.defineMetadata(GLOBAL_MODULE_METADATA, true, target);
  };
}
export const GLOBAL_MODULE_METADATA = '__module:global__';

@Global 데코레이터 자체에는, 생각보다 별다른 로직이 없네요!
target에 GLOBAL_MODULE_METADATA라는 키로, true 값을 metadata로 등록해 주고 있네요.

사실 @nestjs/common 패키지에서 제공되는 데코레이터들은,
가장 많이 쓰이는 @Injectable , @Controller 데코레이터 등을 포함해서 유사한 방식으로 구현된 경우가 많은 것 같습니다 :)

그렇다면, 결국 Nest JS가 실제로 의존성을 주입해주는 부분인 Nestfactory.create 부분을 오늘도 봐야 할 것 같습니다!

P.S.

Nest JS가 실제로 어떻게 의존성을 주입해주는가?에 관해서는,
너무도 잘 정리해 주신 글이 있어 참고하시면 좋을 듯합니다!

참고자료 : Nest.js는 실제로 어떻게 의존성을 주입해줄까?

1) NestFactory.create ( main.ts )

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

Nest JS의 main.ts 파일은 보통 위와 같은 느낌으로 작성하게 되는데요,
오늘은 @Global 데코레이터의 여정을 따라가기 위해 :) 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);
}

private isHttpServer(
  serverOrOptions: AbstractHttpAdapter | NestApplicationOptions,
): serverOrOptions is AbstractHttpAdapter {
  return !!(
    serverOrOptions && (serverOrOptions as AbstractHttpAdapter).patch
  );
}

코드가 꽤나 길고 복잡하지만 ^^;
오늘은 @Global 데코레이터를 알아보기 위해, 모듈을 실제로 초기화하는 것으로 보이는 initialize 메서드를 따라가 보겠습니다!

2) NestFactory - initialize

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

앞서 언급한 참고자료에도 적혀 있지만, initialize 메서드에서 조금 중요해 보이는 부분은 세 부분 정도인 것 같습니다!

  1. 모듈의 모든 의존성을 스캔하기 (dependenciesScanner.scan)
  2. 스캔한 의존성들의 인스턴스를 생성하기 ( instanceLoader.createInstancesOfDependencies)
  3. 인스턴스로 만든 의존성들의 인스턴스를 적용하기 (dependenciesScanner.applyApplicationProviders )

@Global 데코레이터는 어떤 모듈을 다른 모듈들 모두의 의존성으로 넣어주는 역할이니, 우선 1번 부분을 보아야 할 것 같네요!

3) dependenciesScanner.scan

// packages/core/scanner.ts
public async scan(module: Type<any>) {
  await this.registerCoreModule();
  await this.scanForModules(module); 
  // 전체 모듈 정보를 스캔(로딩)!
  await this.scanModulesForDependencies();
  this.calculateModulesDistance();

  this.addScopedEnhancersMetadata();
  this.container.bindGlobalScope(); // !!!
}

오! 마지막 bindGlobalScope 메서드가, 찾아 헤메던 @Global 적용 부분일 것만 같네요!
바로 살펴보러 가보도록 하겠습니다 :)

하지만 그 전에 잠깐! 전체 모듈 정보를 스캔(로딩)하는 부분인 scanForModules 메서드를 보고 넘어가겠습니다 :)

scanForModules

// packages/core/scanner.ts
public async scanForModules(
  moduleDefinition:
    | ForwardReference
    | Type<unknown>
    | DynamicModule
    | Promise<DynamicModule>,
  scope: Type<unknown>[] = [],
  ctxRegistry: (ForwardReference | DynamicModule | Type<unknown>)[] = [],
): Promise<Module[]> {
  // scope = []
  // ctxRegistry = []
  
  const moduleInstance = await this.insertModule(moduleDefinition, scope);
	...
    ...
    ....
    .....
    ....
    
   const moduleRefs = await this.scanForModules(
      innerModule,
      [].concat(scope, moduleDefinition),
      ctxRegistry,
    );
// 재귀를 도는 방식으로, 모든 모듈에 대해 insertModule을 호출!!
...
...
...
....
}

  public async insertModule(
    moduleDefinition: any,
    scope: Type<unknown>[],
  ): Promise<Module | undefined> {
    const moduleToAdd = this.isForwardReference(moduleDefinition)
      ? moduleDefinition.forwardRef()
      : moduleDefinition;

    if (
      this.isInjectable(moduleToAdd) ||
      this.isController(moduleToAdd) ||
      this.isExceptionFilter(moduleToAdd)
    ) {
      throw new InvalidClassModuleException(moduleDefinition, scope);
    }

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

  // 1. moduleDefinition이 ForwardRef 라면 .forwardRef 메서드 호출
  // 2. 모듈이 Injectable이거나, 컨트롤러거나, 예외 필터면 경고 로그 출력
  // 3. 모듈 컴파일 -> 타입, 동적 메타데이터, 토큰을 가져옴
  // 4. 이를 기반으로 새로운 모듈 객체 생성
  // 5. 컨테이너의 modules(ModulesContainer)에 토큰과 모듈을 등록
  // 6. 만들어진 모듈 객체를 반환

사실 scanForModules 메서드는
파라미터로 들어가는 module에서 시작해서, DFS 방식으로 모든 모듈을 순회하며 모듈을 스캔하는 로직을 수행하며,
내용이 꽤나 방대합니다! ^^;

다만 오늘은 @Global 데코레이터를 알아보는 시간이니,
모든 모듈에 대해 insertModule을 호출한다는 점 정도를 알고 넘어가고,
필요한 insertModule 쪽을 집중해서 보도록 할게요!

메서드 자체에 대한 보다 자세한 내용은 앞서 참고한 글을 보시는 걸 권하고자 합니다!

insertModule 또한, 컨테이너에 해당 모듈을 addModule해주는 것 외에 별다른 로직은 없는 것 같은데요,
그렇다면 이제 addModule을 보러 갈 차례네요!

addModule

// packages/core/injector/container.ts
public async addModule(
    metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
    scope: Type<any>[],
  ): Promise<Module | undefined> {
    // In DependenciesScanner#scanForModules we already check for undefined or invalid modules
    // We still need to catch the edge-case of `forwardRef(() => undefined)`
    if (!metatype) {
      throw new UndefinedForwardRefException(scope);
    }
    const { type, dynamicMetadata, token } = await this.moduleCompiler.compile(
      metatype,
    );
    if (this.modules.has(token)) {
      return this.modules.get(token);
    }
    const moduleRef = new Module(type, this);
    moduleRef.token = token;
    moduleRef.initOnPreview = this.shouldInitOnPreview(type);
    this.modules.set(token, moduleRef);

    const updatedScope = [].concat(scope, type);
    await this.addDynamicMetadata(token, dynamicMetadata, updatedScope);


	// GlobalModule이라면
    if (this.isGlobalModule(type, dynamicMetadata)) {
      moduleRef.isGlobal = true;
      this.addGlobalModule(moduleRef);
    }
    return moduleRef;
  }
  
  // Global Module인지 여부를 확인!
  public isGlobalModule(
    metatype: Type<any>,
    dynamicMetadata?: Partial<DynamicModule>,
  ): boolean {
    if (dynamicMetadata && dynamicMetadata.global) {
      return true;
    }
    // GLOBAL_MODULE_METADATA가 true로 설정되어 있는지 여부를 return
    return !!Reflect.getMetadata(GLOBAL_MODULE_METADATA, metatype);
  }
  
// private readonly globalModules = new Set<Module>();
  public addGlobalModule(module: Module) {
    this.globalModules.add(module);
  }  

addModule 메서드는, 앞서 등록된 GLOBAL_MODULE_METADATA로 global 모듈 여부를 확인 후, global 모듈이라면 globalModules Set에 해당 모듈을 add해 주네요!

이제 여기까지, Container의 globalModules에 global 모듈 set이 들어가는 과정을 확인했네요! :)

이제 다시, 5. dependencies.scan 메서드에 bindGlobalScope로 돌아가 보겠습니다! :D

(다시) 3) dependenciesScanner.scan

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

  this.addScopedEnhancersMetadata();
  this.container.bindGlobalScope(); // 이제 bindGlobalScope를 보러 가요!
}

bindGlobalScope

// packages/core/injector/container.ts

  public bindGlobalScope() {
    // 모든 모듈을 순회하며, bindGlobalsToImports 호출!
    this.modules.forEach(moduleRef => this.bindGlobalsToImports(moduleRef));
  }
 public bindGlobalsToImports(moduleRef: Module) {
   // 전역 모듈을 순회하며, globalModule을 모든 모듈에 bind!
    this.globalModules.forEach(globalModule =>
      this.bindGlobalModuleToModule(moduleRef, globalModule),
    );
  }
  public bindGlobalModuleToModule(target: Module, globalModule: Module) {
    // 동일한 모듈이거나, internalCoreModule이라면 그냥 return 
    if (target === globalModule || target === this.internalCoreModule) {
      return;
    }
    // target에 globalModule을 더하기!
    target.addRelatedModule(globalModule);
  }
// packages/core/injector/module.ts
  public addRelatedModule(module: Module) {
    this._imports.add(module);
  }

bindGlobalScope 메서드는 모든 모듈을 순회하며,
globalModule들을 각 모듈의 imports 에 등록된 Set에 더해주네요!

여기까지! @Global 데코레이터가 등록된 모듈들이, 다른 모든 모듈들에 import되는 로직을 다 본 것 같습니다! :D

결론

  • @Global 데코레이터는 GLOBAL_MODULE_METADATA값을 true로 메타데이터를 등록!
  • DependenciesScanner가 초기화 과정에서 Container의 전역 모듈 Set에 전역 모듈을 add해 준다!
  • Container는 이후, 모든 모듈들을 순회하며 Global 모듈 Set을 모든 모듈의 imports Set에 더해준다!
profile
주니어 백엔드 개발자입니다! 조용한 시간에 읽고 쓰는 것을 좋아합니다 :)

0개의 댓글