이 글은 Nest.js 공식 도큐먼트 - Module 파트 를 정리한 글입니다.
Module(이하 모듈 또는 Module) 이란, @Module()
데코레이터가 붙어있는 클래스를 의미한다.
@Module()
데코레이터는 Nest가 전체 어플리케이션의 구조를 만들어나가는데 사용하기 위한 메타데이터를 제공한다.
Nest.js 의 프로젝트에는 최소 한 개 이상의 모듈이 존재하며, 이때 최초 프로젝트 생성 시에 만들어지는 최소 한 개의 모듈은 Root Module
이다. (즉, 프로젝트 내에 Root Module 만 존재하는 태초의 상태!)
이 Root Module 은 Nest 가 Application Graph (전체 앱의 형상 정도?) 를 구축하는데 필요한 시작점이다. 또한, Nest 가 Module 과 Provider 간의 관계, 그리고 종속성 관리를 위해 사용하는 내부적인 데이터 구조라고 한다.
단 하나의 Root Module 만 갖는 매우 간단한 형태의 어플리케이션은 보기 힘들 것이다. 실제 배포된 어플리케이션은 다수의 모듈을 가지고 있을 것이며, 각각의 모듈은 밀접하게 관련있는 기능(인증, 게시판 등)들을 캡슐화하는 형태일 것이다.
@Module()
데코레이터는 단일 객체를 인자로 받으며, 인자로서 사용되는 단일 객체는 모듈의 형태를 나타내는 속성들로 구성되어있다. 이때 사용되는 속성들은 다음과 같다.
속성명 | 설명 |
---|---|
providers | 해당 모듈 이외에 공유되는 Provider 들을 정의하거나, 또는 의존성 주입을 위해 인스턴스화 될 Provider 들을 정의하기 위한 속성 |
controllers | 해당 모듈 내에서 정의된 인스턴스화 되어야 할 컨트롤러들의 집합을 정의하기 위한 속성 |
imports | 해당 모듈에서 필요한 외부 Provider 들을 Export 하는 외부 Module 들의 집합을 정의하기 위한 속성 |
exports | 해당 모듈로부터 제공되는 여러 Provider 들을 정의하기 위한 속성. 해당 모듈을 Import 한 다른 모듈들은 해당 모듈에서 제공하는 Provider 들을 사용할 수 있다. 외부 모듈들은 해당 속성을 통해 제공되는 Provider 만 사용할 수 있다는 것으로 보인다. |
예를 들어, 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 {}
참고
Nest CLI 에서 모듈을 생성하기 위한 명령어는
$ nest g module cats
이다.
CatsController
와CatsService
,Cat Interface
와Cat DTO
는 미리 생성되어있다고 가정한다.
Controller 와 Service(Provider 의 하위 분류), Interface 와 Dto 에 대해서 다루는 파트는 Nest 공식 도큐먼트인 Controller 와 Service 를 참고하면 된다.
CatsModule
의 실제 파일명은 Nest CLI 를 통해 자동 생성하였을 경우 cats.module.ts
로 지정되었을 것이다. 그리고 관련 있는 파일(Controller 와 Service 등)들은 cats
디렉토리에 위치할 것이다.
모듈 생성 이후, 그 다음으로 할 것은 해당 모듈을 Root 모듈인 app.module.ts
파일에 정의된 AppModule
에 Import 하는 것이다.
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule {}
CatsService
인스턴스를 다른 모듈에서 공용으로 사용하고 싶다면, 가장 첫 번째로 할 일은 CatsService
를 모듈의 exports
프로퍼티에 배열 형태로 다음과 같이 추가해주면 된다.import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService] // CatsService 추가!
})
export class CatsModule {}
CatsModule
을 다른 모듈이 임포트하면, 그 모듈은 CatsService
에 접근할 수 있다. 또한, CatsService
는 다른 모듈에서도 동일한 인스턴스로서 사용될 수 있다.상기 예제에서 살펴본 것처럼, 모듈은 내부에 갖고 있는 Provider 를 Export 할 수 있다. 뿐만 아니라, 모듈은 특정 모듈이 Import 한 다른 모듈을 다시 Export 할 수 있다.
하기 예제는 CoreModule
에서 CommonModule
을 Import 하고, 이를 다시 Export 하는 예를 보여준다. 이는 만일 또 다른 모듈이 CoreModule
을 Import 할 때, CoreModule
내에 포함된 다양한 Provider 들을 사용할 수 있다.
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
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) {}
}
Nest 의 경우, Provider 들을 모듈(A 모듈이라고 하자.) 스코프 안에 캡슐화 시켜놓는 형태이기 때문에, 다른 모듈(B 모듈이라고 하자.)에서 A 모듈의 Provider 를 즉시 사용하지 못 한다. 즉, B 모듈에 A 모듈을 임포트해야한다.
그러나, 어떤 공통 모듈이 있다고 할 때, 이 모듈이 지닌 Provider 를 여러 개의 모듈에서 사용해야한다고 한다면, 각각의 모듈에 공통 모듈을 일일이 Import 하는 것은 그렇게 좋지 않은 방법일 것이다.
만일, Database 커넥션이나, 다양한 헬퍼 기능을 제공하는 Provider 를 전역적으로 사용하고 싶다면, @Global()
데코레이터를 사용하면 된다.
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Global() // Global 모듈 데코레이터
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
CatsService
Provider 는 다른 모듈에서 CatsModule
을 Import 하지 않아도 전역적으로 사용할 수 있게 되었다.참고
모든 모듈들을 전역 모듈로 만드는 것은 좋은 결정이 아니다. 전역 모듈은 불필요한 보일러플레이트를 방지하기 위해서 사용되는 것이 좋다. 모듈 안에서 다른 모듈을imports
배열로 사용하는 것이 더 선호된다.
Nest 에는 동적 모듈이라고 불리는 강력한 모듈 시스템이 존재한다.
해당 기능은 Provider 를 동적으로 등록하거나, 설정할 수 있는 모듈을 생성할 수 있도록 한다. 동적 모듈에 자세한 사항은 공식 도큐먼트 - 동적 모듈을 참고하자. 하기 예제는 동적 모듈이 무엇인지만 간략하게 알려준다. 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()
메소드는 동기(Sync) 또는 비동기(Async) 방식으로 동적 모듈을 반환할 수 있다.
해당 모듈은 @Module()
데코레이터 내에서 Connection
Provider 를 기본으로 정의하고 있다. 좀 더 살펴보자면, forRoot()
메소드 내에서 주입되는 entities
와 options
객체에 따라 어떠한 Provider 가 생성된다. 이때, 반환되는 DatabaseModule
은 두 개의 Provider 를 가진 상태로 반환된다. 첫 번째 Provider 는 @Module()
데코레이터 내에 포함되어있는 Connection
Provider 이고, 두 번째는 forRoot()
내에서 생성된 Provider 이다.
만일 동적 모듈을 전역 모듈로 생성하고 싶다면, 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 {}
forRoot()
메소드를 다음과 같이 exports
배열에서 생략할 수 있다.import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';
@Module({
imports: [DatabaseModule.forRoot([User])],
exports: [DatabaseModule], // forRoot() 메소드 생략
})
export class AppModule {}
참고
ConfigurableModuleBuilder
를 활용한 고차원적인 동적 모듈을 생성하는 방법을 알아보기 위해서는 이 챕터를 확인하면 된다.