NestJS Overview - Modules

Min Su Kwon·2021년 10월 17일
0

모듈은 @Module 데코레이터가 달린 클래스다. @Module 데코레이터는 네스트가 애플리케이션 구조를 정리하는데 사용하는 메타데이터를 제공한다.

각각의 애플리케이션은 적어도 하나의 모듈을 가지고 있다. 이는 루트 모듈이다. 루트 모듈은 네스트가 애플리케이션 그래프(네스트가 모듈과 프로바이더 관계를 resolve하기 위해서 사용하는 내부 자료구조)를 만들기 위해서 사용하는 시작지점이다. 아주 작은 애플리케이션이 이론적으로 루트모듈만 가지고 있지만, 대부분의 경우는 이렇지 않다. 네스트 팀은 컴포넌트 구조를 효율적으로 정리하기 위한 방법으로 모듈을 사용하길 강력하게 권장한다. 따라서, 대부분의 애플리케이션에서는 결과물 아키텍쳐가 여러개의 모듈로 구성되고, 각각의 모듈이 관련된 기능들을 캡슐화하고 있는 구조가 된다.

@Module() 데코레이터는 하나의 객체를 인자로 받으며, 아래와 같은 프로퍼티들을 가진다.

  • providers
    네스트 injector를 통해서 인스턴스화되고, 최소 이 모듈 내에서는 공유될 수 있는 프로바이더들
  • controllers
    이 모듈에서 정의된 컨트롤러들로, 인스턴스화되어야 하는 대상들
  • imports
    이 모듈에서 필요로하는 프로바이더를 export하는 모듈들
  • exports
    providers 배열의 부분집합으로, 이 모듈이 제공하고 이 모듈을 import 하는 다른 모듈들이 사용할 수 있어야하는 프로바이더들

모듈은 프로아비더들을 디폴트로 캡슐화한다. 이는 곧 직접적으로 해당 모듈의 프로바이더가 아니거나, import 된 모듈들로부터 export되지 않았을 경우 주입될 수 없다는 것을 뜻한다. 따라서, export 하는 프로바이더들은 곧 해당 모듈의 퍼블릭 인터페이스 또는 API로 취급할 수 있다.

Feature Modules

CatsControllerCatsService는 모두 같은 애플리케이션 도메인에 위치한다. 둘이 깊게 상관 있기 때문에, 둘을 하나의 기능 모듈로 구분하는 것이 바람직하다. 기능 모듈은 간단하게 해당하는 기능과 연관 있는 코드들을 정리해서, 코드를 정리된 상태로 유지하고 명확한 바운더리를 만든다. 이로인해 복잡성을 관리하는 것과 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 {}

이제 디렉토리 구조는 아래와 같다.

Shared modules

네스트에서, 모듈들은 디폴트로 모두 싱글턴이다. 따라서, 여러개의 모듈에서 별로 힘들이지 않고 프로바이더의 같은 인스턴스를 공유할 수 있다.

모든 모듈은 디폴트로 공유되는 모듈이다. 만들어진 이후에는, 언제 어디서든 다른 모듈에 의해서 사용될 수 있다. 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에 대한 접근 권한을 가지고, 같은 인스턴스를 다른 모듈들과 다같이 공유하게된다.

Module re-exporting

위에서 봤듯이, 모듈들은 자신들이 가지고 있는 provider를 export할 수 있다. 추가로, import한 모듈 또한 그대로 export 할 수 있다. 아래의 예시에서, CommonModuleCoreModule에서 import + export 되었다. 따라서, CoreModule을 import하는 모든 모듈이 CommonModule이 export 하는 것들을 사용할 수 있게된다.

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

Dependnecy injection

모듈 클래스에서도 프로바이더를 주입할 수 있다(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) {}
}

하지만, 모듈 클래스를 프로바이더로 넣을 수는 없는데, 이는 순환 의존성을 초래하기 때문이다.

Global modules

같은 모듈을 모든 곳에 다 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 배열을 사용하는 것이 더 선호되는 방법이다.

Dynamic modules

네스트 모듈 시스템은 동적 모듈이라는 강력한 기능을 제공한다. 이 기능을 통해서 손쉽게 커스터마이즈 할 수 있는 모듈을 만들 수 있으며, 이 모듈을 통해서 동적으로 프로바이더를 등록하고 구성할 수 있다. 동적 모듈은 이 문서에서 상세히 다룬다. 아래는 간단하게 오버뷰를 살펴본다.

아래는 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 메서드에 넘겨지는 entitiesoptions 객체에 따라서 다른 프로바이더들을 노출한다. 동적 모듈이 반환하는 프로퍼티들은 @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에 넣어주고!

동적 모듈을 아직 제대로 써본적이 없다. 이제 좀 이해했으니 유용하게 쓸 수 있을 것 같다. 동일 모듈인데 조금씩 달라지는 경우, 캐시모듈 활용할때 써봐야지

Reference

profile
이제 막 커리어를 시작한 소프트웨어 엔지니어입니다. 배운 것을 정리하면서 조금 더 깊이 이해하려는 습관을 들이려고 합니다. 피드백은 언제나 환영입니다.

0개의 댓글