NestJS

5. NestJS 모듈: 애플리케이션 구조화와 모듈간 의존성 관리

오늘의하늘은 2025. 3. 30. 15:24

NestJS에서 모듈(Module)은 애플리케이션의 구조를 효과적으로 조직화하기 위한 핵심 기능입니다. 모듈은 관련된 컨트롤러, 프로바이더, 서비스 등을 하나의 단위로 묶어서 코드의 재사용성과 유지보수성을 향상시키는 역할을 합니다. 이번 글에서는 NestJS 모듈의 개념과 사용법, 그리고 모듈 간 의존성을 효과적으로 관리하는 방법에 대해 알아보겠습니다.

목차

  1. 모듈이란 무엇인가?
  2. 기본 모듈 구조
  3. 모듈 간 의존성 관리
  4. 공유 모듈
  5. 글로벌 모듈
  6. 동적 모듈
  7. 모듈 재구성하기
  8. 순환 의존성 처리
  9. 실전 예제: 멀티 모듈 애플리케이션 구조
  10. 정리

모듈이란 무엇인가?

NestJS에서 모듈은 애플리케이션의 일부를 캡슐화하는 클래스입니다. 이는 관련된 기능들을 하나의 단위로 그룹화하여 더 구조화된 코드 조직을 만들 수 있게 해줍니다. 모든 NestJS 애플리케이션에는 최소한 하나의 모듈, 즉 루트 모듈(AppModule)이 있어야 합니다.

모듈은 다음과 같은 역할을 합니다:

  1. 관련 컴포넌트 그룹화: 관련 기능의 컨트롤러와 프로바이더를 함께 그룹화합니다.
  2. 캡슐화: 모듈 내부의 구성 요소에 대한 접근을 제어합니다.
  3. 의존성 관리: 모듈 간의 의존성 관계를 정의합니다.
  4. 재사용성: 동일한 기능을 여러 애플리케이션에서 재사용할 수 있게 합니다.

기본 모듈 구조

NestJS 모듈은 @Module() 데코레이터로 주석이 달린 클래스입니다. 기본적인 모듈 구조는 다음과 같습니다:

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [],         // 이 모듈이 의존하는 다른 모듈들
  controllers: [UsersController], // 이 모듈에 속한 컨트롤러들
  providers: [UsersService],   // 이 모듈에 속한 프로바이더들
  exports: []          // 이 모듈에서 내보내는 프로바이더들
})
export class UsersModule {}

@Module() 데코레이터는 다음과 같은 속성을 갖습니다:

  • imports: 이 모듈에서 사용할 다른 모듈들의 목록입니다.
  • controllers: 이 모듈에서 정의하고 인스턴스화할 컨트롤러들의 목록입니다.
  • providers: NestJS 인젝터에 의해 인스턴스화되고 이 모듈 내에서 공유될 수 있는 프로바이더들의 목록입니다.
  • exports: 이 모듈에서 제공하고 이 모듈을 가져오는(import) 다른 모듈에서 사용 가능해야 하는 프로바이더들의 하위 집합입니다.

새로운 모듈을 생성하려면 NestJS CLI를 사용할 수 있습니다:

nest generate module users
# 또는 짧게
nest g mo users

이 명령은 src/users/users.module.ts 파일을 생성하고 루트 모듈(AppModule)에 자동으로 가져오기(import)를 추가합니다.

모듈 간 의존성 관리

NestJS 애플리케이션은 일반적으로 여러 모듈로 구성됩니다. 모듈 A가 모듈 B의 기능을 사용해야 한다면, 모듈 A는 모듈 B를 가져와야(import) 합니다. 이것이 모듈 간 의존성입니다.

예를 들어, 사용자(users) 모듈과 인증(auth) 모듈이 있다고 가정해 보겠습니다. 인증 모듈은 사용자 정보를 처리하기 위해 사용자 모듈의 서비스를 사용해야 합니다:

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // UsersService를 다른 모듈에서 사용할 수 있게 내보냄
})
export class UsersModule {}

// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from '../users/users.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';

