Node.js 마이크로서비스 설계와 통신 패턴
Node.js 기반 마이크로서비스에서 서비스 간 통신 패턴과 이벤트 기반 설계, 데이터 일관성 확보 전략을 초보자도 이해하기 쉬운 방식으로 정리한 설계원칙
목차
개요
마이크로서비스 아키텍처는 각 서비스가 독립적으로 배포되고 확장되는 장점을 제공한다. 그러나 서비스 분리로 인해 통신 방식과 데이터 일관성 관리가 핵심 난제로 떠오른다. 이 글은 Node.js 환경에서 흔히 쓰이는 통신 패턴과 데이터 일관성 확보 방법을 초보자도 이해하기 쉽게 정리한다.
서비스 간 통신 모델
동기식 통신(REST/HTTP)
동기식 호출은 단순하고 디버깅이 쉽다. RESTful API나 HTTP 요청을 통해 서비스 간 요청과 응답을 주고받는다. 단점은 호출된 서비스가 느려지면 호출자 전체에 영향을 준다는 점이다. 따라서 단순 CRUD 작업이나 외부 API 연동에 적합하다.
비동기식 통신(메시지 큐/이벤트)
비동기식 통신은 서비스 간 결합도를 낮추고 시스템의 탄력성을 높인다. 대표적으로 메시지 큐(RabbitMQ, Kafka 등)와 이벤트 브로커를 사용한다. 발행-구독 패턴으로 이벤트를 전달하면 서비스가 독립적으로 처리할 수 있어 가용성이 좋아진다.
gRPC와 프로토콜 기반 통신
gRPC는 고성능과 스키마 기반 통신을 제공한다. 특히 마이크로서비스가 많고 지연시간이 중요한 시스템에서 유리하다. 하지만 도입 복잡성과 언어 지원을 고려해야 한다.
통신 패턴별 장단점 요약
- REST: 단순, 디버깅 쉬움, 동기적 한계
- 이벤트 기반: 비동기, 느슨한 결합, 일관성 관리 필요
- 메시지 큐: 신뢰성 있는 전달, 복잡도 증가
- gRPC: 빠른 통신과 스키마, 운영 복잡성
데이터 일관성 전략
분산 환경에서는 단일 트랜잭션으로 모든 서비스를 동시에 변경하기 어렵다. 따라서 최종적 일관성(eventual consistency)을 목표로 설계하는 경우가 많다. 주요 전략을 소개한다.
사가 패턴(SAGA)
사가 패턴은 여러 서비스에 걸친 비즈니스 트랜잭션을 작은 로컬 트랜잭션으로 나눈다. 각 단계가 성공하면 다음 단계로 진행하고, 실패하면 보상 트랜잭션을 실행해 복구한다. 구현 방식은 크게 두 가지이다.
- 조정자(Orchestration): 중앙에서 흐름을 제어하는 서비스가 각 단계를 호출한다.
- 협력(Choreography): 각 서비스가 이벤트를 발행하고 다음 단계가 이를 받아 처리한다.
이벤트 소싱과 CQRS
이벤트 소싱은 상태 변경을 이벤트로 저장한다. 상태를 직접 저장하는 대신 이벤트 로그를 보관해 재구성한다. CQRS(Command Query Responsibility Segregation)는 읽기 모델과 쓰기 모델을 분리해 성능과 확장성을 개선한다. 단점은 시스템 복잡도와 운영 부담이 증가한다는 점이다.
아이덤포턴시(Idempotency)와 중복 처리
이벤트를 여러 번 받을 가능성이 있으므로 중복 처리를 방지하는 설계가 필요하다. 요청에 아이덤포턴트 키를 붙여 중복 요청을 무시하거나, 메시지 소비 시 소비 로그를 기록해 재처리를 막는다.
Node.js에서 적용하는 실무적 패턴
경량 이벤트 브로커 예시
외부 메시지 브로커를 도입하기 전, 내부 이벤트 버스를 통해 설계를 검증할 수 있다. Node.js의 EventEmitter를 활용한 간단한 발행/구독 예시는 다음과 같다.
const EventEmitter = require('events');
const bus = new EventEmitter();
// 발행자
function publishOrderCreated(order) {
bus.emit('order.created', { orderId: order.id, amount: order.amount });
}
// 구독자
bus.on('order.created', (payload) => {
console.log('주문 처리 시작:', payload.orderId);
// 결제, 재고 확인 등 처리
});
// 사용
publishOrderCreated({ id: 'ord_123', amount: 25000 });
이 방식은 로컬 테스트에 유용하지만, 서비스 간 통신에는 RabbitMQ나 Kafka 같은 외부 브로커가 필요하다.
RabbitMQ를 이용한 이벤트 발행/구독 예시(간단)
아래 코드는 개념 설명용으로 최소한의 흐름만 보여준다. 실제 운영 환경에서는 연결 재시도, 에러 처리, 메시지 ACK 등을 추가해야 한다.
const amqp = require('amqplib');
async function publish(queue, msg) {
const conn = await amqp.connect('amqp://localhost');
const ch = await conn.createChannel();
await ch.assertQueue(queue, { durable: true });
ch.sendToQueue(queue, Buffer.from(JSON.stringify(msg)), { persistent: true });
await ch.close();
await conn.close();
}
async function consume(queue, handler) {
const conn = await amqp.connect('amqp://localhost');
const ch = await conn.createChannel();
await ch.assertQueue(queue, { durable: true });
ch.consume(queue, async (msg) => {
if (msg !== null) {
const payload = JSON.parse(msg.content.toString());
await handler(payload);
ch.ack(msg);
}
});
}
실전에서 고려할 점
설계 시 다음 항목을 우선 점검한다.
- 서비스 경계와 책임을 명확히 정의한다.
- 통신 지연과 장애가 전체 흐름에 미치는 영향을 분석한다.
- 데이터 일관성 수준(강력 일관성 vs 최종적 일관성)을 요구사항에 맞춰 결정한다.
- 모니터링, 로깅, 재시도 정책, 장애 격리 전략을 준비한다.
결론
Node.js로 마이크로서비스를 설계할 때는 통신 패턴과 데이터 일관성 전략을 명확히 선택하는 것이 중요하다. 단순한 REST 호출은 빠르게 시작하기 좋고, 이벤트 기반 설계는 확장성과 복원력을 높인다. 사가 패턴이나 이벤트 소싱 같은 기법을 통해 복잡한 일관성 요구사항을 해결할 수 있다. 최종적으로는 시스템의 요구사항, 운영 능력, 팀의 숙련도에 맞춰 적절한 조합을 선택하는 것이 바람직하다.