[Nest.js] Module 이란?

정지현·2022년 10월 26일
4

이 글은 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 만 사용할 수 있다는 것으로 보인다.
  • 모듈은 기본적으로 Provider 들을 캡슐화하고 있다. 이 말은 즉, 사용하려고 하는(또는 의존성 주입되는) Provider 가 해당 모듈 안에 직접적으로 속해있지 않거나, 또는 특정 모듈에서 다른 외부 모듈에 속한 Provider 를 필요로 할 때 Export 속성에 의해 지정되지 않은 Provider 는 사용하지 못 한다는 것이다. 따라서, 어떤 Provider 를 Export 한다는 것은 어플리케이션의 다른 모듈에서 사용할 수 있는 Public Interface 또는 API 로 허용한다는 것을 의미한다. (자바로 치자면, 말 그대로 접근제어지시자를 private 에서 public 으로 변경하는 것과 같은 효과인 것 같다.)

🧩 기능 모듈 (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 {}

참고

Nest CLI 에서 모듈을 생성하기 위한 명령어는 $ nest g module cats 이다.


CatsControllerCatsService, Cat InterfaceCat DTO 는 미리 생성되어있다고 가정한다.
Controller 와 Service(Provider 의 하위 분류), Interface 와 Dto 에 대해서 다루는 파트는 Nest 공식 도큐먼트인 ControllerService 를 참고하면 된다.

  • 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 {}

공유 모듈 (Shared Modules)

  • 네스트에서 모듈은 기본적으로 싱글톤 객체이다. 따라서 Provider 를 여러 모듈에서 임포트하여 사용할 수 있다.

  • 모든 모듈은 자동으로 공유 모듈로 취급된다. 모듈 하나가 생성되면, 다른 모듈에서 재사용이 가능하다는 것이다. 예를 들어, CatsService 인스턴스를 다른 모듈에서 공용으로 사용하고 싶다면, 가장 첫 번째로 할 일은 CatsService 를 모듈의 exports 프로퍼티에 배열 형태로 다음과 같이 추가해주면 된다.

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] // CatsService 추가!
})
export class CatsModule {}
  • 이제 CatsModule 을 다른 모듈이 임포트하면, 그 모듈은 CatsService 에 접근할 수 있다. 또한, CatsService 는 다른 모듈에서도 동일한 인스턴스로서 사용될 수 있다.

모듈 Re-export (Module Re-exporting)

  • 상기 예제에서 살펴본 것처럼, 모듈은 내부에 갖고 있는 Provider 를 Export 할 수 있다. 뿐만 아니라, 모듈은 특정 모듈이 Import 한 다른 모듈을 다시 Export 할 수 있다.

  • 하기 예제는 CoreModule 에서 CommonModule 을 Import 하고, 이를 다시 Export 하는 예를 보여준다. 이는 만일 또 다른 모듈이 CoreModule 을 Import 할 때, CoreModule 내에 포함된 다양한 Provider 들을 사용할 수 있다.

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

의존성 주입 (Dependency Injection)

  • 모듈은 Provider 를 주입할 수 있다. (예를 들면, 설정 등의 목적)

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) {}
}
  • 그러나, 모듈 클래스는 순환 참조(Circular Dependency) 등의 이유로 Provider 로 제공될 수 없다.

전역 모듈 (Global Modules)

  • Nest 의 경우, Provider 들을 모듈(A 모듈이라고 하자.) 스코프 안에 캡슐화 시켜놓는 형태이기 때문에, 다른 모듈(B 모듈이라고 하자.)에서 A 모듈의 Provider 를 즉시 사용하지 못 한다. 즉, B 모듈에 A 모듈을 임포트해야한다.

  • 그러나, 어떤 공통 모듈이 있다고 할 때, 이 모듈이 지닌 Provider 를 여러 개의 모듈에서 사용해야한다고 한다면, 각각의 모듈에 공통 모듈을 일일이 Import 하는 것은 그렇게 좋지 않은 방법일 것이다.

  • 만일, Database 커넥션이나, 다양한 헬퍼 기능을 제공하는 Provider 를 전역적으로 사용하고 싶다면, @Global() 데코레이터를 사용하면 된다.

cats.module.ts

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 배열로 사용하는 것이 더 선호된다.

동적 모듈 (Dynamic Modules)

  • 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() 메소드 내에서 주입되는 entitiesoptions 객체에 따라 어떠한 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 {}
  • 만일, 동적 모듈을 Re-export 하고 싶다면, 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 를 활용한 고차원적인 동적 모듈을 생성하는 방법을 알아보기 위해서는 이 챕터를 확인하면 된다.

profile
나를 성장시키는 좌절에 감사하고 즐기려고 노력 중

0개의 댓글