NestJS Dynamic Module이란?
NestJS의 일반 모듈은 @Module() 데코레이터에 고정된 설정을 가진다. 반면 Dynamic Module은 forRoot(), forRootAsync(), register() 같은 정적 메서드를 통해 런타임에 설정을 주입받는 모듈이다. ConfigModule.forRoot(), TypeOrmModule.forRoot() 등 NestJS 공식 라이브러리 대부분이 이 패턴을 사용한다.
// 일반 모듈: 설정 고정
@Module({
providers: [MyService],
exports: [MyService],
})
export class MyModule {}
// Dynamic Module: 설정을 외부에서 주입
@Module({})
export class CacheModule {
static forRoot(options: CacheOptions): DynamicModule {
return {
module: CacheModule,
providers: [
{ provide: 'CACHE_OPTIONS', useValue: options },
CacheService,
],
exports: [CacheService],
};
}
}
forRoot vs forFeature vs register
NestJS 커뮤니티에서 통용되는 네이밍 컨벤션이 있다. 이를 따르면 모듈 사용자가 직관적으로 용도를 파악할 수 있다.
| 메서드 | 용도 | 호출 위치 | global 여부 |
|---|---|---|---|
| forRoot() | 전역 설정 (1회) | AppModule | 보통 global: true |
| forFeature() | 기능별 세부 설정 | 각 Feature Module | 아니오 |
| register() | 매번 새 인스턴스 | 어디서든 | 아니오 |
// AppModule에서 1회 글로벌 설정
@Module({
imports: [
NotificationModule.forRoot({
defaultChannel: 'email',
retryCount: 3,
}),
],
})
export class AppModule {}
// 각 Feature Module에서 세부 설정
@Module({
imports: [
NotificationModule.forFeature({
channel: 'slack',
webhookUrl: 'https://hooks.slack.com/...',
}),
],
})
export class OrderModule {}
forRootAsync: 비동기 설정 주입
실무에서는 DB 접속 정보, API 키 등을 ConfigService에서 가져와야 한다. forRootAsync()는 useFactory, useClass, useExisting 패턴으로 비동기 설정을 지원한다.
@Module({})
export class StorageModule {
static forRootAsync(options: StorageAsyncOptions): DynamicModule {
return {
module: StorageModule,
global: true,
imports: options.imports || [],
providers: [
{
provide: 'STORAGE_OPTIONS',
useFactory: options.useFactory,
inject: options.inject || [],
},
StorageService,
],
exports: [StorageService],
};
}
}
// 사용 측: ConfigService에서 설정 주입
@Module({
imports: [
StorageModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
bucket: config.get('S3_BUCKET'),
region: config.get('AWS_REGION'),
credentials: {
accessKeyId: config.get('AWS_ACCESS_KEY'),
secretAccessKey: config.get('AWS_SECRET_KEY'),
},
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
AsyncOptions 인터페이스 표준 패턴
재사용 가능한 Dynamic Module을 만들려면 표준화된 AsyncOptions 인터페이스를 정의한다.
import { ModuleMetadata, Type } from '@nestjs/common';
// 옵션 팩토리 인터페이스
export interface StorageOptionsFactory {
createStorageOptions(): StorageOptions | Promise<StorageOptions>;
}
export interface StorageAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
// 방식 1: useFactory
useFactory?: (...args: any[]) => StorageOptions | Promise<StorageOptions>;
inject?: any[];
// 방식 2: useClass (새 인스턴스 생성)
useClass?: Type<StorageOptionsFactory>;
// 방식 3: useExisting (기존 프로바이더 재사용)
useExisting?: Type<StorageOptionsFactory>;
}
// useClass 패턴: 별도 설정 클래스
@Injectable()
export class StorageConfigService implements StorageOptionsFactory {
constructor(private config: ConfigService) {}
createStorageOptions(): StorageOptions {
return {
bucket: this.config.get('S3_BUCKET'),
region: this.config.get('AWS_REGION'),
};
}
}
// 사용
StorageModule.forRootAsync({
useClass: StorageConfigService,
})
ConfigurableModuleBuilder (NestJS 9+)
NestJS 9부터 ConfigurableModuleBuilder가 도입되어 보일러플레이트를 대폭 줄일 수 있다. 위의 모든 패턴을 자동 생성해준다.
import { ConfigurableModuleBuilder } from '@nestjs/common';
export interface NotificationOptions {
defaultChannel: string;
retryCount: number;
apiKey: string;
}
// 빌더가 forRoot, forRootAsync, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE 자동 생성
export const {
ConfigurableModuleClass,
MODULE_OPTIONS_TOKEN,
OPTIONS_TYPE,
ASYNC_OPTIONS_TYPE,
} = new ConfigurableModuleBuilder<NotificationOptions>()
.setClassMethodName('forRoot') // 메서드 이름 지정
.setExtras({ isGlobal: false }, (definition, extras) => ({
...definition,
global: extras.isGlobal, // global 옵션 지원
}))
.build();
// 모듈: ConfigurableModuleClass 상속만 하면 끝
@Module({
providers: [NotificationService],
exports: [NotificationService],
})
export class NotificationModule extends ConfigurableModuleClass {}
// 서비스: MODULE_OPTIONS_TOKEN으로 주입
@Injectable()
export class NotificationService {
constructor(
@Inject(MODULE_OPTIONS_TOKEN)
private options: NotificationOptions,
) {}
async send(message: string) {
console.log(`[${this.options.defaultChannel}] ${message}`);
// retry logic with this.options.retryCount
}
}
// 사용: 동기/비동기 모두 자동 지원
@Module({
imports: [
// 동기
NotificationModule.forRoot({
defaultChannel: 'slack',
retryCount: 3,
apiKey: 'xxx',
}),
// 비동기
NotificationModule.forRootAsync({
isGlobal: true,
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
defaultChannel: config.get('NOTIFY_CHANNEL'),
retryCount: config.getOrThrow('NOTIFY_RETRY'),
apiKey: config.getOrThrow('NOTIFY_API_KEY'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
forRoot + forFeature 조합 패턴
글로벌 설정(forRoot)과 기능별 설정(forFeature)을 함께 제공하는 실전 패턴이다. 메타데이터 기반 DI와 결합하면 매우 유연한 모듈을 설계할 수 있다.
@Module({})
export class EventBusModule {
// 글로벌 설정: 커넥션 정보
static forRoot(options: EventBusOptions): DynamicModule {
return {
module: EventBusModule,
global: true,
providers: [
{ provide: 'EVENT_BUS_OPTIONS', useValue: options },
EventBusService,
],
exports: [EventBusService],
};
}
// 기능별 설정: 이벤트 핸들러 등록
static forFeature(handlers: Type<EventHandler>[]): DynamicModule {
return {
module: EventBusModule,
providers: [
...handlers,
{
provide: 'EVENT_HANDLERS',
useFactory: (...instances: EventHandler[]) => instances,
inject: handlers,
},
],
};
}
}
// AppModule: 전역 설정
EventBusModule.forRoot({ url: 'amqp://localhost' })
// OrderModule: 핸들러 등록
EventBusModule.forFeature([OrderCreatedHandler, OrderCancelledHandler])
테스트에서 Dynamic Module 오버라이드
테스트 시 Dynamic Module의 옵션을 오버라이드하는 패턴이다.
const module = await Test.createTestingModule({
imports: [
// 테스트용 설정으로 오버라이드
StorageModule.forRoot({
bucket: 'test-bucket',
region: 'us-east-1',
}),
],
})
.overrideProvider(StorageService)
.useValue(mockStorageService) // 또는 서비스만 mock
.compile();
MODULE_OPTIONS_TOKEN을 직접 오버라이드하면 실제 서비스 로직은 유지하면서 설정만 교체할 수도 있다. Interceptor 테스트와 함께 활용하면 통합 테스트 효율이 높아진다.
정리
NestJS Dynamic Module은 런타임 설정 주입의 핵심 패턴이다. forRoot/forFeature/register 네이밍 컨벤션을 따르고, forRootAsync로 ConfigService 연동을 지원하며, NestJS 9+에서는 ConfigurableModuleBuilder로 보일러플레이트를 제거할 수 있다. 재사용 가능한 라이브러리 모듈을 만들 때 이 패턴을 마스터하면 NestJS 생태계의 설계 철학을 완전히 이해하게 된다.