Static module binding
아래 UsersModule은 UsersService를 provide & export하고 있다.
즉, UsersModule은 UsersService의 host module이 된다.
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
다음으로, UsersModule을 import하는 AuthModule을 정의한다.
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 {}
이렇게 모듈 관계가 만들어진 상황에서, 아래와 같은 생성자는 UsersService의 inject를 허용한다.
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
*/
}
이러한 module binding을 `static module binding`이라고 한다.
Nest에서 module을 wire하는데 필요한 모든 정보가 이미 host와 consuming modules에 선언되어 있는 상황을 의미한다.
`AuthModule`에서 `UsersService`를 주입받아 사용하는 과정은 다음과 같다.
1. `UsersModule`을 instance화 하면서, transitively하게 다른 dependencies를 resolve한다.
2. `AuthModule`을 인스턴스화 하면서, `UsersModule`의 exported providers를 AuthModule 내부에서 접근 가능한 컴포넌트로 등록한다.
3. `UsersService`가 `AuthService`에 주입된다.
Dynamic module use case
static module binding의 단점은 consuming module에게 host module의 provider들의 configuration에 관여할 수 있는 기회가 주어지지 않는다는 점이다.
Generic한 목적을 가진 module의 경우 다양한 consuming module에서 import 되어 사용할 수 있지만, 각자의 환경에 맞춰 import하는 것이 더 유용할 것이다.
따라서 이러한 문제를 해결하기 위해 도입된 것이 dynamic module이다.
Config module example
대표적으로 generic purpose를 가진 module인 configuration module을 사용해서 dynamic module의 사용 예시를 살펴본다.
우리의 목적은 ConfigModule이 options object를 받아 이를 커스터마이징할 수 있게 하는 것이다.
기본적으로 하드코딩되어 있는 `.env`파일의 위치는 프로젝트의 root 폴더이다.
이를 사용자가 원하는 경로로 변경하는 것을 목적으로 한다고 가정한다.
Dynamic module은 import되는 모듈에게 파라미터를 전달할 수 있게 해주며, 해당 모듈의 동작을 변경할 수 있게 도와준다.
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 {}
위 예시를 통해 몇 가지 사실을 알 수 있다.
1. ConfigModule은 `register()`라는 static method를 가진다. 일반적으로 이러한 static method의 이름은 `forRoot()` 혹은 `register()`로 명명한다.
2. `register()` method는 사용자에 의하여 정의되므로, 우리는 우리가 원하는 어떠한 arguments도 넣을 수 있다.
3. `register()` method는 module을 리턴할 것임을 추측할 수 있다.
Dynamic module은 static moduler과 크게 다르지 않다. 단지 run time에 생성되는 모듈이라는 점에서 그 차이를 가진다.
Dynamic module은 반드시 완벽하게 같은 interface를 가진 객체를 리턴해야 하며, module이라고 하는 하나의 추가적인 property를 가진다.
또한, 모듈의 class name과 동일한 객체를 리턴해야한다.
ConfigModule은 아래와 같이 구현할 수 있다.
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],
};
}
}
그렇다면 어떻게 Module.register 함수를 통해 전달된 option parameter가 Service에게 전달될 수 있는 것일까?
이는 Dependency Injection을 활용하여 이루어진다.
우선, 우리는 의존성을 주입하기 위하여 options 객체를 IoC container에 등록해야하며, 이후 Nest는 이 option 객체를 ConfigService에 주입한다.
이를 구현하면 다음과 같다.
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({})
export class ConfigModule {
static register(options: Record<string, any>): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
ConfigService,
],
exports: [ConfigService],
};
}
}
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import * as path from 'path';
import { Injectable, Inject } from '@nestjs/common';
import { EnvConfig } from './interfaces';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor(@Inject('CONFIG_OPTIONS') private options: Record<string, any>) {
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' 토큰으로 options 객체를 IoC container에 등록하고, `@Inject` 데코레이터를 통해 ConfigService에 해당 객체를 주입한다.