@Module({
  imports: [UsersModule], // UsersModule을 가져와서 사용
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

이제 AuthService에서 UsersService를 주입받아 사용할 수 있습니다:

// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}

  async validateUser(username: string, password: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

이 예제에서 중요한 점은 다음과 같습니다:

  1. UsersModule은 UsersService를 내보내기(export) 합니다.
  2. AuthModule은 UsersModule을 가져오기(import) 합니다.
  3. 이렇게 하면 AuthService에서 UsersService를 주입받아 사용할 수 있습니다.

공유 모듈

NestJS에서 모듈은 기본적으로 싱글톤이므로, 여러 모듈 간에 동일한 프로바이더 인스턴스를 공유할 수 있습니다. 모듈을 가져오는(import) 모든 모듈은 동일한 프로바이더 인스턴스를 공유합니다.

이를 이용하면 공통 기능을 제공하는 모듈을 만들고, 여러 모듈에서 재사용할 수 있습니다:

// src/common/common.module.ts
import { Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
import { ConfigService } from './config.service';

@Module({
  providers: [LoggerService, ConfigService],
  exports: [LoggerService, ConfigService], // 다른 모듈에서 사용할 수 있게 내보냄
})
export class CommonModule {}

이제 다른 모듈에서 이 CommonModule을 가져와 LoggerService와 ConfigService를 사용할 수 있습니다:

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { CommonModule } from '../common/common.module';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  imports: [CommonModule], // CommonModule 가져오기
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

그리고 UsersService에서 LoggerService와 ConfigService를 주입받아 사용할 수 있습니다:

// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { LoggerService } from '../common/logger.service';
import { ConfigService } from '../common/config.service';

@Injectable()
export class UsersService {
  constructor(
    private readonly loggerService: LoggerService,
    private readonly configService: ConfigService,
  ) {}

  findAll() {
    this.loggerService.log('Finding all users');
    return ['user1', 'user2'];
  }
}

글로벌 모듈

특정 모듈을 애플리케이션 전체에서 사용하고 싶다면, 해당 모듈을 글로벌 모듈로 등록할 수 있습니다. 글로벌 모듈은 모든 모듈에서 가져오지(import) 않고도 사용할 수 있습니다.

// src/common/common.module.ts
import { Module, Global } from '@nestjs/common';
import { LoggerService } from './logger.service';
import { ConfigService } from './config.service';

@Global() // 글로벌 모듈로 선언
@Module({
  providers: [LoggerService, ConfigService],
  exports: [LoggerService, ConfigService],
})
export class CommonModule {}

이제 CommonModule을 루트 모듈(AppModule)에 한 번만 가져오면, 애플리케이션의 모든 모듈에서 LoggerService와 ConfigService를 사용할 수 있습니다:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { CommonModule } from './common/common.module';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [CommonModule, UsersModule, AuthModule],
})
export class AppModule {}

글로벌 모듈은 편리하지만, 모듈 간의 의존성 관계가 명확하지 않아질 수 있으므로 신중하게 사용해야 합니다. 일반적으로 명시적인 모듈 가져오기(import)를 사용하는 것이 더 좋은 설계 방식입니다.

동적 모듈

NestJS는 동적 모듈 시스템을 통해 모듈을 더 유연하게 구성할 수 있는 기능을 제공합니다. 동적 모듈을 사용하면 모듈을 가져올 때 특정 설정을 전달할 수 있어, 같은 모듈을 다양한 방식으로 재사용할 수 있습니다.

예를 들어, 데이터베이스 연결을 위한 동적 모듈을 만들어 보겠습니다:

// src/database/database.module.ts
import { Module, DynamicModule } from '@nestjs/common';
import { DatabaseService } from './database.service';

@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useValue: options,
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    };
  }

  static forFeature(entities: any[]): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'ENTITIES',
          useValue: entities,
        },
      ],
      exports: [],
    };
  }
}

interface DatabaseOptions {
  host: string;
  port: number;
  username: string;
  password: string;
  database: string;
}

이 동적 모듈은 다음과 같이 사용할 수 있습니다:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    DatabaseModule.forRoot({
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'password',
      database: 'nestjs',
    }),
    UsersModule,
  ],
})
export class AppModule {}

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { DatabaseModule } from '../database/database.module';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';

