NestJS Module Federation 심화

NestJS Module Federation이란?

Module Federation은 Webpack 5에서 도입된 런타임 모듈 공유 메커니즘으로, NestJS 백엔드에서도 마이크로서비스 간 코드를 런타임에 동적으로 공유할 수 있습니다. 기존 NestJS 마이크로서비스가 TCP/gRPC/Kafka로 메시지를 교환했다면, Module Federation은 모듈 자체를 원격으로 로드하여 네트워크 오버헤드 없이 코드를 재사용합니다.

기존 마이크로서비스 통신과 비교

비교 항목 TCP/gRPC Kafka 이벤트 Module Federation
통신 방식 네트워크 RPC 비동기 메시지 런타임 모듈 로드
레이턴시 네트워크 왕복 큐 지연 초기 로드 후 제로
타입 공유 Proto/DTO 복제 스키마 레지스트리 타입 직접 공유
독립 배포 ✅ (버전 관리 필요)
적합한 상황 서비스 간 요청-응답 이벤트 드리븐 공유 로직·유틸 재사용

@module-federation/node 설정

NestJS에서 Module Federation을 사용하려면 @module-federation/node 패키지와 Webpack 설정이 필요합니다.

// 설치
npm install @module-federation/node @module-federation/utilities
npm install -D webpack webpack-cli

// 프로젝트 구조
├── apps/
│   ├── host-api/          # Host 서비스 (소비자)
│   │   ├── webpack.config.js
│   │   └── src/
│   ├── auth-remote/       # Remote 서비스 (제공자)
│   │   ├── webpack.config.js
│   │   └── src/
│   └── shared-remote/     # 공유 유틸리티 Remote
│       ├── webpack.config.js
│       └── src/
└── packages/
    └── shared-types/      # 공유 타입 정의

Remote 서비스 설정: 모듈 노출

Remote는 다른 서비스에 모듈을 제공하는 측입니다. auth-remote 서비스가 인증 관련 모듈을 노출하는 예시입니다.

// apps/auth-remote/webpack.config.js
const { NodeFederationPlugin, StreamingTargetPlugin } = 
  require('@module-federation/node');

module.exports = {
  target: 'async-node',
  output: {
    publicPath: 'http://localhost:3001/',
    scriptType: 'text/javascript',
  },
  plugins: [
    new NodeFederationPlugin({
      name: 'auth_remote',
      filename: 'remoteEntry.js',
      library: { type: 'commonjs-module' },
      exposes: {
        './AuthModule': './src/auth/auth.module',
        './JwtStrategy': './src/auth/strategies/jwt.strategy',
        './AuthGuard': './src/auth/guards/auth.guard',
        './UserDto': './src/auth/dto/user.dto',
      },
      shared: {
        '@nestjs/common': { singleton: true, eager: true },
        '@nestjs/core': { singleton: true, eager: true },
        rxjs: { singleton: true },
        'class-validator': { singleton: true },
        'class-transformer': { singleton: true },
      },
    }),
    new StreamingTargetPlugin({
      name: 'auth_remote',
      library: { type: 'commonjs-module' },
      remotes: {},
    }),
  ],
};
// apps/auth-remote/src/auth/auth.module.ts
@Module({
  imports: [JwtModule.registerAsync({
    useFactory: (config: ConfigService) => ({
      secret: config.get('JWT_SECRET'),
      signOptions: { expiresIn: '30m' },
    }),
    inject: [ConfigService],
  })],
  providers: [AuthService, JwtStrategy, AuthGuard],
  exports: [AuthService, AuthGuard, JwtStrategy],
})
export class AuthModule {}

// 노출되는 Guard
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly authService: AuthService,
    private readonly reflector: Reflector,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(
      IS_PUBLIC_KEY, [context.getHandler(), context.getClass()]);
    if (isPublic) return true;

    const request = context.switchToHttp().getRequest();
    const token = this.extractToken(request);
    const payload = await this.authService.verifyToken(token);
    request.user = payload;
    return true;
  }
}

Host 서비스 설정: 원격 모듈 소비

Host는 Remote에서 노출한 모듈을 런타임에 로드하여 사용합니다.

// apps/host-api/webpack.config.js
const { NodeFederationPlugin, StreamingTargetPlugin } = 
  require('@module-federation/node');

