본문 바로가기
Spring 7기 프로젝트/모임 플렛폼 프로젝트

모놀리식 아키텍처에서 MSA로의 전환: ALB를 활용한 서비스 분리 과정

by JuNo_12 2025. 7. 31.

들어가며

현재 운영 중인 모임 플랫폼 'MOMO'는 모놀리식 아키텍처로 구성되어 있지만, 이미 MSA 전환을 고려한 설계가 적용되어 있습니다. WebClient를 통한 서비스 간 HTTP 통신과 Spring Event 기반의 비동기 처리가 구현되어 있어, RabbitMQ와 ALB를 활용한 완전한 MSA 전환의 기반이 마련되어 있습니다.


현재 시스템 구조 분석

이미 구현된 MSA 준비 요소들

현재 코드베이스에서 확인할 수 있는 MSA 준비 상태:

1. WebClient 기반 서비스 간 통신

 

2. 도메인별 Event 분리


MSA 전환 전략

1단계: 메시지 큐 도입 (RabbitMQ)

현재 Spring Event 기반의 비동기 처리를 RabbitMQ로 전환합니다:

현재 방식 (Spring Event)

@EventListener
public void handleMeetingJoinEvent(MeetingEvents.Join event) {
    // 알림 처리 로직
}

 

RabbitMQ 전환 후

// Producer (Meeting Service)
@Service
public class MeetingEventPublisher {
    private final RabbitTemplate rabbitTemplate;
    
    public void publishMeetingJoinEvent(MeetingEvents.Join event) {
        rabbitTemplate.convertAndSend("meeting.exchange", "meeting.join", event);
    }
}

// Consumer (Notification Service)
@RabbitListener(queues = "meeting.join.queue")
public void handleMeetingJoinEvent(MeetingEvents.Join event) {
    // 알림 처리 로직
}

 

2단계: Exchange와 Queue 설계

도메인별 이벤트에 맞는 RabbitMQ 토폴로지 구성:

Meeting Events:
├── meeting.exchange (Topic Exchange)
├── meeting.create.queue → meeting.create
├── meeting.join.queue → meeting.join
├── meeting.cancel.queue → meeting.cancel
└── meeting.delete.queue → meeting.delete

User Events:
├── user.exchange (Topic Exchange)
├── user.followed.queue → user.followed
├── user.registered.queue → user.registered
└── user.withdrawn.queue → user.withdrawn

Payment Events:
├── payment.exchange (Topic Exchange)
├── payment.completed.queue → payment.completed
└── payment.refunded.queue → payment.refunded

 

3단계: ALB 라우팅 설계 (기존 WebClient 활용)

현재 WebClient 설정을 그대로 활용하되, ALB를 통한 라우팅으로 변경:

// 현재 WebClient 설정 (이미 구현됨)
@Configuration
public class WebClientConfig {
    @Bean
    public WebClient webClient() {
        return WebClient.builder()
            .baseUrl("http://localhost:8080") // ALB 엔드포인트로 변경
            .defaultHeader("WebclientInternal", passwordEncoder.encode(webSecretKey))
            .build();
    }
}

 

ALB 라우팅 규칙

