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을 프로덕션에 도입할 때 확인해야 할 핵심 사항입니다.
- shared 싱글톤 필수:
@nestjs/common,@nestjs/core는 반드시singleton: true, eager: true - Remote 장애 격리: Remote 로드 실패 시 폴백 로직 구현 (Circuit Breaker 패턴)
- 버전 호환성 검증: 배포 파이프라인에서 매니페스트 기반 호환성 체크
- 캐시 무효화: Remote 업데이트 시 Host의 모듈 캐시 갱신 전략 수립
- 모니터링: Remote 로드 시간, 실패율, 버전 불일치 메트릭 수집
- 보안: Remote URL을 내부 네트워크로 제한, remoteEntry.js 접근 인증
- 롤백 계획: Remote 배포 실패 시 이전 버전 remoteEntry.js로 즉시 롤백
언제 Module Federation을 써야 하는가?
Module Federation이 모든 상황에 적합한 것은 아닙니다.
- 적합: 공통 인증 모듈, 공유 유틸리티, DTO/타입 동기화, 모노레포 내 코드 재사용
- 부적합: 서비스 간 데이터 교환(→ gRPC/Kafka), 완전 독립 배포 필요(→ 전통적 마이크로서비스), 다른 언어 혼용(→ API Gateway)
Module Federation은 마이크로서비스의 코드 재사용 문제를 해결하는 도구입니다. 통신이 아닌 공유가 목적일 때 빛을 발합니다. NestJS 마이크로서비스 기본 패턴은 NestJS Microservices Transport 가이드를, 모노레포 구조 설계는 NestJS Monorepo 구조 설계 글을 참고하세요.