콘텐츠로 이동

메시지 브로커: 필요성, 특징 및 시스템 설계의 도전

1. 직접 네트워크 통신(Direct Communication)의 한계

직접 통신은 TCP 연결을 처음부터 끝까지 유지해야 하는 동기적 특성 때문에 세 가지 치명적인 문제를 가집니다.

  • 연쇄적 종속성: 서비스 A→B→C→D 중 하나만 응답이 늦거나 장애가 나도 전체 트랜잭션이 실패하며, 사용자에게 잘못된 응답을 보낼 위험이 커집니다.
  • 확장성(Scalability) 부족: 1:N 통신 시 발행자가 모든 수신자와 직접 연결을 맺어야 하므로 연결 관리에 한계가 옵니다.
  • 트래픽 서지 대응 불가: 이벤트 유입 속도가 처리 속도를 넘어서면 백엔드 서비스가 중단됩니다.

2. 메시지 브로커의 역할과 핵심 기능

메시지 브로커는 송신자와 수신자 사이의 종속성을 완전히 제거하는 중개자입니다.

  • 비동기 시스템 분리: 큐잉(Queuing)을 통해 동기식 구조를 비동기식으로 전환하며 메시지 전달을 보장합니다.
  • 제어권의 역전 (Pulling): 수신자가 원할 때 메시지를 가져가는 방식을 통해, 서버가 메시지 처리 속도와 빈도를 직접 제어할 수 있게 합니다.
  • 발행/구독(Pub/Sub) 모델: 송신자가 수신자를 몰라도 브로커를 통해 여러 서버에 이벤트를 동시에 전달할 수 있습니다.

3. 메시지 브로커 설계의 핵심: 분산 시스템으로서의 요건

  • 단일 장애 지점(SPOF) 방지: 브로커가 중단되면 전체 시스템의 통신이 마비되므로, 브로커 자체가 내결함성(Fault Tolerance)을 갖춘 분산 시스템으로 구성되어야 합니다.
  • 확장성 확보: 시스템 전체의 병목 지점이 되지 않도록 브로커 또한 부하에 따라 확장(Scalability) 가능해야 합니다.
  • 설계의 난이도: 이러한 내결함성과 확장성을 동시에 갖춘 메시지 브로커를 설계하고 구성하는 일은 매우 어렵고 복잡한 작업입니다.

4. 도입 시의 트레이드오프 (Trade-offs)

기술 선택에는 반드시 얻는 것과 잃는 것이 있습니다.

  • 이득: 시스템 간 결합도 제거, 안정적인 큐잉, 처리 제어권 확보, 1:N 확장성.
  • 비용: 직접 연결보다 높은 지연 시간(Latency) 발생, 브로커 인프라 관리의 복잡성 및 설계 비용.

연쇄적 종속성은 해결하면서 트랜잭션의 원자성을 지켜야 한다면?

"하나라도 실패하면 전체가 실패해야 하는 상황"은 데이터의 정합성이 생명인 금융 결제나 재고 관리에서 매우 빈번하게 발생합니다. 이를 원자성(Atomicity)이라고 하죠.

강의에서 언급한 '직접 통신의 한계'는 "모든 서비스가 살아있고 응답할 때까지 사용자를 무한정 대기하게 만드는 동기적 방식의 위험성"을 지적한 것이지, 실패해도 무시하라는 뜻은 아닙니다. 이런 경우 분산 시스템에서는 다음과 같은 설계 패턴들을 사용합니다.


1. 2단계 커밋 (2-Phase Commit, 2PC)

전통적인 분산 트랜잭션 방식입니다. 모든 서비스가 "준비 완료"를 외쳐야만 최종적으로 데이터를 확정(Commit)합니다.

  • 동작 방식: 중앙의 트랜잭션 관리자(Coordinator)가 모든 서비스에 "저장할 준비 됐니?"라고 묻고(Prepare), 모두가 "Yes"라고 하면 그때 "확정해!"(Commit)라고 명령합니다. 하나라도 "No" 하거나 응답이 없으면 모두 "취소해!"(Rollback)라고 합니다.
  • 문제점: 모든 서비스가 응답할 때까지 자원을 잠그기(Lock) 때문에 성능이 매우 떨어지고, 관리자가 죽으면 시스템 전체가 마비됩니다. 강의에서 말한 '연쇄적 종속성'의 전형적인 단점이 나타납니다.

2. 사가 패턴 (Saga Pattern)

최근 마이크로서비스 아키텍처(MSA)에서 가장 많이 쓰이는 방식입니다. 전체를 하나의 큰 트랜잭션으로 묶는 대신, 로컬 트랜잭션의 연속으로 처리합니다.

  • 보상 트랜잭션 (Compensating Transaction): 만약 서비스 C에서 실패했다면, 이미 성공한 A와 B에게 "방금 했던 거 취소해!"라는 요청을 보내서 원래 상태로 되돌립니다. (예: 결제 취소, 재고 복구)
  • 메시지 브로커 활용: 이때 서비스 간의 취소 명령을 확실히 전달하기 위해 메시지 브로커를 사용합니다. 브로커는 서비스 A가 잠시 죽어있더라도 "취소해"라는 메시지를 보관했다가 나중에라도 반드시 전달하여 결국 데이터 상태를 맞춥니다(최종적 정합성).

3. 아웃박스 패턴 (Transactional Outbox Pattern)

메시지 브로커를 쓸 때, "DB 저장은 성공했는데 브로커에 메시지 보내는 걸 실패"하는 상황을 막기 위한 설계입니다.

  • 방법: 서비스의 로컬 DB 안에 'Outbox'라는 테이블을 만듭니다. 비즈니스 데이터(주문 내역 등)를 저장할 때, 보낼 메시지도 같은 DB 트랜잭션 안에 넣습니다.
  • 이점: DB 저장이 성공하면 메시지도 반드시 저장됩니다. 이후 별도의 프로세스가 Outbox 테이블을 읽어 브로커로 전달하므로, "전체 프로세스의 시작"은 확실히 보장됩니다.

요약: 무엇을 선택해야 할까?

방식 특징 추천 상황
동기식 직접 연결 구현이 가장 쉬움 서비스 개수가 적고 지연 시간이 짧을 때
2PC 데이터 정합성이 완벽함 성능보다 데이터 정확도가 압도적으로 중요할 때
Saga 패턴 확장성이 좋고 빠름 복잡한 MSA 환경에서 고성능이 필요할 때

결국 "사용자를 계속 붙잡아두고 동기적으로 실패를 처리할 것인가(Direct)", 아니면 "사용자에게는 접수되었다고 알리고 백엔드에서 메시지 브로커와 Saga 패턴으로 끝까지 정합성을 맞출 것인가(Async)"의 차이입니다.

현대적인 대규모 시스템에서는 후자(비동기+Saga)를 선택하여 시스템의 가용성을 높이는 것이 일반적입니다.