ALB 엔드포인트: https://api.momo.com
├── /api/v2/users/*     → User Service (Target Group 1)
├── /api/v2/meetings/*  → Meeting Service (Target Group 2)
├── /api/v2/payments/*  → Payment Service (Target Group 3)
├── /api/v2/notifications/* → Notification Service (Target Group 4)
└── /api/v2/categories/* → Category Service (Target Group 5)

 

4단계: 서비스별 분리 및 독립 배포

각 서비스별 Docker 이미지 구성

# User Service
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY user-service/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

# Meeting Service  
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY meeting-service/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

 

Docker Compose로 전체 서비스 구성

version: '3.8'
services:
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      RABBITMQ_DEFAULT_USER: momo
      RABBITMQ_DEFAULT_PASS: password

  user-service:
    image: momo-user-service:latest
    ports:
      - "8081:8080"
    environment:
      - DB_URL=jdbc:mysql://user-db:3306/user_db
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - rabbitmq
      - user-db

  meeting-service:
    image: momo-meeting-service:latest
    ports:
      - "8082:8080"
    environment:
      - DB_URL=jdbc:mysql://meeting-db:3306/meeting_db
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - rabbitmq
      - meeting-db

 

5단계: 이벤트 기반 통신 패턴 적용

1. Saga Pattern 구현 (결제 + 참가자 추가)

// Meeting Service
@Service
public class MeetingParticipantSaga {
    
    @RabbitListener(queues = "payment.completed.queue")
    public void handlePaymentCompleted(PaymentCompletedEvent event) {
        try {
            // 참가자 추가 시도
            meetingService.addParticipant(event.getMeetingId(), event.getUserId());
            
            // 성공 이벤트 발행
            publishMeetingJoinEvent(event);
        } catch (Exception e) {
            // 실패 시 보상 트랜잭션 이벤트 발행
            publishPaymentRefundEvent(event);
        }
    }
}

 

2. CQRS Pattern 적용 (알림 시스템)

// Command: Notification Service에서 알림 저장
@RabbitListener(queues = "notification.create.queue")
public void handleCreateNotification(NotificationDto dto) {
    notificationRepository.save(dto.toEntity());
}

// Query: WebSocket을 통한 실시간 전송
@RabbitListener(queues = "notification.send.queue")
public void handleSendNotification(NotificationDto dto) {
    webSocketNotificationService.send(
        WebSocketNotificationDto.builder()
            .userId(dto.getUserId())
            .content(dto.getContent())
            .build()
    );
}

 

6단계: 데이터베이스 분리

현재 Flyway 마이그레이션을 각 서비스별로 분리:

-- User Service Database
V1__Create_users_table.sql
V2__Create_categories_table.sql
V3__Create_user_categories_table.sql
V4__Create_user_follow_table.sql
V5__Create_user_ratings_table.sql
V8__Create_user_social_table.sql

-- Meeting Service Database  
V6__Create_meetings_table.sql
V7__Create_meeting_participants_table.sql

-- Payment Service Database
V9__Create_payments_table.sql

-- Notification Service Database
V10__Create_notifications_table.sql

RabbitMQ 메시지 플로우

1. 모임 참가 프로세스

1. Meeting Service → payment.register.queue (결제 요청)
2. Payment Service → payment.completed.queue (결제 완료)
3. Meeting Service → meeting.join.queue (참가자 추가)
4. Notification Service → notification.send.queue (알림 전송)

 

2. 팔로우 알림 프로세스

1. User Service → user.followed.queue (팔로우 이벤트)
2. Notification Service → notification.create.queue (알림 저장)
3. Notification Service → notification.send.queue (실시간 전송)

장애 처리 및 복원력

1. Dead Letter Queue 설정

@Bean
public Queue meetingJoinQueue() {
    return QueueBuilder.durable("meeting.join.queue")
        .withArgument("x-dead-letter-exchange", "meeting.dlx")
        .withArgument("x-dead-letter-routing-key", "meeting.join.dlq")
        .build();
}

 

2. Retry 메커니즘

@RabbitListener(queues = "meeting.join.queue")
@Retryable(value = {Exception.class}, maxAttempts = 3)
public void handleMeetingJoinEvent(MeetingEvents.Join event) {
    // 처리 로직
}

 

3. Circuit Breaker Pattern

@Component
public class UserClient {
    
    @CircuitBreaker(name = "user-service")
    public UserClientResponseDto getUser(Long userId) {
        return webClient.get()
            .uri("/api/v2/users/{userId}", userId)
            .retrieve()
            .bodyToMono(UserClientResponseDto.class)
            .block();
    }
}

모니터링 및 추적

1. RabbitMQ 메시지 추적

@Component
public class MessageTracker {
    
    @EventListener
    public void handleMessageSent(MessageSentEvent event) {
        log.info("Message sent to queue: {}, correlationId: {}", 
            event.getQueue(), event.getCorrelationId());
    }
}

 

2. 분산 트레이싱 (이미 구현된 OpenTelemetry 확장)

# otel-config.yml에 RabbitMQ 추가
receivers:
  otlp:
    protocols:
      grpc:
      http:
  rabbitmq:
    endpoint: http://rabbitmq:15672

단계별 마이그레이션 계획

Phase 1: 메시지 큐 도입

  1. RabbitMQ 인프라 구성
  2. Spring Event를 RabbitMQ로 점진적 전환
  3. 기존 WebClient 통신 방식 유지

Phase 2: 서비스 물리적 분리

  1. 가장 독립적인 Category Service부터 분리
  2. Payment Service 분리 (토스페이먼츠 연동 포함)
  3. Notification Service 분리 (WebSocket 포함)

Phase 3: 핵심 서비스 분리

  1. User Service 분리 (인증/인가 포함)
  2. Meeting Service 분리
  3. ALB를 통한 통합 라우팅 적용

Phase 4: 최적화 및 안정화

  1. 서비스 간 통신 최적화
  2. 장애 복구 메커니즘 강화
  3. 성능 튜닝 및 모니터링 고도화

예상 효과

이미 구현된 WebClient 기반 통신과 이벤트 기반 아키텍처를 바탕으로:

  • 점진적 전환: 기존 코드의 큰 변경 없이 단계적 MSA 전환 가능
  • 메시지 기반 느슨한 결합: RabbitMQ를 통한 서비스 간 비동기 통신으로 시스템 복원력 향상
  • 수평적 확장: 각 서비스별 독립적인 스케일링으로 리소스 효율성 극대화
  • 장애 격리: 메시지 큐와 Circuit Breaker를 통한 장애 전파 방지

현재 MOMO 프로젝트는 이미 MSA 전환을 위한 핵심 요소들이 잘 구현되어 있어, RabbitMQ 도입과 ALB 설정만으로도 비교적 안정적인 MSA 전환이 가능할 것으로 예상됩니다.