@Module({
  imports: [DatabaseModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

동적 모듈은 다음과 같은 장점이 있습니다:

  1. 재사용성: 같은 모듈을 다양한 설정으로 재사용할 수 있습니다.
  2. 캡슐화: 모듈의 내부 구현을 숨기고, 명확한 인터페이스를 제공합니다.
  3. 유연성: 모듈의 동작을 런타임에 구성할 수 있습니다.

NestJS의 많은 내장 모듈(예: @nestjs/jwt, @nestjs/typeorm)은 이 동적 모듈 패턴을 사용합니다.

모듈 재구성하기

때로는 기존 모듈의 동작을 수정하거나 확장해야 할 수도 있습니다. NestJS는 모듈 재구성을 위한 다양한 방법을 제공합니다.

프로바이더 재정의

모듈을 가져오면서 해당 모듈의 프로바이더를 재정의할 수 있습니다:

@Module({
  imports: [
    // 다른 모듈의 프로바이더를 재정의하려면 imports 배열에 객체를 사용
    ConfigModule.forRoot({
      isGlobal: true, // 글로벌 모듈로 설정
      // ConfigService를 재정의
      providers: [
        {
          provide: ConfigService,
          useClass: CustomConfigService,
        },
      ],
    }),
  ],
})
export class AppModule {}

모듈 확장

기존 모듈의 기능을 확장하여 새로운 모듈을 만들 수도 있습니다:

// src/enhanced-logger/enhanced-logger.module.ts
import { Module } from '@nestjs/common';
import { LoggerModule } from '../logger/logger.module';
import { EnhancedLoggerService } from './enhanced-logger.service';

@Module({
  imports: [LoggerModule],
  providers: [EnhancedLoggerService],
  exports: [EnhancedLoggerService],
})
export class EnhancedLoggerModule {}

EnhancedLoggerService는 LoggerModule에서 제공하는 기본 LoggerService를 확장하여 추가 기능을 제공할 수 있습니다.

순환 의존성 처리

때로는 두 모듈이 서로를 의존하는 상황이 발생할 수 있습니다. 이를 순환 의존성(Circular Dependency)이라고 합니다. NestJS는 이러한 상황을 해결하기 위한 방법을 제공합니다.

모듈 수준에서 순환 의존성을 해결하려면 forwardRef()를 사용합니다:

// src/cats/cats.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { DogsModule } from '../dogs/dogs.module';

@Module({
  imports: [forwardRef(() => DogsModule)],
})
export class CatsModule {}

// src/dogs/dogs.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { CatsModule } from '../cats/cats.module';

@Module({
  imports: [forwardRef(() => CatsModule)],
})
export class DogsModule {}

프로바이더 수준에서도 순환 의존성이 발생할 수 있으며, 이 경우에도 forwardRef()를 사용하여 해결합니다:

// src/cats/cats.service.ts
import { Injectable, forwardRef, Inject } from '@nestjs/common';
import { DogsService } from '../dogs/dogs.service';

@Injectable()
export class CatsService {
  constructor(
    @Inject(forwardRef(() => DogsService))
    private readonly dogsService: DogsService,
  ) {}
}

// src/dogs/dogs.service.ts
import { Injectable, forwardRef, Inject } from '@nestjs/common';
import { CatsService } from '../cats/cats.service';

@Injectable()
export class DogsService {
  constructor(
    @Inject(forwardRef(() => CatsService))
    private readonly catsService: CatsService,
  ) {}
}

순환 의존성은 가능한 한 피하는 것이 좋습니다. 대신, 두 모듈이 의존하는 공통 기능을 별도의 모듈로 추출하는 것을 고려해 보세요.

실전 예제: 멀티 모듈 애플리케이션 구조

이제 지금까지 배운 내용을 종합하여 멀티 모듈 애플리케이션의 구조를 설계해 보겠습니다. 이 예제는 블로그 애플리케이션으로, 사용자, 게시물, 댓글 기능을 각각 별도의 모듈로 구현합니다.

먼저 각 모듈의 기본 구조를 정의합니다:

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

// src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule], // 사용자 정보를 조회하기 위해 UsersModule 가져오기
  controllers: [PostsController],
  providers: [PostsService],
  exports: [PostsService],
})
export class PostsModule {}

// src/comments/comments.module.ts
import { Module } from '@nestjs/common';
import { CommentsController } from './comments.controller';
import { CommentsService } from './comments.service';
import { UsersModule } from '../users/users.module';
import { PostsModule } from '../posts/posts.module';

@Module({
  imports: [UsersModule, PostsModule], // 사용자 및 게시물 정보를 조회하기 위해 가져오기
  controllers: [CommentsController],
  providers: [CommentsService],
})
export class CommentsModule {}

그리고 데이터베이스 연결을 위한 공통 모듈을 만듭니다:

// src/database/database.module.ts
import { Module, Global } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Global() // 앱 전체에서 사용할 수 있도록 글로벌로 설정
@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get('DB_HOST', 'localhost'),
        port: configService.get('DB_PORT', 5432),
        username: configService.get('DB_USERNAME', 'postgres'),
        password: configService.get('DB_PASSWORD', 'password'),
        database: configService.get('DB_NAME', 'blog'),
        entities: [__dirname + '/../**/*.entity{.ts,.js}'],
        synchronize: configService.get('DB_SYNC', false),
      }),
    }),
  ],
})
export class DatabaseModule {}

