04 [NestJS] Module
모듈(Module),
NestJS에서 모듈은 @Module() 데코레이터가 붙은 클래스다. 이 데코레이터는 NestJS 애플리케이션의 구조를 구성하는 데 필요한 메타데이터를 제공한다.
단순히 말해, 모듈은 애플리케이션의 특정 기능과 관련된 구성 요소들, 즉 컨트롤러, 프로바이더(서비스 등)를 그룹화하는 단위다. 예를 들어 '고양이'와 관련된 기능을 만든다면 CatsController, CatsService 등을 CatsModule이라는 하나의 상자 안에 담아 관리하는 식이다. 이렇게 관련된 코드들을 함께 묶어두니 확실히 코드의 응집도가 올라가고, 기능 단위로 파악하기가 쉬워진다.
// src/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 {}
@Module() 데코레이터의 속성들
모듈의 진짜 힘은 @Module() 데코레이터 안에 들어가는 메타데이터 객체에서 나온다. 이 속성들이 어떻게 상호작용하는지 이해하는 것이 핵심이었다.
providers: 이 모듈 안에서 사용할 프로바이더(서비스 등)의 목록이다. 여기에 등록된 프로바이더는 NestJS의 DI 시스템, 즉 IoC 컨테이너에 의해 관리된다. 중요한 점은, 여기에 등록된 프로바이더는 기본적으로 해당 모듈 내부에서만 직접 주입해서 사용할 수 있다는 것이다. 일종의 비공개 멤버처럼 동작한다.controllers: 이 모듈에 포함될 컨트롤러의 목록이다. NestJS는 여기에 등록된 컨트롤러들을 스캔해서 해당하는 라우팅 정보를 자동으로 설정해 준다.imports: 다른 모듈을 가져와서 사용할 때 쓰는 배열이다. 만약CatsModule에서AuthModule에 있는 특정 서비스가 필요하다면,imports: [AuthModule]와 같이 추가해줘야 한다. 다른 모듈의 기능을 현재 모듈에서 사용하기 위한 '수입' 통로인 셈이다.exports:imports와 쌍을 이루는 개념이다.providers에 등록된 서비스는 기본적으로 해당 모듈의 비공개 멤버라고 했다. 만약 이 서비스를 다른 모듈에서도 사용할 수 있게 하려면, 반드시exports배열에 명시적으로 추가해서 외부로 공개해야 한다. 내 모듈의 특정 기능을 다른 모듈에서 사용할 수 있도록 열어주는 '수출' 통로라고 이해하면 쉽다.
모듈 간의 협력: imports와 exports
처음에는 이 imports, exports 개념이 좀 헷갈렸다. "그냥 가져다 쓰면 안 되나?" 싶었는데, 직접 코드를 짜보니 왜 이런 구조가 필요한지 알 수 있었다. 캡슐화와 명확한 의존 관계 설정 때문이었다.
예를 들어, 모든 모듈에서 공통으로 사용할 ConfigService가 있다고 가정해 보자.
서비스 주입: 이제 CatsService에서 ConfigService를 아무 문제 없이 주입받아 사용할 수 있다.src/cats/cats.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '../config/config.service';
@Injectable()
export class CatsService {
constructor(private readonly configService: ConfigService) {
// 이제 configService 사용 가능!
}
// ...
}
다른 모듈에서 사용(import): 이제 CatsModule에서 ConfigService가 필요하다고 해보자. CatsModule의 imports 배열에 ConfigModule을 추가한다.src/cats/cats.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '../config/config.module'; // import
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
imports: [ConfigModule], // ConfigModule을 가져온다
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
프로바이더 등록 및 공개(export): ConfigModule에 ConfigService를 providers로 등록하고, 다른 모듈에서 쓸 수 있도록 exports에도 추가한다.src/config/config.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({
providers: [ConfigService],
exports: [ConfigService], // 이 부분이 핵심!
})
export class ConfigModule {}
만약 여기서 exports를 빼먹으면, 다른 모듈에서 ConfigModule을 import 해도 ConfigService를 주입받을 수 없어 에러가 발생한다.
공통 모듈 생성: 먼저 ConfigService를 제공할 ConfigModule을 만든다.src/config/config.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class ConfigService {
// ... 환경 변수 관련 로직
}
이 흐름을 겪고 나니 imports와 exports의 역할이 명확하게 이해됐다. 모듈은 단순히 파일을 묶는 것을 넘어, 기능의 경계를 명확히 하고, 모듈 간의 의존성을 명시적으로 관리하는 아주 중요한 역할을 하는 것이었다.
정리하며
NestJS의 모듈 시스템에 대해 정리해보니, 결국 '잘 구조화된 애플리케이션' 이라는 목표를 향하고 있다는 걸 알 수 있었다.
- 모듈은 관련된 컴포넌트를 묶어 응집도를 높인다.
exports를 통해 모듈의 공개 API를 명확히 정의하여 캡슐화를 강화한다.imports를 통해 모듈 간의 의존 관계를 명시적으로 표현한다.
이런 특징들 덕분에 기능이 추가되거나 변경되어도 다른 부분에 미치는 영향을 최소화할 수 있다. 결국 애플리케이션이 커질수록 유지보수성과 확장성을 결정짓는 핵심이 바로 이 모듈 설계에 달려있다는 생각이 든다.