모듈

모듈은 @Module() 데코레이터가 달린 클래스 입니다. @Module() 데코레이터는 Nest가 애플리케이션 구조를 만들때 사용할 수 있는 메타데이터를 제공해주는 역할을 하지요.

Nest Module Picture1

각 응용 프로그램에는 적어도 하나 이상의 루트 모듈이 있습니다. 루트 모듈은 마치 Javapublic static void main(String[] args) 메서드처럼 Nest가 애플리케이션 그래프를 구성하기 위해 사용하는 시작점입니다. 여기서 말하는 애플리케이션 그래프는 Nest가 모듈과 프로바이더 간의 관계 및 종속성을 연결하기 위해 사용하는 내부 데이터 구조입니다. 매우 작은 응용 프로그램에는 루트 모듈 단 하나만 사용할 수 있습니다. 하지만 이는 그저 연습용이지 일반적인 경우는 아닙니다. 어플리케이션이 커지면 컴포넌트를 분리해야하고, 컴포넌트를 구성하는 효과적인 방법으로 여러 개의 모듈을 사용하는 걸 피할 수 없습니다. 따라서 대부분의 애플리케이션에서는 각각 밀접하게 관련된 기능을 묶어서 캡슐화하는 모듈을 여러개 사용합니다.

@Module() 데코레이터는 아래 속성을 가지는 객체가 필요합니다. 이 객체는 모듈을 구성하는데 필요한 정보를 가지고 있습니다.

  • providers(프로바이더): Nest 인젝터(Injector: 의존성을 주입하는 Nest 내부 모듈)가 인스턴스화시키고 적어도 이 모듈 안에서 공유하는 프로바이더.
  • controllers(컨트롤러): 이 모듈안에서 정의된, 인스턴스화 되어야하는 컨트롤러의 집합
  • imports: 해당 모듈에서 필요한 모듈의 집합. 여기에 들어가는 모듈은 프로바이더를 노출하는 모듈입니다.
  • exports: 해당 모듈에서 제공하는 프로바이더의 부분집합이며, 이 모듈을 가져오는 다른 모듈에서 사용할 수 있도록 노출할 프로바이더

노출과 내보내기
둘 다 영어 단어로 export 입니다. 직역하면 내보낸다지만 이 단어보다는 노출이라는 단어가 적절하다고 여겨서 노출이라고 했습니다. 내보낸다고 실제로 나가지는 않으니까요.

모듈은 기본적으로 프로바이더를 캡슐화합니다. 즉, 현재 모듈에 직접 속하지 않거나 가져온 모듈에서 노출(export)하지 않는 프로바이더를 주입할 수 없습니다. 따라서 모듈에서 노출한 프로바이더를 모듈의 공용 인터페이스 또는 API로 간주할 수 있습니다.

기능 모듈(Feature modules)

CatsControllerCatsService는 둘 다 고양이를 다루니 같은 도메인에 속합니다. 밀접하게 관련되어 있으므로 기능 모듈로 이동하는 것이 좋습니다. 기능 모듈은 특정 기능과 관련된 코드를 구성하여 코드를 체계적으로 유지하고 명확한 경계를 설정합니다. 이는 특히 애플리케이션이나 팀의 규모가 커짐에 따라 커지는 복잡성을 관리하는데 도움을 주고 SOLID 원칙으로 개발하는데 도움이 됩니다.

이를 증명하기 위해 CatsModule을 만들어보겠습니다.

// cats/cats.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

힌트
CLI를 사용하여 모듈을 생성하려면 $ nest g module cats 명령을 실행하세요.

위에서 우리 cats.module.ts파일의 CatsModule을 정의했습니다. 그리고 이 모듈과 관련된 모든 것을 cats 디렉토리로 옮겼습니다. 마지막으로 해야 할 일은 이 모듈을 루트 모듈(app.module.ts파일에 정의된 AppModule)로 가져오는 것입니다.

// app.module.ts

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

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

여태까지 한 작업의 디렉토리 구조는 다음과 같습니다.

  • src
    • cat
      • dto
        • create-cat.dto.ts
      • interface
        • cat.interface.ts
      • cats.controller.ts
      • cats.module.ts
      • cats.service.ts
    • app.module.ts
    • main.ts

공유 모듈(Shared modules)

