Node.js 마이크로서비스 내 모듈 페더레이션을 통한 원활한 코드 공유
Daniel Hayes
Full-Stack Engineer · Leapcell

소개
현대 소프트웨어 개발의 진화하는 환경에서 마이크로서비스 아키텍처는 독립적인 배포 가능성, 확장성 및 기술 다양성과 같은 수많은 이점을 제공하는 지배적인 패턴으로 부상했습니다. 그러나 개별 서비스가 늘어남에 따라 새로운 과제가 자주 발생합니다. 바로 일반적인 코드, 유틸리티 또는 구성 요소를 이러한 서비스 전반에 걸쳐 효과적으로 공유하는 방법입니다. 이는 종종 버전 관리 문제나 쓸데없이 큰 종속성을 초래하는 기존 패키지 관리 전략에 의존하지 않고 이루어져야 합니다. 이는 Node.js 환경에서 특히 중요합니다. 공유 코드 베이스는 상용구 코드를 크게 줄이고 일관성을 개선하며 개발 주기를 가속화할 수 있기 때문입니다. 이를 해결하기 위해 프론트엔드 세계에서 처음으로 대중화된 Module Federation은 백엔드에서도 동적 코드 공유를 가능하게 하는 혁신적이고 강력한 솔루션을 제공합니다. 이 글은 Node.js 마이크로서비스 내에서 Module Federation의 실용적인 적용을 다루며, 서비스 간 코드 협업을 어떻게 재정의할 수 있는지 설명합니다.
Node.js 마이크로서비스를 위한 Module Federation 이해하기
구현 세부 사항을 자세히 살펴보기 전에 Node.js 환경에서의 Module Federation과 관련된 몇 가지 핵심 개념과 관련성을 명확히 해보겠습니다.
핵심 개념
- Module Federation: webpack 기능으로, JavaScript 애플리케이션이 또 다른 애플리케이션의 코드를 종속성으로 취급하여 동적으로 로드할 수 있습니다. 기존 종속성 관리와 달리 Federated Module은 소비 애플리케이션 내에서 미리 번들로 묶이지 않습니다. 대신 런타임에 노출되고 소비됩니다. 이러한 동적 연결 기능이 강력함의 핵심입니다.
- Host (Container): 다른 애플리케이션에서 노출된 모듈을 소비하는 애플리케이션입니다. 마이크로서비스 설정에서 다른 서비스의 기능을 사용해야 하는 서비스는 호스트 역할을 합니다.
- Remote (Exposed Module): 다른 애플리케이션에서 소비할 수 있도록 일부 모듈을 노출하는 애플리케이션입니다. 공유 유틸리티 또는 논리를 제공하는 마이크로서비스는 원격이 됩니다.
- Shared Modules: 여러 페더레이션 애플리케이션에 공통적인 종속성 (
lodash
,express
,uuid
등). Module Federation의shared
구성은 이러한 종속성이 한 번만 로드되도록 하여 동일한 라이브러리의 여러 인스턴스를 방지함으로써 성능과 일관성을 개선합니다. 이는 Node.js 서비스가 종속성을 효율적으로 관리하는 데 중요합니다.
작동 원리
Module Federation은 본질적으로 각 원격 애플리케이션에 대해 "remoteEntry.js"라는 특수 번들을 생성하여 작동합니다. 이 파일은 노출된 모듈에 대한 메타데이터와 로드 스크립트를 포함하는 매니페스트 역할을 합니다. 호스트가 원격 모듈을 소비해야 할 때 노출된 모듈에 대한 메타데이터와 로드 스크립트를 포함하는 매니페스트 역할을 합니다. 호스트가 원격 모듈을 소비해야 할 때 원격 서비스(Node.js 설정에서는 종종 HTTP를 통해)에서 이 remoteEntry.js
파일을 동적으로 가져와 요청된 모듈을 로드하는 부트스트랩 로직을 실행합니다. 그런 다음 webpack의 런타임 글루는 이 모듈을 호스트 애플리케이션의 모듈 그래프에 통합하여 마치 로컬에 설치된 종속성인 것처럼 사용할 수 있게 합니다.
Node.js 마이크로서비스의 경우, 서비스는 유틸리티 함수, API 클라이언트 또는 전체 미들웨어 스택을 노출할 수 있으며 다른 서비스는 런타임에 직접 이를 소비할 수 있습니다. 개인 npm 패키지를 게시할 필요도 없고, 공유 코드만을 위한 복잡한 모노레포 설정도 필요 없습니다. 단순히 직접적인 런타임 관계만 있습니다.
구현 및 적용
실용적인 예를 통해 이를 설명해 보겠습니다. User Service
와 Auth Service
라는 두 개의 Node.js 마이크로서비스가 있다고 가정해 보겠습니다. Auth Service
는 사용자 인증을 처리하고 JWT 토큰을 생성합니다. User Service
는 인증된 요청에 대해 이러한 토큰을 검증해야 합니다. 토큰 검증 로직을 중복하거나 내부 npm 라이브러리로 패키징하는 대신 Module Federation을 사용할 수 있습니다.
Auth Service (Remote)
Auth Service
는 validateJwtToken
유틸리티 함수를 노출합니다.
먼저 Node.js용 webpack 5가 설치 및 구성되어 있는지 확인합니다.
npm install webpack webpack-cli webpack-node-externals @module-federation/node@next
Auth Service의 webpack.config.js
:
const { ModuleFederationPlugin } = require('@module-federation/node'); const path = require('path'); const nodeExternals = require('webpack-node-externals'); module.exports = { entry: './src/index.js', target: 'node', mode: 'development', // or 'production' output: { path: path.resolve(__dirname, 'dist'), filename: 'main.js', libraryTarget: 'commonjs-static', // Important for Node.js }, externals: [nodeExternals()], // Exclude node_modules from the bundle plugins: [ new ModuleFederationPlugin({ name: 'authServiceRemote', filename: 'remoteEntry.js', // This file will be fetched by hosts exposes: { './tokenValidator': './src/utils/tokenValidator.js', // Exposing a utility }, shared: { // Shared dependencies to avoid duplication. // Webpack will ensure only one instance is loaded. jsonwebtoken: { singleton: true, requiredVersion: '^8.5.1', }, dotenv: { singleton: true, requiredVersion: '^16.0.0', }, }, }), ], };
Auth Service의 src/utils/tokenValidator.js
:
const jwt = require('jsonwebtoken'); require('dotenv').config(); const JWT_SECRET = process.env.JWT_SECRET || 'supersecretkey'; function validateJwtToken(token) { try { const decoded = jwt.verify(token, JWT_SECRET); return { isValid: true, user: decoded }; } catch (error) { return { isValid: false, error: error.message }; } } module.exports = { validateJwtToken };
Auth Service
는 일반적으로 remoteEntry.js
파일을 알려진 엔드포인트(예: http://localhost:3001/remoteEntry.js
)에서 노출합니다. 이는 일반적으로 dist
폴더를 직접 제공하거나 기존 Express 앱에 통합하는 것을 의미합니다.
User Service (Host)
User Service
는 Auth Service
에서 validateJwtToken
함수를 소비합니다.
User Service의 webpack.config.js
:
const { ModuleFederationPlugin } = require('@module-federation/node'); const path = require('path'); const nodeExternals = require('webpack-node-externals'); module.exports = { entry: './src/index.js', target: 'node', mode: 'development', output: { path: path.resolve(__dirname, 'dist'), filename: 'main.js', libraryTarget: 'commonjs-static', }, externals: [nodeExternals()], plugins: [ new ModuleFederationPlugin({ name: 'userServiceHost', remotes: { // Point to the Auth Service's remoteEntry.js authServiceRemote: 'authServiceRemote@http://localhost:3001/remoteEntry.js', }, shared: { jsonwebtoken: { singleton: true, requiredVersion: '^8.5.1', }, dotenv: { singleton: true, requiredVersion: '^16.0.0', }, express: { singleton: true, requiredVersion: '^4.17.1', }, }, }), ], };
User Service의 src/index.js
:
const express = require('express'); const { validateJwtToken } = require('authServiceRemote/tokenValidator'); // Consuming the remote module const app = express(); app.use(express.json()); // Middleware to protect routes using the federated token validator app.use(async (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).send('No authorization header'); } const token = authHeader.split(' ')[1]; if (!token) { return res.status(401).send('Token not provided'); } // Use the federated function! const validationResult = await validateJwtToken(token); if (validationResult.isValid) { req.user = validationResult.user; // Attach user info to request next(); } else { res.status(403).send(`Invalid token: ${validationResult.error}`); } }); app.get('/profile', (req, res) => { res.json({ message: `Welcome ${req.user.username}! This is your profile.`, user: req.user }); }); const PORT = 3000; app.listen(PORT, () => { console.log(`User Service running on port ${PORT}`); });
User Service
가 시작될 때 http://localhost:3001/remoteEntry.js
에서 authServiceRemote/tokenValidator
를 동적으로 로드하려고 시도합니다. 이를 통해 User Service
는 jsonwebtoken
또는 dotenv
를 두 번 번들로 묶거나 사전 게시된 token-validator
패키지가 필요 없이 Auth Service
의 로직을 직접 활용할 수 있습니다.
이점 및 사용 사례
- 중복 감소: 공통 로직(예: 검증 스키마, 특정 알고리즘, API 클라이언트)을 중앙 집중화하고 서비스 전반에 공유합니다.
- 일관성 향상: 모든 서비스가 공유 구성 요소 또는 유틸리티의 동일한 버전을 사용하는지 확인하여