Node.js EventEmitter 및 메시지 큐를 사용한 이벤트 기반 마이크로서비스
Takashi Yamamoto
Infrastructure Engineer · Leapcell

소개
오늘날 빠르게 변화하는 디지털 세계에서 견고하고 확장 가능한 애플리케이션은 무엇보다 중요합니다. 시스템이 복잡해짐에 따라 모놀리식 아키텍처는 종종 병목 현상이 되어 개발 속도와 배포 유연성을 저해합니다. 이로 인해 애플리케이션을 느슨하게 결합되고 독립적으로 배포 가능한 서비스 모음으로 구성하는 아키텍처 스타일인 마이크로서비스가 널리 채택되었습니다. 마이크로서비스는 수많은 이점을 제공하지만, 이러한 분산 구성 요소 간의 통신을 효과적으로 오케스트레이션하고 상태를 관리하는 데는 새로운 과제가 있습니다.
이 글에서는 Node.js EventEmitter와 메시지 큐를 함께 사용하여 효율적인 이벤트 기반 마이크로서비스 아키텍처의 백본을 형성하는 방법을 탐구하며, 비동기 통신, 향상된 분리 및 응답성을 지원합니다. 기본 개념, 실제 구현 세부 정보 및 이러한 패턴이 강력한 분산 시스템 구축에 가져다주는 상당한 가치를 살펴보겠습니다.
강력한 분산 시스템 구축
EventEmitter
와 메시지 큐의 조합의 강력함을 완전히 이해하려면 관련된 핵심 개념을 먼저 파악해야 합니다.
- 마이크로서비스: 서비스 지향 아키텍처(SOA) 아키텍처 스타일의 변형으로, 애플리케이션을 느슨하게 결합된 서비스 모음으로 구성하는 소프트웨어 개발 기법입니다. 각 서비스는 자체 포함되어 있으며 일반적으로 단일 비즈니스 기능에 중점을 둡니다.
- 이벤트 기반 아키텍처(EDA): 구성 요소가 이벤트를 생산하고 소비함으로써 통신하는 아키텍처 패턴입니다. '이벤트'는 '주문 완료' 또는 '사용자 등록'과 같은 상태의 중요한 변경입니다. EDA는 분리, 확장성 및 응답성을 촉진합니다.
- Node.js EventEmitter: 단일 프로세스 내에서 이벤트 기반 프로그래밍을 촉진하는 핵심 Node.js 모듈입니다. 등록된
Function
객체를 호출하는 명명된 이벤트를 발생시키는 객체를 허용합니다. 프로세스 내 통신을 위한 간단한 게시/구독 메커니즘으로 생각하세요. - 메시지 큐: 서버리스 및 마이크로서비스 아키텍처에서 사용되는 비동기 서비스 간 통신 형태입니다. 메시지는 소비 서비스가 처리할 때까지 임시로 저장됩니다. 인기 있는 예로는 RabbitMQ, Apache Kafka 및 AWS SQS가 있습니다. 중간자 역할을 하여 생산자와 소비자를 분리하고 버퍼링, 안정성 및 확장을 제공합니다.
시너지: EventEmitter 및 메시지 큐
본질적으로 이벤트 기반 마이크로서비스 아키텍처는 직접 요청이 아닌 이벤트를 통해 통신하는 서비스에서 번성합니다. EventEmitter
는 내부 프로세스 내 이벤트 처리에 뛰어나지만, 프로세스 간 또는 서비스 간 통신을 위해 설계되지는 않았습니다. 여기서 메시지 큐가 필수적이 됩니다.
UserService
가 새 사용자를 생성하는 시나리오를 생각해 보세요. UserService
내부에서는 로컬 모듈(예: 로깅, 로컬 캐시 업데이트)이 반응하도록 EventEmitter
를 사용하여 'userCreated'
이벤트를 발생시킬 수 있습니다. 그러나 EmailService
또는 BillingService
와 같은 다른 마이크로서비스도 이 새 사용자에 대한 알림을 받아야 합니다. 이러한 서비스를 UserService
에서 직접 호출하면 긴밀한 결합이 발생합니다.
대신 UserService
는 'UserCreated'
이벤트를 메시지 큐에 게시할 수 있습니다. EmailService
와 BillingService
는 소비자 역할을 하여 이 큐를 구독하고 이벤트를 독립적으로 처리할 수 있습니다. 이를 통해 다음을 달성할 수 있습니다.
- 분리: 서비스는 서로의 존재를 알 필요 없이 자신이 생산하거나 소비하는 이벤트만 알면 됩니다.
- 비동기: 서비스는 자신의 속도에 맞춰 이벤트를 처리할 수 있어 전반적인 시스템 응답성과 내결함성이 향상됩니다.
- 확장성: 메시지 큐는 이벤트의 폭증을 처리할 수 있어 서비스를 독립적으로 확장할 수 있습니다.
- 복원력: 소비자 서비스가 일시적으로 다운되면 메시지는 큐에 남아 서비스 복구 후 처리할 수 있습니다.
Node.js를 사용한 실제 예제를 통해 이를 설명해 보겠습니다.
예제: 주문 처리 마이크로서비스
세 개의 마이크로서비스가 있는 전자 상거래 애플리케이션을 상상해 보세요.
- 주문 서비스: 새 주문 처리를 담당합니다.
- 결제 서비스: 주문에 대한 결제를 처리합니다.
- 알림 서비스: 이메일 알림을 보냅니다.
Order Service
내부의 내부 이벤트를 위해 Node.js EventEmitter
를 사용하고 서비스 간 통신을 위해 가상의 메시지 큐(간단한 MessageQueueClient
로 표시)를 사용하겠습니다.
주문 서비스 (생산자)
// order-service/index.js const EventEmitter = require('events'); const messageQueueClient = require('./messageQueueClient'); // 단순화된 MQ 클라이언트 class OrderServiceEmitter extends EventEmitter {} const orderEvents = new OrderServiceEmitter(); // 주문 생성 시뮬레이션 function createOrder(orderData) { const order = { id: Math.random().toString(36).substr(2, 9), ...orderData, status: 'pending' }; console.log(`Order Service: Creating order ${order.id}`); // 내부 이벤트: 주문 서비스 내의 수신자에게 알림 orderEvents.emit('orderPending', order); // 다른 서비스에서 사용할 이벤트 큐에 이벤트 게시 messageQueueClient.publish('order_events', { type: 'OrderCreated', payload: order }); return order; } // 예제 내부 수신자 orderEvents.on('orderPending', (order) => { console.log(`Order Service: Internal listener - Order ${order.id} is pending.`); // 여기에서 로컬 캐시를 업데이트하거나 다른 내부 작업을 수행할 수 있습니다. }); // API 엔드포인트 노출 시뮬레이션 function handleCreateOrderRequest(req, res) { const newOrder = createOrder(req.body); res.status(201).json(newOrder); } // ... handleCreateOrderRequest를 위한 express 앱 설정
// order-service/messageQueueClient.js (단순화된 플레이스홀더) module.exports = { publish: (topic, message) => { console.log(`Order Service: Publishing to topic '${topic}':`, message); // 실제 애플리케이션에서는 RabbitMQ, Kafka 등으로 전송됩니다. // 로컬 테스트를 위해 단순화된 직접 호출을 시뮬레이션할 수 있지만, // 원칙은 비동기 메시징입니다. }, subscribe: (topic, handler) => { console.log(`Order Service: Subscribing to topic '${topic}' (client side)`); // 생산자 서비스 자체 이벤트에서는 일반적으로 사용되지 않습니다. } };
결제 서비스 (소비자)
// payment-service/index.js const messageQueueClient = require('./messageQueueClient'); // 동일한 MQ 클라이언트 function processPayment(order) { console.log(`Payment Service: Processing payment for order ${order.id}...`); // 결제 처리 로직 시뮬레이션 setTimeout(() => { const paymentSuccessful = Math.random() > 0.1; // 90% 성공률 if (paymentSuccessful) { console.log(`Payment Service: Payment for order ${order.id} successful.`); // 다른 서비스를 위한 결제 성공 이벤트 게시 messageQueueClient.publish('payment_events', { type: 'PaymentApproved', payload: { orderId: order.id, amount: order.amount } }); } else { console.warn(`Payment Service: Payment for order ${order.id} failed.`); messageQueueClient.publish('payment_events', { type: 'PaymentFailed', payload: { orderId: order.id, reason: 'Failed to authorize' } }); } }, 1500); // 네트워크 지연/처리 시간 시뮬레이션 } // 주문 생성 이벤트 구독 messageQueueClient.subscribe('order_events', (event) => { if (event.type === 'OrderCreated') { console.log(`Payment Service: Received OrderCreated event for order ${event.payload.id}`); processPayment(event.payload); } }); console.log('Payment Service: Started and listening for order_events...');
// payment-service/messageQueueClient.js (단순화된 플레이스홀더, order-service와 동일) module.exports = { publish: (topic, message) => { console.log(`Payment Service: Publishing to topic '${topic}':`, message); // ... }, subscribe: (topic, handler) => { console.log(`Payment Service: Subscribing to topic '${topic}' (client side)`); // 실제 MQ 클라이언트에서는 여기에 수신기를 설정합니다. // 이 예제에서는 이벤트 흐름을 보여주기 위해 수신을 시뮬레이션합니다. // 실제 MQ는 게시된 메시지를 구독하는 서비스에 전달합니다. } }; // 실제 MQ 설정에서 'subscribe' 메서드는 메시지가 도착할 때마다 호출되는 콜백을 등록합니다. // 실제 MQ 없이 멀티 서비스 데모를 하려면 중앙 시뮬레이터나 직접 메시지 전달이 필요할 수 있습니다. // 명확성을 위해 '중앙 메시지 브로커'가 publish에서 메시지를 수신하고 subscribe 핸들러에 다시 배포한다고 가정합니다. const messageBroker = require('../../global-message-broker'); // 데모를 위한 전역 객체 if (messageBroker) { module.exports.publish = (topic, message) => { console.log(`[MQ Client] Publishing to ${topic}:`, message); messageBroker.publish(topic, message); }; module.exports.subscribe = (topic, handler) => { console.log(`[MQ Client] Subscribing to ${topic}`); messageBroker.subscribe(topic, handler); }; } else { // 전역 브로커가 없는 경우 (예: 개별 서비스 테스트) console.warn("Global message broker not found. MQ client operating in simulated isolated mode."); }
알림 서비스 (소비자)
// notification-service/index.js const messageQueueClient = require('./messageQueueClient'); // 동일한 MQ 클라이언트 function sendOrderConfirmationEmail(orderId, email) { console.log(`Notification Service: Sending order confirmation email for order ${orderId} to ${email}...`); // 이메일 전송 시뮬레이션 setTimeout(() => { console.log(`Notification Service: Order confirmation email sent for order ${orderId}.`); }, 1000); } function sendPaymentFailureNotification(orderId) { console.warn(`Notification Service: Sending payment failure notification for order ${orderId}...`); // 알림 또는 다른 이메일 전송 시뮬레이션 setTimeout(() => { console.warn(`Notification Service: Payment failure notification sent for order ${orderId}.`); }, 1000); } // 주문 생성 및 결제 이벤트 구독 messageQueueClient.subscribe('order_events', (event) => { if (event.type === 'OrderCreated') { console.log(`Notification Service: Received OrderCreated event for order ${event.payload.id}`); // 주문 페이로드에 사용자 이메일이 포함되어 있다고 가정 sendOrderConfirmationEmail(event.payload.id, event.payload.customerEmail || 'customer@example.com'); } }); messageQueueClient.subscribe('payment_events', (event) => { if (event.type === 'PaymentApproved') { console.log(`Notification Service: Received PaymentApproved event for order ${event.payload.orderId}`); // 주문 확인과 다른 '결제 성공' 이메일을 트리거할 수 있습니다. } else if (event.type === 'PaymentFailed') { console.warn(`Notification Service: Received PaymentFailed event for order ${event.payload.orderId}`); sendPaymentFailureNotification(event.payload.orderId); } }); console.log('Notification Service: Started and listening for order_events and payment_events...');
// notification-service/messageQueueClient.js (동일하게) // ...
전역 메시지 브로커 (간단한 로컬 데모용)
실제 MQ 서버 없이 이 데모를 로컬에서 실행하려면 모든 messageQueueClient.js
파일이 가져올 수 있는 매우 간단한 '인메모리' 메시지 브로커를 만들 수 있습니다.
// global-message-broker.js const EventEmitter = require('events'); class GlobalMessageBroker extends EventEmitter { constructor() { super(); this.setMaxListeners(0); // 다른 주제에 대한 무제한 수신자 } publish(topic, message) { console.log(`[Global MQ Broker] Emitting event on topic: ${topic}`); this.emit(topic, message); } subscribe(topic, handler) { console.log(`[Global MQ Broker] Subscriber registered for topic: ${topic}`); this.on(topic, handler); } } const broker = new GlobalMessageBroker(); module.exports = broker; // 예제의 단순화된 messageQueueClient에서 올바르게 참조할 수 있도록 전역적으로 접근 가능하게 만듭니다. global.messageBroker = broker;
이 간단한 데모를 실행하려면:
global-message-broker.js
파일을 만듭니다.- 각 서비스의
messageQueueClient.js
파일에서global.messageBroker = broker;
또는 유사한 메커니즘이global-message-broker.js
가 올바르게 참조되도록 합니다. payment-service/index.js
를 시작하고,notification-service/index.js
를 시작한 다음,order-service/index.js
에서 주문 생성을 시뮬레이션합니다(예:createOrder
함수를 직접 호출하거나 HTTP 요청을 통해).
그러면 다음을 관찰할 수 있습니다.
Order Service
는 내부orderPending
이벤트를 발생시킵니다.Order Service
는 '메시지 큐'에OrderCreated
이벤트를 게시합니다.Payment Service
는OrderCreated
를 수신하고 결제를 처리합니다.Payment Service
는 '메시지 큐'에PaymentApproved
또는PaymentFailed
를 게시합니다.Notification Service
는OrderCreated
및PaymentApproved
/PaymentFailed
이벤트에 반응하여 이메일을 보냅니다.
주요 이점 및 고려 사항
이점:
- 느슨한 결합: 서비스는 독립적으로 작동하여 상호 의존성을 줄이고 개발, 배포 및 확장을 용이하게 합니다.
- 비동기 처리: 무거운 처리를 백그라운드 작업으로 오프로드하여 요청에 빠르게 응답함으로써 사용자 경험을 개선합니다.
- 확장성: 메시지 큐는 트래픽 급증을 완충하여 서비스가 자체 속도로 이벤트를 처리하고 독립적으로 확장할 수 있도록 합니다.
- 복원력 및 내결함성: 서비스가 다운되면 메시지는 큐에 남아 서비스가 복구될 때 결국 처리되도록 합니다. 데드 레터 큐는 실패한 메시지 처리를 처리할 수 있습니다.
- 이벤트 소싱 및 감사 추적: 이벤트를 저장하여 애플리케이션 상태를 재구성하거나 포괄적인 감사 로그를 제공할 수 있습니다.
고려 사항:
- 복잡성: 메시지 큐를 도입하면 관리하고 모니터링해야 할 구성 요소가 하나 더 추가됩니다.
- 최종 일관성: 비동기 처리로 인해 서비스 간 데이터가 즉시 일관되지 않을 수 있습니다. 이는 신중한 설계가 필요합니다.
- 디버깅: 여러 서비스와 메시지 큐에 걸쳐 이벤트 흐름을 추적하는 것은 동기 요청-응답 디버깅보다 더 어려울 수 있습니다. 분산 추적 도구가 필수적입니다.
- 메시지 스키마 관리: 생산자와 소비자 간의 호환성을 위해 이벤트 페이로드에 일관되고 진화하는 스키마가 있는지 확인하는 것이 중요합니다.
결론
로컬 이벤트 처리를 위한 Node.js EventEmitter
와 서비스 간 통신을 위한 강력한 메시지 큐를 결합하면 확장 가능하고 복원력이 있으며 매우 분리된 이벤트 기반 마이크로서비스를 구축하는 강력한 기반을 제공합니다. 초기 복잡성이 발생하지만, 분산 환경에서의 유지 관리성, 유연성 및 성능 측면에서 장기적인 이점은 상당합니다. 이 아키텍처 패턴을 채택함으로써 개발자는 현대 클라우드 네이티브 애플리케이션의 요구 사항을 우아하게 처리하는 복잡한 시스템을 구축할 수 있습니다. 확장 가능하고 분리된 마이크로서비스를 잠금 해제하려면 이벤트를 활용하세요.