MSA 확장성을 결정짓는 메시지 브로커 선정 및 설계 전략

마이크로서비스 아키텍처(MSA)의 핵심은 서비스 간의 '결합도(Coupling)'를 낮추는 것입니다. 서비스가 증가함에 따라 HTTP 기반의 동기(Synchronous) 통신만으로는 트래픽 폭주 시 시스템 전체의 장애 전파(Cascading Failure)를 막기 어렵습니다.

이벤트 기반 아키텍처(Event-Driven Architecture, EDA)는 비동기 메시징을 통해 생산자와 소비자를 분리함으로써 시스템의 탄력성과 확장성을 보장합니다. 하지만 모든 상황에 Kafka가 정답은 아닙니다. 비즈니스 요구사항에 따라 RabbitMQ나 AWS SQS가 더 효율적인 선택이 될 수 있습니다.

본고에서는 Kafka, RabbitMQ, AWS SQS의 아키텍처 차이점을 기술적으로 분석하고, 각 시나리오에 적합한 메시지 브로커 선정 기준과 Spring Boot 기반의 구현 패턴을 다룹니다.

Kafka vs RabbitMQ vs AWS SQS 아키텍처 심층 비교

메시지 브로커를 선택할 때 가장 먼저 이해해야 할 것은 각 솔루션의 데이터 처리 방식입니다. Kafka는 '로그 기반(Log-based)' 스토리지 시스템에 가깝고, RabbitMQ는 전통적인 '메시지 큐'의 라우팅 로직에 강점이 있습니다.

비교 항목 Apache Kafka RabbitMQ AWS SQS
설계 철학 대용량 로그 스트리밍 및 파이프라인 복잡한 라우팅 및 메시지 브로커 완전 관리형 서버리스 큐
메시지 영속성 디스크에 영구 저장 (보존 정책 따름) 소비 후 삭제 (메모리 위주) 설정된 기간 후 자동 삭제
전송 모델 Pull 모델 (Consumer가 주도) Push 모델 (Broker가 주도) Pull 모델 (Polling)
처리량 (Throughput) 초당 수십~수백만 건 (배치 처리에 최적화) 초당 수만 건 (개별 메시지 응답 중시) 무제한 확장이 가능하나 비용 고려 필요
복잡도 높음 (Zookeeper/KRaft, 파티션 관리) 중간 (Exchange, Queue 바인딩) 매우 낮음 (API 호출 방식)

Kafka는 'Smart Endpoint, Dumb Pipe' 철학을 따릅니다. 브로커는 단순히 메시지를 저장하고, 소비자가 오프셋(Offset)을 관리하며 데이터를 읽어갑니다. 이로 인해 이벤트 소싱(Event Sourcing)이나 데이터 재처리(Replay)가 필요한 환경에 적합합니다.

반면 RabbitMQ는 'Smart Broker' 방식을 취합니다. Exchange 내의 복잡한 라우팅 키를 통해 메시지를 특정 큐로 정확히 전달하며, 메시지 전달 보장(Delivery Guarantee)이 중요한 트랜잭션 처리에 유리합니다.

비즈니스 시나리오별 기술 선정 가이드

단순히 성능 지표만 보고 도구를 선택해서는 안 됩니다. 운영 복잡도와 팀의 숙련도, 그리고 데이터의 성격을 종합적으로 고려해야 합니다.

1. 실시간 로그 집계 및 데이터 스트리밍 (Kafka)

사용자 행동 로그(Clickstream), IoT 센서 데이터 등 유실되어도 치명적이지 않지만 엄청난 양의 데이터가 지속적으로 유입되는 경우 Kafka가 압도적입니다. 또한, 동일한 데이터를 여러 서비스(분석팀, 마케팅팀, 백업 시스템)가 각기 다른 속도로 소비해야 한다면 Kafka의 Consumer Group 기능이 필수적입니다.

2. 복잡한 작업 분배 및 트랜잭션 처리 (RabbitMQ)

주문 처리 후 이메일 발송, 결제 후 재고 차감 등 순서가 중요하거나 메시지 별로 우선순위 처리가 필요한 경우 RabbitMQ를 권장합니다. 특히 라우팅 키를 활용해 특정 메시지만 특정 워커에 할당하는 유연한 구성이 가능합니다.

3. 인프라 관리 없는 빠른 개발 (AWS SQS)

초기 스타트업이나 트래픽 예측이 불가능한 경우 SQS와 SNS를 조합하여 사용하는 것이 효율적입니다. 인프라를 프로비저닝할 필요가 없으며, AWS Lambda와 결합하면 완전한 서버리스 아키텍처를 구축할 수 있습니다.

