8.10 Node.js 기반 백엔드 및 마이크로서비스 아키텍처 연동
프론트엔드 생태계가 Browser API 제약에 갇힌 “소비자(Consumer)“라면, Node.js 백엔드는 포트 포워딩과 파일 시스템 접근 권한이 모두 열린 “공급자(Provider)“이자 “중계자(Broker)“다.
실제 상용(Production) 아키텍처에서 모든 웹 클라이언트가 직접 Zenoh 라우터에 8000번 WebSocket 포트를 열고 들어가게 만들진 않는다. 보안 인가(Auth), 데이터베이스 로깅, 레거시 HTTP 트래픽과의 융합을 위해 최전방에는 항상 Node.js (Express, NestJS)가 백엔드 관문(Gateway)으로 버티고 있어야 한다.
이 장에서는 기존의 느리고 무거운 HTTP/REST 생태계를 어떻게 빠르고 날카로운 Zenoh 멀티캐스트 망으로 우아하게 “변환(Bridging)“하는지 그 하이브리드(Hybrid) 결합의 진수를 펼쳐 보인다. 여기서 당신의 Node.js 서버는 REST로 요청을 받아, P2P 그물망에 쿼리를 꽂아 데이터를 퍼온 뒤, 다시 브라우저에 JSON으로 넘겨주는 진정한 거미(Spider)로 진화하게 된다.
1. Express.js / NestJS와 Zenoh를 결합한 하이브리드 서버 구축
만약 당신이 NestJS나 Express.js 로 짠 백엔드를 들고 있다면, 제일 먼저 해야 할 일은 이 프레임워크들의 부팅 루프(Booting Loop) 안에 Zenoh 세션 초기화 로직을 우겨넣는 것이다.
1.0.1 [Runbook] NestJS 의존성 주입(DI) 브릿지 전술
가장 모던한 백엔드 프레임워크인 NestJS의 의존성 주입 생태계에 Zenoh를 Provider 로 장착한다.
// --- zenoh.module.ts ---
import { Module, Global } from '@nestjs/common';
import * as zenoh from "@eclipse-zenoh/zenoh";
@Global() // 전역 모듈로 선언하여 어디서든 이 세션을 쓸 수 있게 강제함
@Module({
providers: [
{
provide: 'ZENOH_SESSION',
useFactory: async () => {
console.log('[Zenoh] 로컬 스웜 라우터 스카우팅 개시...');
// Node.js 네이티브 N-API가 이 컴퓨터의 UDP 멀티캐스트 자원을 동원하여
// 0.1초만에 스웜을 찾아 뚫어낸다.
const session = await zenoh.open();
console.log('[Zenoh] 세션 바인딩 완료 완료');
return session;
},
},
],
exports: ['ZENOH_SESSION'],
})
export class ZenohGatewayModule {}
// --- robot.controller.ts (컨트롤러 측면) ---
import { Controller, Get, Param, Inject } from '@nestjs/common';
import * as zenoh from "@eclipse-zenoh/zenoh";
@Controller('api/robot')
export class RobotController {
constructor(
// [무기 장착]
@Inject('ZENOH_SESSION')
private readonly session: zenoh.Session
) {}
@Get(':id/ping')
pingRobot(@Param('id') id: string) {
// 10.2 장에서 다룰 'REST to Zenoh' 쿼리를 여기서 쏘면 된다.
return { status: "ready" };
}
}
이 구조의 핵심은 zenoh.open() 의 1회성 호출 보장이다.
Express의 경우 app.listen() 전에 세션을 전역 변수(Singleton 객체)로 박아두고 컨트롤러로 바인딩한다. Node.js 메모리에 단 1개의 네이티브 Rust 세션만이 자리 잡아 TCP 커넥션 풀(Pool)을 공유하는 것이 벡엔드 연동의 제 1원칙이다.
2. 기존 HTTP RESTful API 요청을 Zenoh 쿼리로 브릿징(Bridging)
외부 클라이언트(모바일 앱이나 낡은 파이썬 스크립트)가 당신의 백엔드로 GET /api/v1/drones/ALL/battery 라는 고전적인 REST 명령을 날렸다.
당신의 Node.js 서버는 DB를 뒤지지 않고, 이 질문을 Zenoh 멀티캐스트 망으로 우회(Bypass)시킨 뒤 대답을 주워 모아 다시 HTTP JSON으로 포장해 돌려준다.
2.0.1 [Runbook] HTTP -> Zenoh 분산망 파이프 변환술
Express 라우터 내부 로직이다.
import express from "express";
import * as zenoh from "@eclipse-zenoh/zenoh";
const app = express();
// session 은 이미 글로벌하게 붙어있다고 가정
let session: zenoh.Session;
app.get('/api/v1/drones/:drone_id/battery', async (req, res) => {
const target = req.params.drone_id; // "ALL" 이거나 "1" 등
// 1. URL 해석 및 Zenoh 통신 경로 조립
// REST URL을 Zenoh의 Key Expression 으로 치환한다. "ALL" 을 "*" 로 바꾼다!
const keyExpr = target === "ALL" ? `drones/*/battery` : `drones/${target}/battery`;
console.log(`[Proxy] HTTP 진입 -> Zenoh 망으로 쿼리 파동 발사: ${keyExpr}`);
try {
const resultPayloads = [];
const decoder = new TextDecoder();
// 2. 1초 타임아웃을 걸고 망에 뿌린다!
const replies = await session.get(keyExpr, zenoh.Queueing.default(), { timeout: 1000 });
// 3. 비동기 순회
for await (const reply of replies) {
const dataStr = decoder.decode(reply.ok.payload);
const source = reply.ok.keyExpr.split('/')[1]; // 경로에서 드론 아이디 탈취
resultPayloads.push({
drone_id: source,
battery: parseFloat(dataStr)
});
}
// 4. 수집 완료. 이 HTTP 요청을 던진 모바일 앱에게 일제히 반환!
// (만약 타임아웃 1초 내에 대답이 아무것도 안왔다면 빈 배열이 간다)
res.status(200).json({
success: true,
count: resultPayloads.length,
records: resultPayloads
});
} catch (e) {
res.status(500).json({ success: false, msg: "내부 스웜 통신 에러" });
}
});
이 기술을 응용하면, 공장에서 돌고 있는 모든 로봇들의 마이크로서비스 엔진을 외부에 일일이 노출(포트 포워딩)할 필요 없이, 가장 앞단에 서 있는 Node.js 라우터 1대만 공인 IP로 열어두고(API Gateway 패턴) 뒷단의 무수한 기계들을 내부망 P2P 쿼리로 관제하는 강력한 보안 쉴드가 형성된다.
3. 마이크로서비스 간 이벤트 기반 통신(Event-driven Architecture) 구성
백엔드를 “결제(Payment)”, “재고(Inventory)”, “배송(Shipping)” 서버로 쪼갰다(Microservices).
“결제 완료!” 이벤트가 떴을 때 이 세 서버가 Kafka나 RabbitMQ 를 통해 통신하게 만드는 것은 인프라 오버헤드가 너무 크다.
Node.js 백엔드 워커들끼리 서로를 Zenoh 로 통신하게 만들면 브로커(Broker) 서버 없이 P2P로 직접 이벤트를 패스하는 진정한 가벼움(Lightweight)을 얻을 수 있다.
3.0.1 [Runbook] 브로커-리스(Broker-less) 이벤트 버스 전술
A서버 (결제 파트) - 이벤트 생성(Publish)
async function finalizePayment(orderId: string, amount: number) {
// DB 로직 처리 ... (100 밀리초 소요)
// [핵심] 다른 백엔드 워커들에게 배송 준비하라고 쿨하게 알려주고 끝냄.
// 그들이 받았든 안받았든 내 알 바 아님 (Non-blocking Fire & Forget)
const eventBytes = Buffer.from(JSON.stringify({ orderId, amount, authType: "CARD" }));
await session.put(`events/payment/success/${orderId}`, eventBytes);
}
B서버 (재고 파트) - 이벤트 감지 (Subscribe)
async function watchPaymentEvents() {
// 이 노드는 켜지자마자 "events/payment/success/**" 로 날아오는 버스에 귀를 박는다.
await session.declareSubscriber("events/payment/success/*", (sample) => {
// orderId 추출
const orderId = sample.keyExpr.split('/').pop();
const rawJson = new TextDecoder().decode(sample.payload);
const eventData = JSON.parse(rawJson);
console.log(`[이벤트 포착] 결제 완료됨! 창고에서 ${orderId} 물건 뺍니다.`);
// 재고 마이너스 DB 트랜잭션 진행...
});
}
이 패턴의 파괴적인 장점은, 새로운 마이크로서비스인 “C서버 (고객 카카오톡 알림 파트)” 를 추가하려 할 때, A서버나 B서버의 코드를 단 한 줄도 고칠 필요 없이 C서버 혼자 저 경로(events/payment/**)에 숟가락(Subscriber)만 얹으면 된다는 점이다. 완벽한 도메인 분리(Decoupling)가 달성된다.
4. 데이터베이스 변경 사항(CDC)의 실시간 브로드캐스팅 라우팅
프론트엔드 대시보드의 화면을 데이터베이스 갱신 타이밍과 0.1초 이내로 일치시키는 극한의 기술, 이른바 CDC (Change Data Capture) 아키텍처 연동이다.
주로 MongoDB 의 Change Stream 이나 PostgreSQL 의 WAL (Write-Ahead Logging) 을 Node.js 백엔드가 훔쳐보다가, 테이블의 값이 바뀌는 순간 그 차분(Diff) 덩어리를 Zenoh 라우터망에 통째로 퍼블리싱 해버린다.
4.0.1 [Runbook] 데이터 투사(Data Mesh) CDC 중계기 전술
가장 보편적인 MongoDB Change Stream 과 Zenoh 의 융합 코드다.
import { MongoClient } from "mongodb";
import * as zenoh from "@eclipse-zenoh/zenoh";
async function runCdcRelay() {
// 1. Zenoh 망 개통
const session = await zenoh.open();
// 2. 몽고DB 연결
const client = new MongoClient("mongodb://localhost:27017");
await client.connect();
const db = client.db("factory");
const robotsCol = db.collection("robots_status");
console.log(">> DB 변경사항 도청기 가동 완료.");
// 3. 몽고DB의 실시간 변경 사항 커서 오픈! (Insert, Update, Delete 감지)
const changeStream = robotsCol.watch([], { fullDocument: 'updateLookup' });
changeStream.on('change', async (next) => {
// 문서가 변경될 때마다 이 이벤트가 트리거된다!
if (next.operationType === 'update' || next.operationType === 'insert') {
const doc = next.fullDocument;
const rId = doc.robot_id; // 예: "R-99"
// 바뀐 전체 도큐먼트를 압축
const payload = Buffer.from(JSON.stringify(doc));
// 4. Zenoh 그물망에 무차별 살포
// -> 브라우저에서 'db/sync/robot/R-99' 를 구독 중이던 관리자 모니터가 즉각 반응한다!
await session.put(`db/sync/robot/${rId}`, payload);
console.log(`[CDC 전파] 로봇 ${rId} 상태 업데이트가 망에 뿌려짐.`);
}
});
// 앱 종료 시 자원 회수
process.on('SIGINT', async () => {
await changeStream.close();
await client.close();
await session.close();
});
}
클라이언트는 브라우저 창에서 Refresh(F5) 버튼을 누르거나 폴링(Polling)을 시도할 필요조차 없어진다. Node.js 백엔드는 그저 DB 의 변화를 도청하여 라우터망의 튜브 속으로 흘려주는 ’수도관 관리자’로 역할이 국한되며 서버 자체의 CPU 부하율은 1% 미만으로 억제된다.