모듈은 @Module
데코레이터가 달린 클래스다. @Module
데코레이터는 네스트가 애플리케이션 구조를 정리하는데 사용하는 메타데이터를 제공한다.
각각의 애플리케이션은 적어도 하나의 모듈을 가지고 있다. 이는 루트 모듈이다. 루트 모듈은 네스트가 애플리케이션 그래프(네스트가 모듈과 프로바이더 관계를 resolve하기 위해서 사용하는 내부 자료구조)를 만들기 위해서 사용하는 시작지점이다. 아주 작은 애플리케이션이 이론적으로 루트모듈만 가지고 있지만, 대부분의 경우는 이렇지 않다. 네스트 팀은 컴포넌트 구조를 효율적으로 정리하기 위한 방법으로 모듈을 사용하길 강력하게 권장한다. 따라서, 대부분의 애플리케이션에서는 결과물 아키텍쳐가 여러개의 모듈로 구성되고, 각각의 모듈이 관련된 기능들을 캡슐화하고 있는 구조가 된다.
@Module()
데코레이터는 하나의 객체를 인자로 받으며, 아래와 같은 프로퍼티들을 가진다.
providers
controllers
imports
exports
providers
배열의 부분집합으로, 이 모듈이 제공하고 이 모듈을 import 하는 다른 모듈들이 사용할 수 있어야하는 프로바이더들모듈은 프로아비더들을 디폴트로 캡슐화한다. 이는 곧 직접적으로 해당 모듈의 프로바이더가 아니거나, import 된 모듈들로부터 export되지 않았을 경우 주입될 수 없다는 것을 뜻한다. 따라서, export 하는 프로바이더들은 곧 해당 모듈의 퍼블릭 인터페이스 또는 API로 취급할 수 있다.
CatsController
와 CatsService
는 모두 같은 애플리케이션 도메인에 위치한다. 둘이 깊게 상관 있기 때문에, 둘을 하나의 기능 모듈로 구분하는 것이 바람직하다. 기능 모듈은 간단하게 해당하는 기능과 연관 있는 코드들을 정리해서, 코드를 정리된 상태로 유지하고 명확한 바운더리를 만든다. 이로인해 복잡성을 관리하는 것과 SOLID 원칙을 지키는 것에 도움을 받을 수 있다. 애플리케이션 규모가 커지면 커질수록 더.
CatsModule
을 만들어보자
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
위의 예시에서, cats.module.ts
파일에 CatsModule
을 정의 한 뒤, 해당 모듈과 관련 있는 것들을 모두 cats
디렉토리에 옮겼다. 마지막으로 해야 할 일은 이 모듈을 루트 모듈에서 import 해오는 것이다.
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule {}
이제 디렉토리 구조는 아래와 같다.
네스트에서, 모듈들은 디폴트로 모두 싱글턴이다. 따라서, 여러개의 모듈에서 별로 힘들이지 않고 프로바이더의 같은 인스턴스를 공유할 수 있다.
모든 모듈은 디폴트로 공유되는 모듈이다. 만들어진 이후에는, 언제 어디서든 다른 모듈에 의해서 사용될 수 있다. CatsService
인스턴스를 여러 모듈 사이에 공유하고 싶다고 생각해보자. 이를 가능케하려면, 먼저 CatsService
프로바이더를 export해야한다.
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
export class CatsModule {}
이제 CatsModule
을 import 하는 모든 모듈이 CatsService
에 대한 접근 권한을 가지고, 같은 인스턴스를 다른 모듈들과 다같이 공유하게된다.
위에서 봤듯이, 모듈들은 자신들이 가지고 있는 provider를 export할 수 있다. 추가로, import한 모듈 또한 그대로 export 할 수 있다. 아래의 예시에서, CommonModule
이 CoreModule
에서 import + export 되었다. 따라서, CoreModule
을 import하는 모든 모듈이 CommonModule
이 export 하는 것들을 사용할 수 있게된다.
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
모듈 클래스에서도 프로바이더를 주입할 수 있다(config 목적 등으로)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
constructor(private catsService: CatsService) {}
}
하지만, 모듈 클래스를 프로바이더로 넣을 수는 없는데, 이는 순환 의존성을 초래하기 때문이다.
같은 모듈을 모든 곳에 다 import 하는 것이 싫증날 수 있다. 네스트와 다르게, Angular 프로바이더들은 글로벌 스코프에 등록된다. 한번 정의되면, 별도 import 없이 아무데서나 사용 가능한 셈이다. 하지만 네스트에서는 프로바이더들이 모듈에 의해서 캡슐화되기 때문에, 캡슐화하고 있는 모듈을 import 하기 전에는 해당 모듈의 프로바이더를 사용할 수 없다.
언제나 사용 가능한 프로바이더들을 정의하려면, @Global()
데코레이터를 사용한다.
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
이 데코레이터는 모듈을 전역 스코프로 바꿔주며, 전역 모듈들은 한번만 등록되어야한다. 일반적으로 루트 또는 코어 모듈이 이를 담당하게 된다. 위의 예시에서는 CatsService
프로바이더가 전역으로 사용가능하게 되고, 더이상 CatsModule
을 import 하지 않고도 CatsService
인스턴스를 사용할 수 있게 된다.
모두 다 전역으로 만드는 것은 좋은 결정이 아니다. 전역 모듈을 통해 보일러플레이트를 줄일 수 있다. 하지만
imports
배열을 사용하는 것이 더 선호되는 방법이다.
네스트 모듈 시스템은 동적 모듈이라는 강력한 기능을 제공한다. 이 기능을 통해서 손쉽게 커스터마이즈 할 수 있는 모듈을 만들 수 있으며, 이 모듈을 통해서 동적으로 프로바이더를 등록하고 구성할 수 있다. 동적 모듈은 이 문서에서 상세히 다룬다. 아래는 간단하게 오버뷰를 살펴본다.
아래는 DatabaseModule
에 대한 동적 모듈의 예시다.
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';
@Module({
providers: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
forRoot
메서드는 동기적으로, 또는 비동기적으로 동적 모듈을 반환할 수 있다.
이 모듈은 Connection
프로바이더를 디폴트로 정의하고 있으며, 추가적으로 forRoot
메서드에 넘겨지는 entities
와 options
객체에 따라서 다른 프로바이더들을 노출한다. 동적 모듈이 반환하는 프로퍼티들은 @Module
데코레이터에 인자로 넘겨진 provider들을 extend하는 형태가 되며, 따라서 Connection
프로바이더 + 동적으로 추가된 프로바이더들이 export되게 된다.
전역 스코프에서 동적 모듈을 등록하고 싶으면, global
프로퍼티를 true
로 설정해준다.
{
global: true,
module: DatabaseModule,
providers: providers,
exports: providers,
}
DatabaseModule
은 다음과 같은 방법으로 import되고 구성될 수 있다.
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';
@Module({
imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}
동적 모듈을 re-export 하고 싶으면, exports 쪽에서는 forRoot
메서드 호출을 생략해도 된다.
mport { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';
@Module({
imports: [DatabaseModule.forRoot([User])],
exports: [DatabaseModule],
})
export class AppModule {}
모듈이 어떻게 굴러가는지 다시 잘 정리하게 된 것 같다. 필요로 하는 다른 모듈들을 imports
배열에 넣어고, 모듈에 포함되는 프로바이더는 providers
배열에 넣어주고, 프로바이더/import 해온 중에 export 할 친구들은 exports
에 넣어주고!
동적 모듈을 아직 제대로 써본적이 없다. 이제 좀 이해했으니 유용하게 쓸 수 있을 것 같다. 동일 모듈인데 조금씩 달라지는 경우, 캐시모듈 활용할때 써봐야지
@nestjs/core
버전 8.0.11