소개
현대적인 웹 애플리케이션은 높은 확장성, 유연성, 그리고 견고성을 갖춘 백엔드 아키텍처가 필요합니다. 이 글에서는 진행했던 프로젝트 중 RabbitMQ를 중심으로 한 마이크로서비스 기반 시스템 아키텍처를 분석하고, 이러한 설계의 장단점을 심층적으로 살펴보겠습니다.
아키텍처 개요
다음은 RabbitMQ를 메시지 브로커로 활용하여 여러 서비스 간의 통신을 관리하는 마이크로서비스 아키텍처의 전체 구성도입니다:
이 아키텍처는 RabbitMQ를 통한 비동기 통신을 기반으로 하며, 각 컴포넌트는 명확히 정의된 책임을 가지고 있습니다. 이제 각 구성 요소와 통신 흐름을 자세히 살펴보겠습니다.
핵심 구성 요소
1. 클라이언트 (CLIENTS)
- 사용자 인터페이스를 제공하고 웹 서버와 통신
- 요청(request)을 보내고 응답(response)을 받는 구조
- 브라우저, 모바일 앱 등 다양한 클라이언트 지원
2. 웹 서버 (Web 서버)
- Express(Node.js) 기반의 웹 애플리케이션 서버
- express-session을 통한 세션 관리
- AMQP(rpc client)를 통해 마이크로서비스와 통신
- 클라이언트 요청을 처리하고 적절한 서비스에 메시지 라우팅
3. REDIS (MemoryDB)
- 세션 공유를 위한 인메모리 데이터베이스
- 웹 서버 간 세션 정보 공유 담당
- 높은 성능의 캐싱 레이어 제공
- 세션 만료 및 지속성 관리
4. RabbitMQ BROKER
- 시스템의 핵심 메시지 브로커
- REQUEST/REPLY 큐를 통한 비동기 통신 관리
- 서비스 간 메시지 라우팅 담당
- 메시지 지속성 및 신뢰성 있는 전달 보장
5. 지갑중계
- AMQP(rpc server) 구현
- 지갑 서비스와의 통신 담당
- TCP/IP를 통해 지갑 서비스와 연결
- 암호화폐 지갑 작업을 추상화
6. WALLET
- 디지털 자산 관리 서비스
- MongoDB와 연동하여, 사용자 자산 정보 관리
- 안전한 암호화폐 트랜잭션 처리
- 개인키 관리 및 보안 기능
7. DB AGENT
- mongoose를 통한 MongoDB 연결 관리
- AMQP(rpc server)를 통해 데이터베이스 작업 수행
- 데이터베이스 작업 추상화 계층 제공
- 데이터 유효성 검증 및 보안 관리
8. 비즈니스 서버
- AMQP(rpc client)를 통해 요청 수신
- AMQP(rpc server)를 통해 응답 처리
- 핵심 비즈니스 로직 구현
- 다른 마이크로서비스와 협력
9. MongoDB
- 시스템의 주요 데이터 저장소
- 분산 데이터베이스로 확장성 제공
- 문서 기반 데이터 모델 사용
- 레플리카셋 및 샤딩 지원
통신 흐름 분석
이 아키텍처의 통신 흐름은 다음과 같습니다:
- 클라이언트가 웹 서버에 요청을 보냅니다.
- 웹 서버는 Express 프레임워크를 사용하여 요청을 처리하고, 필요한 경우 세션 정보를 Redis에서 조회합니다.
- 웹 서버는 RabbitMQ 브로커를 통해 적절한 마이크로서비스(비즈니스 서버, 지갑중계, DB AGENT)에 메시지를 전달합니다.
- 각 마이크로서비스는 요청을 처리하고 결과를 RabbitMQ의 REPLY 큐를 통해 반환합니다.
- 웹 서버는 응답을 수신하여 클라이언트에게 최종 결과를 전달합니다.
이 과정에서 RabbitMQ는 비동기 통신의 중추 역할을 하며, 서비스 간의 느슨한 결합을 가능하게 합니다. 또한 메시지 지속성을 제공하여 서비스 장애 시에도 메시지가 손실되지 않도록 보장합니다.
아키텍처의 장점
1. 높은 확장성
이 아키텍처는 수평적 확장이 용이합니다. 각 마이크로서비스는 독립적으로 스케일 아웃할 수 있어 트래픽 증가에 유연하게 대응할 수 있습니다.
# 예: 수평 확장을 위한 Docker Compose 설정
version: '3'
services:
web-server:
image: express-app
deploy:
replicas: 5
depends_on:
- redis
- rabbitmq
business-server:
image: business-service
deploy:
replicas: 3
depends_on:
- rabbitmq
|
2. 견고한 메시지 큐 시스템
RabbitMQ를 중심으로 한 메시지 브로커 시스템은 다음과 같은 이점을 제공합니다:
- 비동기 통신: 서비스 간 느슨한 결합(loose coupling)으로 전체 시스템 안정성 향상
- 메시지 지속성: 시스템 장애 발생 시에도 메시지 손실 방지
- 부하 분산: 여러 서비스 인스턴스 간에 작업을 효율적으로 분배
- 신뢰성 있는 전달: 메시지 확인(acknowledgement) 메커니즘으로 안정적인 메시지 전달 보장
3. 서비스 분리와 책임 분리
각 마이크로서비스는 명확한 책임을 가지고 있어 코드 관리와 유지보수가 용이합니다:
- 웹 서버: 클라이언트 요청 처리 및 라우팅
- 지갑중계: 지갑 서비스 관련 작업 관리
- DB AGENT: 데이터베이스 작업 추상화
- 비즈니스 서버: 핵심 비즈니스 로직 처리
이러한 분리는 각 서비스의 독립적인 개발, 배포, 확장을 가능하게 합니다.
4. 세션 관리의 중앙화
Redis를 사용한 세션 관리는 다음과 같은 이점을 제공합니다:
- 여러 웹 서버 인스턴스 간의 세션 공유
- 빠른 인메모리 접근 속도
- 세션 만료 기능 내장
- 장애 복구를 위한 지속성 옵션
아키텍처의 단점
1. 시스템 복잡성 증가
마이크로서비스 아키텍처는 분산 시스템의 복잡성을 수반합니다:
- 서비스 간 통신 디버깅이 어려움
- 전체 시스템 모니터링과 로깅이 복잡해짐
- 통합 테스트의 난이도 증가
- 서비스 간 의존성 관리 필요
2. 네트워크 오버헤드
서비스 간 통신이 네트워크를 통해 이루어지므로 다음과 같은 단점이 있습니다:
- 서비스 간 통신 지연 발생
- 네트워크 대역폭 사용량 증가
- 네트워크 장애 시 서비스 연쇄 장애 가능성
- 성능 저하 가능성
3. 데이터 일관성 관리 어려움
분산 시스템에서 트랜잭션과 데이터 일관성 유지는 큰 도전 과제입니다:
- 분산 트랜잭션 구현의 어려움
- 최종 일관성(eventual consistency) 모델 적용 필요
- 데이터 중복과 동기화 문제
- 데이터 무결성 보장 메커니즘 필요
4. 운영 복잡성
여러 서비스를 관리하는 운영 측면의 복잡성이 존재합니다:
- 배포와 버전 관리가 복잡해짐
- 서비스 디스커버리 메커니즘 필요
- 장애 발생 시 문제 추적 어려움
- 인프라 관리 오버헤드 증가
구현 예제: Express 서버와 RabbitMQ 연동
다음은 웹 서버(Express)와 RabbitMQ를 연동하는 간단한 코드 예제입니다:
const express = require('express');
const amqp = require('amqplib');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const Redis = require('ioredis');
const app = express();
const port = 3000;
// Redis 클라이언트 설정
const redisClient = new Redis({
host: 'redis',
port: 6379
});
// 세션 설정
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: { secure: false, maxAge: 86400000 }
}));
// RabbitMQ 연결 설정
async function setupRabbitMQ() {
const connection = await amqp.connect('amqp://rabbitmq');
const channel = await connection.createChannel();
// 요청 큐 설정
await channel.assertQueue('request_queue', { durable: true });
return { connection, channel };
}
// API 엔드포인트 설정
app.post('/api/wallet/transfer', async (req, res) => {
try {
const { channel } = await setupRabbitMQ();
const { amount, toAddress } = req.body;
// 고유 상관 ID 생성
const correlationId = generateUuid();
// 응답 큐 설정
const { queue: replyQueue } = await channel.assertQueue('', { exclusive: true });
// 메시지 속성 설정
const message = {
amount,
toAddress,
userId: req.session.userId
};
// 메시지 발행
channel.sendToQueue('request_queue', Buffer.from(JSON.stringify(message)), {
correlationId,
replyTo: replyQueue
});
// 응답 대기
channel.consume(replyQueue, (msg) => {
if (msg.properties.correlationId === correlationId) {
const response = JSON.parse(msg.content.toString());
res.json(response);
channel.close();
}
}, { noAck: true });
} catch (error) {
console.error('Error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// 서버 시작
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
// 유틸리티 함수
function generateUuid() {
return Math.random().toString() + Math.random().toString();
}
|
RabbitMQ 워커 서비스 구현 예제
다음은 RabbitMQ를 통해 메시지를 수신하고 처리하는 비즈니스 서버의 예제 코드입니다:
const amqp = require('amqplib');
async function startWorker() {
try {
// RabbitMQ 연결
const connection = await amqp.connect('amqp://rabbitmq');
const channel = await connection.createChannel();
// 요청 큐 설정
const requestQueue = 'request_queue';
await channel.assertQueue(requestQueue, { durable: true });
// 프리페치 설정 (한 번에 처리할 메시지 수 제한)
channel.prefetch(1);
console.log('Worker started, waiting for messages...');
// 메시지 소비
channel.consume(requestQueue, async (msg) => {
if (msg) {
try {
console.log('Processing message...');
const content = JSON.parse(msg.content.toString());
// 비즈니스 로직 처리
const result = await processBusinessLogic(content);
// 응답 전송
channel.sendToQueue(
msg.properties.replyTo,
Buffer.from(JSON.stringify(result)),
{ correlationId: msg.properties.correlationId }
);
// 메시지 확인
channel.ack(msg);
} catch (error) {
console.error('Error processing message:', error);
// 오류 발생 시에도 메시지 확인 (선택적)
channel.ack(msg);
}
}
});
} catch (error) {
console.error('Worker startup error:', error);
}
}
async function processBusinessLogic(data) {
// 실제 비즈니스 로직 구현
// 예: 데이터베이스 조회, 계산 수행 등
// 샘플 응답
return {
status: 'success',
transactionId: generateTransactionId(),
timestamp: new Date().toISOString(),
data: {
amount: data.amount,
toAddress: data.toAddress,
processed: true
}
};
}
function generateTransactionId() {
return 'tx_' + Date.now() + '_' + Math.random().toString(36).substring(2, 15);
}
// 워커 시작
startWorker().catch(console.error);
|
확장성 고려사항
이 아키텍처를 대규모 시스템으로 확장할 때 고려해야 할 사항들:
- 서비스 디스커버리: 동적으로 서비스를 발견하고 연결하기 위한 메커니즘 필요 (예: Consul, etcd)
- 로드 밸런싱: 여러 서비스 인스턴스 간의 효율적인 부하 분산
- 서킷 브레이커 패턴: 장애 확산 방지를 위한 패턴 구현
- API 게이트웨이: 클라이언트 요청의 중앙 집중식 처리 및 라우팅
- 컨테이너 오케스트레이션: Kubernetes와 같은 도구를 사용한 서비스 관리
결론
RabbitMQ를 중심으로 한 마이크로서비스 아키텍처는 확장성과 유연성을 제공하지만, 복잡성과 운영 오버헤드가 증가하는 트레이드오프가 있습니다. 중소 규모 애플리케이션에서는 이러한 복잡성이 불필요할 수 있으나, 대규모 시스템에서는 서비스 분리와 비동기 통신이 제공하는 이점이 단점을 상쇄할 수 있습니다.
이 아키텍처는 특히 다음과 같은 상황에서 적합합니다:
- 높은 확장성이 요구되는 시스템
- 독립적으로 개발되고 배포되어야 하는 컴포넌트
- 장애 격리가 중요한 미션 크리티컬 애플리케이션
- 다양한 기술 스택을 사용하는 이기종 시스템
결국, 시스템 요구사항과 개발팀의 역량을 고려하여 적절한 아키텍처를 선택하는 것이 중요합니다. 마이크로서비스는 만능 해결책이 아니며, 특정 상황에서는 모놀리식 아키텍처가 더 적합할 수 있습니다.