Node
Nest.js
Module system

Dependency Injection

*.module.ts에서 관리함

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Module 데코레이터 속성

NestJS의 @Module() 데코레이터는 모듈을 정의하는 메타데이터 객체를 받으며, 다음과 같은 속성들을 가집니다. NestJS 공식 문서 (opens in a new tab)

NestJS 공식 문서에서 "The @Module() decorator provides metadata that Nest makes use of to organize the application structure" 라고 명시되어 있습니다.

providers

  • 의미: 이 모듈에서 인스턴스화되고 관리될 프로바이더(주로 서비스)들의 목록입니다.
  • 동작: NestJS의 IoC 컨테이너에 의해 인스턴스가 생성되며, 기본적으로 이 모듈 내에서만 공유됩니다.
  • 사용 시점: 비즈니스 로직을 담당하는 서비스 클래스를 등록할 때 사용합니다.
users.module.ts
@Module({
  providers: [UsersService, UsersRepository],
  controllers: [UsersController]
})
export class UsersModule {}

DigitalOcean - Dependency Injection in NestJS (opens in a new tab)에서 "Providers are the fundamental building blocks in NestJS that encapsulate the application's business logic" 라고 설명합니다.

controllers

  • 의미: 이 모듈에서 정의될 컨트롤러들의 목록입니다.
  • 동작: HTTP 요청을 처리하는 엔트리포인트 역할을 하며, NestJS가 자동으로 라우팅을 설정합니다.
  • 사용 시점: REST API 엔드포인트를 정의할 때 사용합니다.
users.module.ts
@Module({
  controllers: [UsersController],
  providers: [UsersService]
})
export class UsersModule {}

imports

  • 의미: 이 모듈이 의존하는 다른 모듈들의 목록입니다.
  • 동작: 다른 모듈에서 export한 프로바이더들을 이 모듈에서 사용할 수 있게 됩니다.
  • 사용 시점: 다른 모듈의 서비스를 사용해야 할 때 해당 모듈을 import합니다.
auth.module.ts
@Module({
  imports: [UsersModule],  // UsersModule의 export된 프로바이더 사용 가능
  providers: [AuthService],
  controllers: [AuthController]
})
export class AuthModule {}

Medium - Understanding NestJS Modules (opens in a new tab)에서 "When you import modules containing providers, NestJS instantiates these providers and stores them in its built-in DI container" 라고 설명합니다.

exports

  • 의미: 이 모듈에서 다른 모듈에 제공할 프로바이더들의 목록입니다.
  • 동작: 기본적으로 프로바이더는 모듈 내부에서만 사용 가능하지만, exports에 추가하면 다른 모듈에서도 사용할 수 있습니다.
  • 사용 시점: 이 모듈의 서비스를 다른 모듈에서도 사용하도록 하고 싶을 때 사용합니다.
users.module.ts
@Module({
  providers: [UsersService],
  exports: [UsersService]  // 다른 모듈에서 UsersService 사용 가능
})
export class UsersModule {}

"Export the provider, import the module" - 이것이 NestJS의 모듈 간 의존성 관리 패턴입니다. NestJS 공식 문서 (opens in a new tab)

Module System 동작 원리

NestJS의 Module System은 IoC(Inversion of Control) 컨테이너와 **의존성 주입(Dependency Injection)**을 기반으로 동작합니다. DigitalOcean - Dependency Injection in NestJS (opens in a new tab)

"NestJS uses the IoC container for dependency injection and managing dependencies" 라고 공식 문서에 명시되어 있습니다.

애플리케이션 시작 과정

Medium - NestJS Modules & Dependency Injection (opens in a new tab)에서 "From the main module, Nest will know all of the related modules that you have imported, and then create the application tree to manage all of the Dependency Injections" 라고 설명합니다.

의존성 주입 흐름

DEV Community - Understanding Providers and Dependency Injection (opens in a new tab)에서 "NestJS uses reflect-metadata library to define the metadata for the service class so that it can be managed by the NestJS DI system" 라고 설명합니다.

핵심 동작 원리

  1. 메타데이터 기반 분석: NestJS는 reflect-metadata 라이브러리를 사용하여 데코레이터의 메타데이터를 저장하고 런타임에 읽습니다.

  2. 싱글톤 패턴: 기본적으로 모든 프로바이더는 싱글톤으로 관리되어, 애플리케이션 전체에서 하나의 인스턴스만 생성됩니다.

  3. 모듈 캡슐화: 프로바이더는 기본적으로 해당 모듈 내에서만 사용 가능하며, exports를 통해서만 다른 모듈에 노출됩니다.

GeeksforGeeks - Dependency Injection in NestJS (opens in a new tab)에서 "NestJS initializes providers and resolves their dependencies during the startup phase of your application" 라고 명시합니다.

실제 사용 예시

다음은 실제 애플리케이션에서 모듈 시스템이 어떻게 동작하는지 보여주는 예시입니다.

users/users.service.ts
@Injectable()
export class UsersService {
  findOne(id: string) {
    // 사용자 조회 로직
    return { id, name: 'John Doe' };
  }
}
users/users.module.ts
@Module({
  providers: [UsersService],
  exports: [UsersService]  // 다른 모듈에서 사용 가능하도록 export
})
export class UsersModule {}
auth/auth.service.ts
@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService  // UsersModule에서 export한 서비스 주입
  ) {}
 
  async validateUser(id: string) {
    const user = this.usersService.findOne(id);
    // 인증 로직
    return user;
  }
}
auth/auth.module.ts
@Module({
  imports: [UsersModule],  // UsersService를 사용하기 위해 import
  providers: [AuthService],
  controllers: [AuthController],
  exports: [AuthService]  // 다른 모듈에서도 사용 가능
})
export class AuthModule {}
app.module.ts
@Module({
  imports: [
    UsersModule,
    AuthModule
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}

동작 흐름 설명

  1. AppModule 로드: 애플리케이션 시작 시 AppModule이 로드되며, imports 배열의 UsersModuleAuthModule을 로드합니다.

  2. UsersModule 초기화: UsersService가 IoC 컨테이너에 등록되고 인스턴스가 생성됩니다. exports에 포함되어 있어 다른 모듈에서 사용 가능합니다.

  3. AuthModule 초기화: UsersModule을 import했으므로, AuthService의 constructor에서 UsersService를 주입받을 수 있습니다.

  4. 의존성 해결: IoC 컨테이너가 AuthService 생성 시 필요한 UsersService 인스턴스를 자동으로 주입합니다.

Stack Overflow - Inject service from another module (opens in a new tab)에서 "The module encapsulates providers by default. It's impossible to inject providers that are neither directly part of the current module nor exported from the imported modules" 라고 설명합니다.

주의사항

순환 의존성

모듈 간 순환 의존성이 발생하면 NestJS는 에러를 발생시킵니다. 이를 해결하려면 forwardRef()를 사용해야 합니다.

@Module({
  imports: [forwardRef(() => AuthModule)]
})
export class UsersModule {}

Global Modules

@Global() 데코레이터를 사용하면 모든 모듈에서 import 없이 사용할 수 있지만, 의존성 파악이 어려워지므로 신중하게 사용해야 합니다.

@Global()
@Module({
  providers: [ConfigService],
  exports: [ConfigService]
})
export class ConfigModule {}

하지만 과도한 Global Module 사용은 의존성 관리를 어렵게 만들므로, 꼭 필요한 경우에만 사용하는 것이 좋습니다.