Nest에서 모듈은 기본적으로 싱글톤(singleton)입니다. 이는 Nest 고유의 특성이 아니라 Node의 특성이죠. 이런 특성 때문에 Nest에서는 여러 모듈간에 쉽게 공급자의 동일한 인스턴스를 공유 할 수 있습니다.

이미지2

모든 모듈은 자동으로 공유 모듈 입니다. 즉 공유가 가능한 모듈이라는 뜻이며, 일단 생성되면 모든 모듈에서 재사용 할 수 있습니다. 다른 여러 모듈간에 CatsService의 인스턴스를 공유하는 상황을 가정해 보겠습니다. 그렇게 하려면 먼저 아래와 같이 모듈의 exports 배열에 CatsService 프로바이더를 추가하여 프로바이더를 노출해야 합니다.

//cats.module.ts

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에 접근할 수 있으며, 이 모듈을 가져오는 다른 모듈과 동일한 인스턴스를 공유합니다.

모듈 다시 내보내기

위에서 볼 수 있듯이 모듈은 모듈 내부의 프로바이더를 노출할 수 있습니다. 또한 가져온 모듈을 다시 내보낼 수 있습니다. 아래 예를 볼까요? CommonModule을 가져와서 이를 바로 노출합니다. 그럼 CoreModule 모듈을 가져오는 다른 모듈에서는 CommonModule을 사용할 수 있게되죠.

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

의존성 주입

모듈 클래스도 프로바이더를 주입할 수 있습니다. 예를 들어 설정관련된 목적으로요.

// cats.module.ts

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)

모든 곳에서 동일한 모듈을 가져오는 일은 지루한 일이죠. 별도의 절차 없이 어플리케이션 어디서나 사용할 수 있게 하면 좋겠습니다. 이럴 때 전역적으로(즉, 앱 전체적으로) 적용할 수 있는 수단이 필요합니다. Nest가 Angular로부터 영감을 받아서 만들어졌다는 사실은 유명합니다. 하지만 Angular와는 달리 Nest는 전역적으로 프로바이더를 등록할 수 없습니다. 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 프로바이더는 어디서나 사용할 수 있으며, CatsService 서비스를 주입하려는 모듈은 CatsModule을 모듈의 import 배열에 추가할 필요가 없습니다.

힌트
모든 모듈을 전역으로 만드는 것은 좋은 디자인이 아닙니다. 어떤 기능을 완성하기 위한 보일러플레이트 코드가 많다면 이를 줄이기 위해 전역 모듈을 사용하면 좋습니다. 모듈의 imports 배열에 넣는게 일반적으로 선호하는 방법입니다.

동적 모듈

Nest 모듈 시스템에는 동적 모듈 이라 불리는 강력한 기능이 포함되어 있습니다. 이 기능을 사용하면 프로바이더를 동적으로 등록하고 구성할 수 있는 커스터마이징 모듈을 쉽게 만들 수 있습니다. 여기서는 간략히 언급하고 나중에 별도의 장으로 자세히 설명하도록 하겠습니다. (사실 좀 어렵습니다만, 여기서 자세히 적자면 흐름이 좀 깨집니다)

다음은 데이터베이스 기능을 다루는 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() 메서드는 Promise를 통해 동적 모듈을 동기식 또는 비동기식으로 반환할 수 있습니다.

이 모듈은 기본적으로 Connection 프로바이더를 정의하지만(@Module() 데코레이터 메타데이터), forRoot() 메서드에 전달된 entitiesoptions 객체에 따라 레파지토리와 같은 여러 프로바이더를 노출합니다. 동적 모듈이 반환하는 속성은 @Module() 데코레이터에 정의된 모듈의 기본적인 메타데이터를 재정의(override)하지 않고 확장합니다. 이렇게하면 정적으로 선언된 연결 제공자와 동적으로 생성된 저장소 제공자가 모두 모듈에서 내보내집니다.

전역 범위에 동적 모듈을 등록하려면 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 {}

다시 동적 모듈을 다시 내보내려면 exports 배열에서 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 {}

동적 모듈에 대한 맛보기 설명이었는데, 사실 이 부분이 조금 어렵습니다. 나중에 기회될 때 동적모듈에 대해서 더 자세히 적도록 하겠습니다.