module.exports = {
  target: 'async-node',
  output: {
    publicPath: 'http://localhost:3000/',
    scriptType: 'text/javascript',
  },
  plugins: [
    new NodeFederationPlugin({
      name: 'host_api',
      filename: 'remoteEntry.js',
      library: { type: 'commonjs-module' },
      remotes: {
        auth_remote: 'auth_remote@http://localhost:3001/remoteEntry.js',
        shared_remote: 'shared_remote@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        '@nestjs/common': { singleton: true, eager: true },
        '@nestjs/core': { singleton: true, eager: true },
        rxjs: { singleton: true },
      },
    }),
    new StreamingTargetPlugin({
      name: 'host_api',
      library: { type: 'commonjs-module' },
      remotes: {
        auth_remote: 'auth_remote@http://localhost:3001/remoteEntry.js',
      },
    }),
  ],
};
// apps/host-api/src/app.module.ts
// 동적 import로 Remote 모듈 로드
const loadAuthModule = async () => {
  const { AuthModule } = await import('auth_remote/AuthModule');
  return AuthModule;
};

const loadAuthGuard = async () => {
  const { AuthGuard } = await import('auth_remote/AuthGuard');
  return AuthGuard;
};

@Module({
  imports: [
    // Remote 모듈을 동적으로 import
    LazyModuleLoader.forRootAsync({
      useFactory: async () => {
        const AuthModule = await loadAuthModule();
        return { imports: [AuthModule] };
      },
    }),
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {}
}

// 컨트롤러에서 Remote Guard 사용
@Controller('orders')
export class OrderController {
  @Get()
  @UseGuards(/* Remote AuthGuard가 DI로 주입됨 */)
  async findAll(@User() user: UserDto) {
    return this.orderService.findByUser(user.id);
  }
}

shared 설정: 의존성 충돌 방지

Module Federation에서 가장 중요한 설정이 shared입니다. NestJS의 싱글톤 DI 컨테이너가 깨지지 않도록 핵심 패키지를 공유해야 합니다.

// 공유 설정 패턴
shared: {
  // 필수: NestJS 코어 (싱글톤 강제)
  '@nestjs/common': { 
    singleton: true, 
    eager: true,
    requiredVersion: '^10.0.0'  // 버전 범위 지정
  },
  '@nestjs/core': { 
    singleton: true, 
    eager: true,
    requiredVersion: '^10.0.0'
  },
  
  // RxJS: NestJS 내부에서 사용
  'rxjs': { singleton: true },
  
  // 검증 라이브러리: DTO 공유 시 필수
  'class-validator': { singleton: true },
  'class-transformer': { singleton: true },
  
  // ORM: DB 커넥션 공유 필요 시
  '@mikro-orm/core': { singleton: true },
  '@mikro-orm/nestjs': { singleton: true },
  
  // 주의: 공유하면 안 되는 것
  // - 서비스별 ConfigModule (환경변수가 다름)
  // - 서비스별 DB 모듈 (커넥션이 다름)
}

singleton: true를 빠뜨리면 NestJS의 @nestjs/core가 중복 인스턴스화되어 “Nest can’t resolve dependencies” 에러가 발생합니다. 이것이 가장 흔한 함정입니다.

동적 Remote 로딩: 런타임 서비스 디스커버리

하드코딩된 Remote URL 대신 런타임에 서비스를 발견하고 모듈을 로드하는 패턴입니다.

// 동적 Remote 로더
@Injectable()
export class FederationLoaderService {
  private readonly remoteCache = new Map<string, any>();

  constructor(
    private readonly configService: ConfigService,
    private readonly discoveryService: ServiceDiscoveryService,
  ) {}

  async loadRemoteModule<T>(
    remoteName: string, 
    exposedModule: string,
  ): Promise<T> {
    const cacheKey = `${remoteName}/${exposedModule}`;
    
    if (this.remoteCache.has(cacheKey)) {
      return this.remoteCache.get(cacheKey);
    }

    // 서비스 디스커버리에서 Remote URL 조회
    const remoteUrl = await this.discoveryService
      .resolve(remoteName);
    
    // @module-federation/utilities의 동적 로드
    const { importRemote } = await import(
      '@module-federation/utilities'
    );
    
    const module = await importRemote({
      url: remoteUrl,
      scope: remoteName,
      module: exposedModule,
      remoteEntryFileName: 'remoteEntry.js',
    });

    this.remoteCache.set(cacheKey, module);
    return module;
  }

  // Remote 모듈 헬스 체크
  async healthCheck(remoteName: string): Promise<boolean> {
    try {
      const url = await this.discoveryService.resolve(remoteName);
      const response = await fetch(`${url}/remoteEntry.js`, {
        method: 'HEAD',
        signal: AbortSignal.timeout(3000),
      });
      return response.ok;
    } catch {
      return false;
    }
  }
}

버전 관리와 안전한 배포

Module Federation에서 Remote 버전 불일치는 런타임 에러로 직결됩니다. 안전한 배포 전략이 필수입니다.

// 버전 매니페스트 패턴
// apps/auth-remote/federation-manifest.json
{
  "name": "auth_remote",
  "version": "2.3.1",
  "exposes": {
    "./AuthModule": {
      "version": "2.3.1",
      "breaking": false
    },
    "./AuthGuard": {
      "version": "2.3.0",
      "breaking": false
    }
  },
  "compatibility": {
    "host_api": ">=1.5.0",
    "shared_remote": ">=1.0.0"
  }
}

// Host에서 버전 호환성 검증
@Injectable()
export class FederationVersionGuard {
  async validateCompatibility(
    remoteName: string,
    requiredVersion: string,
  ): Promise<void> {
    const manifest = await this.loadManifest(remoteName);
    
    if (!semver.satisfies(manifest.version, requiredVersion)) {
      throw new IncompatibleRemoteError(
        `${remoteName} v${manifest.version}은 ` +
        `요구 버전 ${requiredVersion}과 호환되지 않습니다.`
      );
    }
  }
}

테스트 전략: Remote 모킹

Host 서비스를 단독으로 테스트할 때 Remote 의존성을 모킹하는 패턴입니다.

// test/federation.mock.ts
// Remote 모듈을 로컬 모킹으로 대체
jest.mock('auth_remote/AuthModule', () => ({
  AuthModule: class MockAuthModule {
    static forRoot() {
      return {
        module: MockAuthModule,
        providers: [
          { provide: 'AuthService', useClass: MockAuthService },
        ],
      };
    }
  },
}), { virtual: true });

jest.mock('auth_remote/AuthGuard', () => ({
  AuthGuard: class MockAuthGuard implements CanActivate {
    canActivate(): boolean { return true; }
  },
}), { virtual: true });

// E2E 테스트
describe('OrderController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [AppModule],
    })
    .overrideGuard(AuthGuard)
    .useClass(MockAuthGuard)
    .compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/orders (GET) - 인증된 사용자', () => {
    return request(app.getHttpServer())
      .get('/orders')
      .set('Authorization', 'Bearer mock-token')
      .expect(200);
  });
});