💡 Check Point: 이벤트 소싱(Event Sourcing)
시스템의 상태 변경을 모두 이벤트로 저장하고, 이를 통해 언제든 과거 특정 시점의 상태로 복원해야 한다면 Kafka가 유일한 대안입니다. 메시지를 디스크에 보존하고 재생(Replay)할 수 있기 때문입니다.

Spring Boot 기반 구현 패턴 비교

개발 관점에서 두 기술의 구현 차이를 살펴보겠습니다. 설정의 복잡도와 메시지를 발행(Publish)하는 방식에서 차이가 드러납니다.

Kafka Producer 설정 (Batching 중심)

Kafka는 네트워크 오버헤드를 줄이기 위해 메시지를 배치(Batch)로 묶어서 전송하는 설정을 튜닝하는 것이 성능의 핵심입니다.

// KafkaProducerConfig.java
@Bean
public ProducerFactory<String, String> producerFactory() {
    Map<String, Object> configProps = new HashMap<>();
    configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    
    // 성능 최적화 핵심 설정
    configProps.put(ProducerConfig.LINGER_MS_CONFIG, 10); // 10ms 대기 후 배치 전송
    configProps.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); // 16KB 배치 크기
    
    return new DefaultKafkaProducerFactory<>(configProps);
}

RabbitMQ Configuration (Routing 중심)

RabbitMQ는 Exchange와 Queue를 정의하고, 이들을 Binding하는 설정이 주를 이룹니다. 메시지가 어디로 갈지 명확히 정의해야 합니다.

// RabbitMqConfig.java
@Bean
public DirectExchange exchange() {
    return new DirectExchange("order.exchange");
}

@Bean
public Queue queue() {
    return new Queue("order.queue", true); // Durable 설정
}

@Bean
public Binding binding(DirectExchange exchange, Queue queue) {
    // Routing Key 'order.created'와 바인딩
    return BindingBuilder.bind(queue).to(exchange).with("order.created");
}

안정적인 운영을 위한 트러블슈팅 전략

비동기 시스템에서 가장 큰 문제는 '메시지 처리 실패'와 '중복 처리'입니다. 이를 해결하기 위한 표준 패턴을 반드시 적용해야 합니다.

1. Dead Letter Queue (DLQ) 설계

메시지 소비 중 에러가 발생했을 때, 무한정 재시도(Retry)를 하게 되면 전체 시스템의 병목을 유발합니다. 일정 횟수 이상 실패한 메시지는 별도의 DLQ로 이동시켜 격리해야 합니다. 이후 운영자가 원인을 분석하거나 별도의 배치 잡으로 재처리합니다.

  • RabbitMQ: x-dead-letter-exchange 속성을 통해 설정 가능합니다.
  • Kafka: 기본 기능이 아니므로 애플리케이션 레벨에서 예외 발생 시 다른 토픽으로 발행하는 로직을 구현하거나, Spring Cloud Stream의 DLQ 기능을 활용해야 합니다.

2. 멱등성(Idempotency) 보장

네트워크 이슈로 인해 Producer가 메시지를 중복 발송하거나, Consumer가 ACK를 보내지 못해 메시지를 다시 받는 경우가 빈번합니다. 따라서 Consumer는 동일한 메시지가 여러 번 오더라도 한 번만 처리되도록 설계해야 합니다.

⚠️ 주의: Exactly-Once Semantics
Kafka가 '정확히 한 번 전송'을 지원한다고 하지만, 이는 트랜잭션 설정과 성능 저하를 동반합니다. 실무에서는 일반적으로 '적어도 한 번(At-Least-Once)' 전송을 기본으로 하고, Consumer 내부에서 DB의 Unique Key나 Redis를 활용하여 중복 처리를 막는 것이 훨씬 효율적입니다.

결론: 아키텍처에 맞는 도구 선택

Kafka는 데이터 파이프라인과 대용량 처리에, RabbitMQ는 복잡한 라우팅이 필요한 마이크로서비스 간 통신에 강점이 있습니다. AWS SQS는 관리 비용을 최소화하고 싶을 때 최적의 선택입니다.

하나의 기술로 모든 것을 해결하려 하지 마십시오. 메인 데이터 파이프라인은 Kafka로 구축하고, 서비스 간의 트랜잭션 이벤트 알림은 RabbitMQ나 SQS를 사용하는 하이브리드 패턴도 훌륭한 전략입니다. 현재 팀의 운영 역량과 비즈니스의 데이터 성격을 파악하여 최적의 조합을 설계하시기 바랍니다.

Post a Comment