또한 인증 기능을 위한 모듈도 필요합니다:

// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';

@Module({
  imports: [
    UsersModule,
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        secret: configService.get('JWT_SECRET'),
        signOptions: { expiresIn: '1h' },
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy, LocalStrategy],
  exports: [AuthService],
})
export class AuthModule {}

마지막으로, 모든 모듈을 루트 모듈에 등록합니다:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';
import { CommentsModule } from './comments/comments.module';
import { AuthModule } from './auth/auth.module';

@Module({
  imports: [
    // 환경 변수 로드
    ConfigModule.forRoot({
      isGlobal: true, // 앱 전체에서 사용할 수 있도록 글로벌로 설정
    }),
    DatabaseModule,
    UsersModule,
    PostsModule,
    CommentsModule,
    AuthModule,
  ],
})
export class AppModule {}

이 구조는 다음과 같은 특징을 갖습니다:

  1. 관심사 분리: 각 기능은 독립적인 모듈로 분리되어 있습니다.
  2. 의존성 관리: 모듈 간의 의존성이 명확하게 정의되어 있습니다.
  3. 재사용성: 공통 기능(DatabaseModule, ConfigModule)은 재사용 가능한 모듈로 추출되어 있습니다.
  4. 확장성: 새로운 기능을 추가하기 위해 새 모듈을 만들고 필요한 의존성을 주입할 수 있습니다.

정리

이번 글에서는 NestJS의 모듈 시스템에 대해 알아보았습니다. 모듈은 애플리케이션의 구조를 효과적으로 조직화하고, 관련 기능을 하나의 단위로 그룹화하는 데 도움이 됩니다. 다음은 NestJS 모듈 시스템의 주요 개념입니다:

  1. 기본 모듈 구조: 모듈은 @Module() 데코레이터로 주석이 달린 클래스로, 컨트롤러, 프로바이더, 가져올 모듈, 내보낼 프로바이더 등을 정의합니다.
  2. 모듈 간 의존성 관리: 모듈은 다른 모듈을 가져와서(import) 해당 모듈의 기능을 사용할 수 있습니다.
  3. 공유 모듈: 여러 모듈에서 공통으로 사용하는 기능을 제공하는 모듈입니다.
  4. 글로벌 모듈: 애플리케이션 전체에서 사용할 수 있는 모듈입니다.
  5. 동적 모듈: 런타임에 구성할 수 있는 모듈로, 같은 모듈을 다양한 설정으로 재사용할 수 있습니다.
  6. 순환 의존성 처리: forwardRef()를 사용하여 순환 의존성 문제를 해결할 수 있습니다.

효과적인 모듈 설계는 다음과 같은 원칙을 따르는 것이 좋습니다:

  • 단일 책임 원칙(SRP): 각 모듈은 하나의 명확한 책임을 가져야 합니다.
  • 관심사 분리: 기능별로 모듈을 분리하여 코드의 가독성과 유지보수성을 향상시킵니다.
  • 내부 캡슐화: 모듈 내부의 구현 세부 사항을 숨기고, 명확한 인터페이스만 노출합니다.
  • 명시적 의존성: 모듈 간의 의존성을 명확하게 정의하여 코드의 예측 가능성을 높입니다.

NestJS의 모듈 시스템을 활용하면 확장 가능하고 유지보수하기 쉬운 애플리케이션을 구축할 수 있습니다. 이는 특히 대규모 프로젝트에서 중요한 이점을 제공합니다.

다음 글에서는 NestJS의 미들웨어(Middleware)에 대해 알아보고, 요청 처리 파이프라인을 어떻게 구성하고 활용하는지 살펴보겠습니다.


 

반응형