프로덕션 운영 체크리스트

NestJS Module Federation을 프로덕션에 도입할 때 확인해야 할 핵심 사항입니다.

  1. shared 싱글톤 필수: @nestjs/common, @nestjs/core는 반드시 singleton: true, eager: true
  2. Remote 장애 격리: Remote 로드 실패 시 폴백 로직 구현 (Circuit Breaker 패턴)
  3. 버전 호환성 검증: 배포 파이프라인에서 매니페스트 기반 호환성 체크
  4. 캐시 무효화: Remote 업데이트 시 Host의 모듈 캐시 갱신 전략 수립
  5. 모니터링: Remote 로드 시간, 실패율, 버전 불일치 메트릭 수집
  6. 보안: Remote URL을 내부 네트워크로 제한, remoteEntry.js 접근 인증
  7. 롤백 계획: Remote 배포 실패 시 이전 버전 remoteEntry.js로 즉시 롤백

언제 Module Federation을 써야 하는가?

Module Federation이 모든 상황에 적합한 것은 아닙니다.

  • 적합: 공통 인증 모듈, 공유 유틸리티, DTO/타입 동기화, 모노레포 내 코드 재사용
  • 부적합: 서비스 간 데이터 교환(→ gRPC/Kafka), 완전 독립 배포 필요(→ 전통적 마이크로서비스), 다른 언어 혼용(→ API Gateway)

Module Federation은 마이크로서비스의 코드 재사용 문제를 해결하는 도구입니다. 통신이 아닌 공유가 목적일 때 빛을 발합니다. NestJS 마이크로서비스 기본 패턴은 NestJS Microservices Transport 가이드를, 모노레포 구조 설계는 NestJS Monorepo 구조 설계 글을 참고하세요.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux