이전 포스팅에서 다룬 module은 전부 정적(static) module이다.
그 말인즉슨, 런타임이 아니라 컴파일 시에 모두 provider가 구성된다는 뜻이다.
한 module에서 정의된 provider는 속한 module내의 모든 멤버가 export할 필요 없이 사용(주입)할 수 있다(이걸 'visible'하다고 표현한다).
그렇지만 다른 module에서 그 provider를 주입할 수 있게 하려면 속한 module이 export한 뒤 그 module를 import해야 하는 것이다.
이게 NestJS에서 module로 의존성을 주입하는 기본 방식이다.
이 방식은 정적 모듈 바인딩(static module binding)이라고 한다.
provider를 export하는 module를 Host module,
그 provider를 사용하기 위해 Host module를 import하는 쪽을 Consuming module이라고 하는데
정적 바인딩을 하면 Consuming module 쪽에서 import하려는 module의 provider들을 입맛에 맞게 따로 구성할 방법은 없다.
예를 들어, UserModule를 CartModule이 import한다고 했을 때
CartModule은 오로지 UserModule에서 export하고 있는 provider만 주입이 가능하다. UserModule이 CartModule에 대해 따로 구성하거나 할 방법이 없는 것이다.
Dynamic module
Dynamic module은 말 그대로 동적 모듈, 즉, 컴파일 때 정적으로 구성되는 게 아니라 런타임 때 provider를 동적으로 구성하는 module이다.
Dynamic module은 static module과 속성값(imports, controllers, providers)이 똑같다. 그저 컴파일 때 구성되는지, 런타임에 구성되는지가 다를 뿐이다.
Dynamic module이 필요한 경우는 어떤 경우일까?
공통 기능을 가지는 module (e.g. HttpModule, ConfigModule, JwtModule, TypeOrmModule, ...)이 다른 module 별로 다르게 구성되어야 하면 어떻게 해야 할까?
가령 내부적으로 axios를 사용하는 HttpModule을 예로 들어보자.
HttpModule를 import 하는 여러 module들이 있다고 할 때
만약 Consuming module별로 host url을 달리 해야 한다면 어떻게 해야 할까? 일일이 모든 요청의 url마다 host를 다르게 적어줘야 할까?
그럴 필요 없이 다음과 같이 구성해주면 된다.
// UserModule은 'https://user-example.com' 으로 요청을 보내는 경우
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'
import { UserService } from './user.service';
import { HttpModule } from '@nestjs/axios';
@Module({
imports: [HttpModule.register({
baseURL: 'https://user-example.com'
})],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
// CartModule은 'https://cart-example.com' 으로 요청을 보내는 경우
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'
import { CartService } from './user.service';
import { HttpModule } from '@nestjs/axios';
@Module({
imports: [HttpModule.register({
baseURL: 'https://cart-example.com'
})],
providers: [CartService],
exports: [CartService],
})
export class CartModule {}
밑에서 자세히 설명하겠지만 일단 register() 안에서 Consuming module 마다 baseURL을 다르게 구성해주고 있음을 볼 수 있다.
즉, UserModule과 CartModule 모두 똑같이 HttpModule을 import하고 있지만 자신들의 요구에 맞게 다른 설정을 주고 있는 것이다.
TypeOrmModule 예시 (데이터베이스)
NestJS에서 대표적으로 사용하는 Database ORM인 TypeOrm을 사용한다. 이와 관련된 예를 보자.
우선 애플리케이션이 사용할 데이터베이스 관련 설정을 TypeOrmModule의 forRoot()안에 적어준다.
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController} from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { Cartmodule } from './cart/cart.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
UserModule,
CartModule,
],
controllers: [AppController],
providers: [AppService]
})
export class AppModule {}
위와 같이 forRoot()로 구성된 데이터베이스 설정은 모든 module들이 공유하게 된다.
만약 웹애플리케이션 A와 B가 있고 둘 다 서로 다른 데이터베이스 A, B를 사용한다고 할 때, 각 애플리케이션의 forRoot()안에 들어가는 데이터베이스 설정값은 달라지게 될 것이다. 즉 애플리케이션별로 데이터베이스 모듈을 동적으로 구성해 준 것이다.
가령 UserModule이 User 엔티티에 대한 repository를 사용해야 한다면
UserModule을 다음과 같이 수정해 줄 수 있다.
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
TypeOrmModule.forFeature()안에 entity나 repository를 넣으면, UserModule 범위에 해당 repository가 등록되었다는 의미이다.
register(), forRoot(), forFeature()
@nestjs/로 시작하는 패키지에서
register(), forRoot(), forFeature() 이 세 가지 정적 메소드들을 제공하는 걸 볼 수 있다. 셋 다 DynamicModule 타입을 반환하긴 하나 차이가 있다.
register
module을 import할 때마다 다르게 구성/등록하고 싶을 때,
즉, Consuming module 별로 구성을 다르게 줄 수 있다.
위에서 예시를 든 HttpModule.register()가 그 예이다. HttpModule을 import 할때마다 서로 다른 baseURL을 주고 있는 것이다.
forRoot
동적 모듈을 한 번 구성하고 그 구성을 여러 곳에서 재사용하고 싶을 때. 요컨데 애플리케이션 전반에서 공유될 전역 설정을 할 때 호출된다.
위에서 예시를 든 TypeOrmModule.forRoot()가 그 예이다. 연결하고자 하는 데이터베이스의 url, account, password 등을 설정하고 다른 module에서 다시 그 설정값을 적을 필요 없이 공유될 수 있게끔 해 준 것이다.
forFeature
forRoot()와 헷갈릴 수 있는데 무슨 차이냐면,
기본적으로 forRoot()로 지정한 구성을 사용하되, Consuming module의 요구에 맞게 재구성하는 것이다.
위에서 UserModule이 기본적으로 forRoot()로 설정해준 데이터베이스에 접근하되, 추가적으로 User 엔티티에 대한 repository를 사용하기 위해 TypeOrmModule.forFeature()안에 엔티티를 적어준 게 그 예이다.
비동기 → registerAsync(), forRootAsync(), forFeatureAsync()
위에서 설명한 register(), forRoot(), forFeature() 는 동기적인 방식이다.
비동기적으로 동적 모듈을 구성하기 위해 registerAsync(), forRootAsync(), forFeatureAsync() 가 존재한다.
module의 구성이 하드코딩되어 있다면 그냥 동기 메서드를 쓰면 되고,
운영환경별로 다른 환경변수를 읽어온다던가 외부 파일로부터 설정을 가져온다던가 하는 등의 비동기적 작업이 필요하면 비동기 메서드를 쓰면 된다.
예를 들어, 위에서 TypeOrmModule.forRoot()로 구성한 데이터베이스 설정을 환경변수로부터 읽어 비동기적으로 구성하고 싶을 땐 다음과 같이 쓰면 된다.
import { TypeOrmModule } from '@nestjs/typeorm';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { CartModule } from './cart/cart.module';
@Module({
imports: [
TypeOrmModule.forRootAsync({
useFactory: () => ({
type: 'mysql',
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_DB,
entities: [],
synchronize: false,
}),
}),
UserModule,
CartModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}