RabbitMQ와 마이크로서비스를 활용한 확장 가능한 백엔드 아키텍처 설계

소개

현대적인 웹 애플리케이션은 높은 확장성, 유연성, 그리고 견고성을 갖춘 백엔드 아키텍처가 필요합니다. 이 글에서는 진행했던 프로젝트 중 RabbitMQ를 중심으로 한 마이크로서비스 기반 시스템 아키텍처를 분석하고, 이러한 설계의 장단점을 심층적으로 살펴보겠습니다.

아키텍처 개요

다음은 RabbitMQ를 메시지 브로커로 활용하여 여러 서비스 간의 통신을 관리하는 마이크로서비스 아키텍처의 전체 구성도입니다:

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

  • 시스템의 주요 데이터 저장소
  • 분산 데이터베이스로 확장성 제공
  • 문서 기반 데이터 모델 사용
  • 레플리카셋 및 샤딩 지원

통신 흐름 분석

이 아키텍처의 통신 흐름은 다음과 같습니다:

  1. 클라이언트가 웹 서버에 요청을 보냅니다.
  2. 웹 서버는 Express 프레임워크를 사용하여 요청을 처리하고, 필요한 경우 세션 정보를 Redis에서 조회합니다.
  3. 웹 서버는 RabbitMQ 브로커를 통해 적절한 마이크로서비스(비즈니스 서버, 지갑중계, DB AGENT)에 메시지를 전달합니다.
  4. 각 마이크로서비스는 요청을 처리하고 결과를 RabbitMQ의 REPLY 큐를 통해 반환합니다.
  5. 웹 서버는 응답을 수신하여 클라이언트에게 최종 결과를 전달합니다.

이 과정에서 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);

확장성 고려사항

이 아키텍처를 대규모 시스템으로 확장할 때 고려해야 할 사항들:

  1. 서비스 디스커버리: 동적으로 서비스를 발견하고 연결하기 위한 메커니즘 필요 (예: Consul, etcd)
  2. 로드 밸런싱: 여러 서비스 인스턴스 간의 효율적인 부하 분산
  3. 서킷 브레이커 패턴: 장애 확산 방지를 위한 패턴 구현
  4. API 게이트웨이: 클라이언트 요청의 중앙 집중식 처리 및 라우팅
  5. 컨테이너 오케스트레이션: Kubernetes와 같은 도구를 사용한 서비스 관리

결론

RabbitMQ를 중심으로 한 마이크로서비스 아키텍처는 확장성과 유연성을 제공하지만, 복잡성과 운영 오버헤드가 증가하는 트레이드오프가 있습니다. 중소 규모 애플리케이션에서는 이러한 복잡성이 불필요할 수 있으나, 대규모 시스템에서는 서비스 분리와 비동기 통신이 제공하는 이점이 단점을 상쇄할 수 있습니다.

이 아키텍처는 특히 다음과 같은 상황에서 적합합니다:

  • 높은 확장성이 요구되는 시스템
  • 독립적으로 개발되고 배포되어야 하는 컴포넌트
  • 장애 격리가 중요한 미션 크리티컬 애플리케이션
  • 다양한 기술 스택을 사용하는 이기종 시스템

결국, 시스템 요구사항과 개발팀의 역량을 고려하여 적절한 아키텍처를 선택하는 것이 중요합니다. 마이크로서비스는 만능 해결책이 아니며, 특정 상황에서는 모놀리식 아키텍처가 더 적합할 수 있습니다.