Dynamic Modules

이연중·2021년 9월 14일
0

NestJS

목록 보기
13/22
post-custom-banner

모듈은 어플리케이션의 모듈식 부분으로 연관된 프로바이더 및 컨트롤러와 같은 컴포넌트를 그룹화고, 이러한 구성 요소에 대한 실행 컨텍스트 또는 범위를 제공한다.

예를 들어, 모듈에 정의된 프로바이더는 export할 필요없이 같은 모듈의 다른 멤버에서 사용할 수 있다.

프로바이더가 모듈 외부에서 사용되어야 하는 경우 먼저 호스트 모듈에서 export하고 사용하려는 모듈에서 import하면 된다.

다음은 UsersService를 포함하는 UsersModule을 정의하는 것이다.

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

이 모듈을 import하는 AuthModule을 정의하여 UsersModule에서 export한 프로바이더를 사용할 수 있게 한다.

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

이를 통해 다음과 같이 AuthModule에서 호스팅되는 AuthServiceUsersService를 삽입할 수 있다.

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}
  /*
    Implementation that makes use of this.usersService
  */
}

위와 같은 바인딩 과정을 정적 모듈 바인딩이라 한다.

Nest가 모듈을 함께 연결하는 데 필요한 모든 정보는 호스트 및 이를 사용하려는 모듈에서 선언이 된다.

Nest는 다음과 같은 과정을 통해 AuthMdule내에서 UsersService를 사용할 수 있다.

  1. UsersModuleUsersModule이 import한 다른 모듈들을 인스턴스화 한다.
  2. AuthModule을 인스턴스화하고, UsersModule에서 export한 프로바이더를 AuthModule에서 사용할 수 있게 한다.
  3. AuthServiceUsersService의 인스턴스를 삽입한다.

Dynamic Module Use Case


정적 모듈 바인딩을 사용하면, 호스트 모듈의 프로바이더가 구성되는 방식에 이를 사용하는 모듈(이하 소비 모듈)이 관여할 수 없다.

만약, 상황마다 다르게 동작해야 하는 범용 모듈의 경우, 사용전에 조작이 필요한 경우가 있을 것이다.

Nest에서 이를 위해 Configuration Module(이하 구성 모듈)을 제공한다. 많은 어플리케이션은 구성 모듈을 사용해 구성 세부 정보를 구체화할 수 있다.

이를 통해 개발자를 위한 개발 데이터베이스, staging/test 환경을 위한 staging 데이터베이스 등 다양한 배포에서 어플리케이션 설정을 동적으로 쉽게 변경할 수 있다.

문제는 구성 모듈을 소비 모듈에서 Custom하게 정의해야한다는 것인데, 동적 모듈이 이를 해결해 준다.

동적 모듈 기능을 사용해 구성 모듈을 동적으로 만들 수 있으므로 소비 모듈이 API를 사용해 가져올 때 구성 모듈을 Custom하게 정의하는 방법을 제어할 수 있다.

즉, 동적 모듈은 정적 바인딩을 사용하는 것과 반대로 한 모듈을 다른 모듈로 가져오고 해당 모듈을 가져올 때 해당 모듈의 속성과 동작을 Custom하게 정의하는 API를 제공한다.

Config Module Example


ConfigModuleoptions 객체를 받아 사용자 정의하도록 한다.

동적 모듈을 이용해 매개변수로 넘어오는 options으로 넘어온 경로에서 .env 파일을 찾도록 한다.

다음은 구성 객체를 전달하는 동적 모듈을 import하는 예이다. 아래 예는 config 디렉터리 아래 .env파일 생성해 config 파일에서 이를 찾아올 수 있게 한다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [ConfigModule.register({ folder: './config' })],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

이때, register() 메서드는 DynamicModule이다. 동적 모듈은 런타임에 생성되며, 정적 모듈과 동일한 속성과 module(동적 모듈의 이름을 정의하는 필드)이라는 하나의 추가 속성을 포함한다.

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static register(): DynamicModule {
    return {
      module: ConfigModule,
      providers: [ConfigService],
      exports: [ConfigService],
    };
  }
}

ConfigModule.register(...)을 호출하면 DynamicModule 객체가 반환된다.

Module Configuration


위 예에서 ConfigModuleConfigService를 export하는 호스트 모듈이다.

동작을 사용자 정의하기 위해 options 객체를 읽어내는 것은 실제로 ConfigService이다.

register() 메서드에서 ConfigServiceoptions를 가져오는 방법을 알고 있다고 가정해보자

options 객체의 속성을 기반으로 동작을 사용자 지정하기 위해 서비스를 몇가지 변경할 수 있다.

import { Injectable } from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor() {
    const options = { folder: './config' };

    const filePath = `${process.env.NODE_ENV || 'development'}.env`;
    const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
    this.envConfig = dotenv.parse(fs.readFileSync(envFile));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}

이제 ConfigServiceoptions에서 지정한 폴더해서 .env 파일을 찾을 수 있다.

남은 작업은 register() 메서드에서 options 객체를 ConfigService에 주입하는 것이다.

ConfigModuleConfigService를 제공한다. ConfigService는 런타임에만 제공되는 options 객체에 의존한다.

따라서 런타임에 먼저 options 객체를 Nest IoC 컨테이너에 바인딩한 다음 Nest가 이를 ConfigService에 주입하도록 해야한다.

options 객체를 IoC 컨테이너에 바인딩 하는 작업은 register() 메서드에서 수행한다.

다음과 같이 options 객체를 프로바이더로 정의하면 된다.

import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({})
export class ConfigModule {
  static register(options): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

이제 'CONFIG_OPTIONS' 프로바이더를 ConfigService에 주입하면 된다.

import * as dotenv from 'dotenv';
import * as fs from 'fs';
import { Injectable, Inject } from '@nestjs/common';
import { EnvConfig } from './interfaces';

@Injectable()
export class ConfigService {
  private readonly envConfig: EnvConfig;

  constructor(@Inject('CONFIG_OPTIONS') private options) {
    const filePath = `${process.env.NODE_ENV || 'development'}.env`;
    const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
    this.envConfig = dotenv.parse(fs.readFileSync(envFile));
  }

  get(key: string): string {
    return this.envConfig[key];
  }
}

위 예에서는 'CONFIG_OPTIONS' 객체를 문자열 그대로 사용했지만, 이 부분은 지양한다.

이렇게 하는 것이 아닌, 아래와 같이 이를 별도에 파일에 상수 or symbol로 정의하고 해당 파일을 가져오는 방식을 사용하는 것이 좋다.

export const CONFIG_OPTIONS = 'CONFIG_OPTIONS';

참고

https://docs.nestjs.kr/fundamentals/dynamic-modules

profile
Always's Archives
post-custom-banner

0개의 댓글