Node.js/NestJS

[NestJS] 4. 모듈

턴태 2022. 9. 28. 14:19

모듈

모듈은 @Module() 데코레이터로 주석이 달린 클래스입니다. @Module() 데코레이터는 Nest가 어플리케이션 구조를 이용하고 정리하게 해주는 메타데이터를 제공합니다.

각각의 어플리케이션은 최소한 하나의 모듈, 루트 모듈을 가집니다. 루트 모듈은 네스트가 어플리케이션 그래프(Nest가 모듈과 프로바이더 관계와 의존성을 해결하는 데 사용하는 내부 데이터 구조)를 구축하는 데 사용하는 스타팅 포인트입니다.

 

매우 작은 어플리케이션이 이론적으로 단 하나의 루트 모듈을 갖게 되지만, 이는 일반적인 경우는 아닙니다. Nest는 모듈을 컴포넌트를 정리하는 효과적인 방법으로 강력히 추천합니다. 그래서 대부분의 어플리케이션에 대해 결과적인 아키텍쳐는 밀접하게 관련되어 있는 기능들을 캡슐화 해주는 다수의 모듈을 포함합니다.

 

@Module() 데코레이터는 하나의 단일객체를 사용하며 이 객체의 프로퍼티는 다음과 같습니다.

  • providers: Nest로 인스턴스화되며 이 모듈에 최소 한 번은 이어지는 프로바이더들
  • controllers: 해당 모듈에서 인스턴스화 되어야 하는 컨트롤러 집합
  • imports: 이 모듈에서 요구되는 프로바이더를 내보내는 import된 모듈의 리스트
  • exports: 이 모듈에서 제공되며, 다른 모듈에서 import 하여 사용가능한 프로바이더의 부분집합

모듈은 기본적으로 프로바이더들을 캡슐화합니다. 이것은 현재 모듈에 속하지 않거나 임포트된 모듈로부터 노출되지 않는 프로바이더들을 주입하는 것이 불가능하다는 의미입니다. 그래서 모듈에서 노출한 프로바이더를 모듈의 공용 인터페이스나 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 {}

위에서 CatsModule을 cats.module.ts에 정의했으며, 이 모듈과 관련된 모든 것을 cats 디렉터리로 옮겼습니다. 마지막으로 해야하는 것은 해당 모듈을 루트 모듈로 임포트하는 것입니다.

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

공유된 모듈

Nest에서, 모듈은 기본적으로 singletons(싱글톤)입니다. 그래서 여러 모듈 사이에서 모든 프로바이더의 동일한 인스턴스를 편리하게 공유할 수 있습니다.

모든 모듈은 자동적으로 공유된 모듈이 됩니다. 한번 생성되면 어떤 모듈로든 재사용 가능합니다. 다른 다양한 모듈 사이에서 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],
})
export class CatsModule {}

이제 CatsModule을 임포트하는 모든 모듈은 CatsService에 접근할 수 있으며 같은 인스턴스를 공유할 수 있습니다.

 

모듈 재노출

위에서 봤던 것처럼, 모듈은 내부 프로바이더를 노출시킬 수 있습니다. 추가적으로 모듈은 임포트하는 모듈들을 재노출할 수 있습니다. 하단의 예처럼 CommonModule은 CoreModule로부터 임포트와 노출이 되어서, CoreModule을 임포트하는 다른 모듈에서도 CommonModule을 사용할 수 있습니다.

@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],
  // exports: [CatsService],
})
export class CatsModule {
  constructor(private catsService: CatsService) {}
}

그러나 모듈 클래스는 순환 의존성으로 인해 프로바이더로 주입될 수 없습니다.

 

전역 모듈

동일한 모듈을 모든 곳에 임포트해야 한다면 여간 쉬운 일이 아닐 겁니다. Nest는 프로바이더들을 모듈 수준에서 캡슐화하기 때문에 전역 스코프로 등록할 수 없습니다. 캡슐화된 모듈을 불러오지 않으면, 모듈의 프로바이더들을 사용할 수 없습니다.

 

어디에서나 사용할 수 있는 프로바이더들을 제공하고 싶을 때, @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 {}

@Global() 데코레이터는 모듈을 전역 스코프로 만들어줍니다. 전역 모듈은 오직 한 번 등록해야 하며, 일반적으로 루트 혹은 코어 모듈에 의해 등록되어야 합니다. 위의 예처럼, CatsService 프로바이더는 어디에서나 사용할 수 있으며 해당 서비스를 주입하고자 하는 모듈은 CatsModule 을 import할 필요 없이 바로 사용하면 됩니다.

 

동적 모듈

Nest 모듈 시스템은 동적 모듈이라고 불리는 강력한 기능을 갖고 있습니다. 이 기능은 개발자가 프로바이더를 동적으로 등록하거나 설정할 수 있도록 커스터마이징 할 수 있는 모듈을 쉽게 만들 수 있게 해줍니다.

 

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() 메서드는 동적 모듈을 Promise를 사용해 동기 혹은 비동기적으로 반환할 수 있습니다.

위 모듈은 Connection 프로바이더를 기존적으로 정의하지만, 추가적으로 forRoot() 메서드로 전해지는 entities와 options 객체에 의존하여 레포지토리와 같은 프로바이더들을 노출시킵니다. 동적 모듈에 의해 반환된 프로퍼티는 @Module() 데코레이터에서 정의된 기본 모듈의 메타데이터(프로바이더 등)를 덮어쓰는 것이 아니라 확장합니다. 이것이 정적으로 선언된 Connection 프로바이더와 동적으로 생성된 레포지토리 프로바이더들이 모듈로 부터 export 되는 방법입니다.

 

전역으로 동적 모듈을 등록하고 싶다면, global 프로퍼티를 true로 설정해줍니다.

{
  global: true,
  module: DatabaseModule,
  providers: providers,
  exports: providers,
}

DatabaseModule은 아래와 같은 방법으로 설정하여 임포트할 수 있습니다.

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() 메서드를 생략할 수 있습니다.

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],
})
export class AppModule {}