NestJS에서 @Module() 데코레이터가 붙은 클래스는 하나의 모듈 단위를 의미한다. Nest는 이 @Module() 데코레이터에 포함된 메타데이터를 활용하여 애플리케이션 구조를 구성하고 관리한다.
기본 구조
모든 Nest 애플리케이션은 최소 하나의 모듈 (AppModule)을 가지고 있으며, Nest는 이 루트 모듈을 기준으로 내부적으로 의존성 그래프 (Dependency Graph) 를 만들어 컴포넌트 간의 관계를 관리한다.
@Module() 데코레이터의 속성
@Module({
providers: [CatsService],
controllers: [CatsController],
imports: [OtherModule],
exports: [CatsService],
})
- providers: Nest의 DI 컨테이너가 인스턴스화할 서비스 목록 (예: CatsService)
- controllers: 라우팅 역할을 하는 컨트롤러 목록 (예: CatsController)
- imports: 이 모듈에서 사용하려는 다른 모듈들
- exports: 이 모듈을 가져가는 다른 모듈이 사용할 수 있도록 공개하는 provider
기본적으로 모듈 내부의 provider는
해당 모듈에서만 접근 가능
Feature Module
기능 단위로 모듈을 나누는 구조이다. 예: CatsController, CatsService는 고양이 관련 기능을 담당하므로 CatsModule에 묶는다.
// cats.module.ts
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
루트 모듈에 아래처럼 추가한다:
// app.module.ts
@Module({
imports: [CatsModule],
})
export class AppModule {}
이런 식으로 기능별로 디렉토리와 모듈을 구성해 코드를 조직화한다.
Shared Module
Nest의 모듈은 기본적으로 singleton이다. 즉, 하나의 인스턴스를 여러 모듈에서 공유할 수 있다.
공유하고 싶은 provider를 exports 에 추가해야 함:
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService], // 공유!
})
export class CatsModule {}
이제 CatsModule을 다른 모듈이 imports로 가져오기만 하면, 그 모듈에서 CatsService를 사용할 수 있고 같은 인스턴스를 공유한다.
만약 각 모듈에서 CatsService를 개별 등록하면, 서로 다른 인스턴스를 사용하게 되어 상태 불일치 및 메모리 낭비가 발생할 수 있다.
Module Re-exporting
다른 모듈을 가져오는 것뿐 아니라, 가져온 모듈을 다시 export할 수 있다:
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
CoreModule을 가져오는 모듈은 CommonModule의 provider까지 함께 사용할 수 있게 된다.
모듈 클래스에 provider 주입하기
모듈 클래스는 constructor를 통해 provider를 주입받을 수 있지만, 일반적으로 사용되지는 않는다:
export class CatsModule {
constructor(private catsService: CatsService) {}
}
참고:
모듈 클래스 자체는 provider로 주입할 수 없다.
Global Module
어디서든 provider를 사용할 수 있도록 글로벌 범위로 등록하는 방법이다.
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
이제 다른 모듈에서 CatsService를 사용하려면 CatsModule을 imports에 명시하지 않아도 된다.
하지만
모든 모듈을 글로벌로 만드는 것은 좋은 설계가 아니다
Dynamic Module
Dynamic Module은 런타임에서 설정을 받아 모듈을 구성할 수 있도록 해주는 유연한 모듈이다.
구조 예시:
@Module({
providers: [Connection],
exports: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
- forRoot() 메서드가 설정을 받고 동적으로 provider를 생성해 반환한다.
- Connection은 정적으로 선언되어 있고,
- 추가적으로 forRoot를 통해 받은 설정으로 동적 provider를 생성한다.
사용 시:
@Module({
imports: [DatabaseModule.forRoot([UserEntity])],
})
export class AppModule {}
이렇게 하면 UserEntity 기반의 설정이 적용된 DatabaseModule이 앱 전체에 적용된다.
동적 모듈을 글로벌로 만들기
return {
global: true,
module: DatabaseModule,
providers: providers,
exports: providers,
}
이렇게 하면 모든 모듈에서 자동으로 이 provider들을 사용할 수 있다.
re-export
@Module({
imports: [DatabaseModule.forRoot([User])],
exports: [DatabaseModule], // forRoot()는 호출하지 않고 모듈 자체를 export
})
export class AppModule {}