Composite Destinations로 메시지를 분기할 때 트랜잭션 범위는?
엔터프라이즈 환경에서 메시지 브로커(Message Broker)를 활용하다 보면, 단일 시스템에서 발생한 이벤트를 여러 이기종 시스템으로 동시에 전파해야 하는 요구사항에 직면하게 됩니다. ActiveMQ와 같은 메시징 시스템은 이러한 요구를 해결하기 위해 Composite Destinations(복합 목적지)라는 강력한 기능을 제공합니다.
하나의 가상 목적지(Virtual Destination)로 메시지를 발행하면, 브로커가 이를 설정된 여러 개의 물리적 Queue나 Topic으로 알아서 복사 및 분기해 주는 편리한 기능입니다. 하지만 여기서 매우 중요한 아키텍처적 질문이 발생합니다.
"하나의 메시지가 여러 목적지로 쪼개어질 때, 트랜잭션(Transaction)은 도대체 어디서부터 어디까지 보장되는 것일까?"
이 글에서는 Composite Destinations를 사용할 때 생산자(Producer), 브로커(Broker), 소비자(Consumer) 관점에서 트랜잭션의 범위와 데이터 정합성이 어떻게 유지되는지 상세히 파헤쳐 보겠습니다.

1. Composite Destinations의 기본 동작 원리
트랜잭션을 논하기 전에 먼저 메시지가 어떻게 분기되는지 이해해야 합니다. ActiveMQ 설정 파일(activemq.xml)에 다음과 같이 가상 목적지를 설정했다고 가정해 봅시다.
- 가상 목적지 (입구):
MY.COMPOSITE.QUEUE - 물리적 목적지 (출구):
TARGET.QUEUE.A,TARGET.TOPIC.B
생산자는 오직 MY.COMPOSITE.QUEUE 하나에만 메시지를 보냅니다. 브로커는 이 메시지를 가로채어(Intercept) 내부적으로 TARGET.QUEUE.A와 TARGET.TOPIC.B 두 곳으로 메시지를 복사하여 전달합니다. 코드를 수정하지 않고도 인프라 설정만으로 메시지 라우팅을 구현할 수 있다는 것이 핵심 장점입니다.
2. 생산자(Producer) 관점의 트랜잭션 범위
가장 핵심적인 부분입니다. 생산자가 Session.SESSION_TRANSACTED 모드로 세션을 생성하고 MY.COMPOSITE.QUEUE로 메시지를 전송한 뒤 commit()을 호출하면 어떤 일이 벌어질까요?
결론부터 말씀드리면, 브로커 내부에서의 분기 작업은 하나의 거대한 원자적(Atomic) 작업으로 처리됩니다. 즉, 'All-or-Nothing'이 보장됩니다.
- 메시지 전송 및 커밋: 생산자가 트랜잭션을 커밋하면 브로커는 수신한 메시지를 영속소(DB나 파일 시스템 등 KahaDB)에 기록할 준비를 합니다.
- 브로커 내부 분기: 브로커는 Composite Destinations 설정에 따라 메시지를
TARGET.QUEUE.A와TARGET.TOPIC.B를 위한 저장 공간에 각각 기록하려고 시도합니다. - 트랜잭션 결과:
- 성공 (Commit): 두 목적지 모두에 메시지를 안전하게 저장하는 데 성공하면 생산자에게 커밋 성공 응답(ACK)을 보냅니다.
- 실패 (Rollback): 만약 디스크 공간 부족, 설정 오류 등으로 인해
TARGET.QUEUE.A에는 저장했지만TARGET.TOPIC.B에는 저장하지 못하는 상황이 발생한다면, 브로커는 전체 트랜잭션을 롤백시킵니다. 즉,TARGET.QUEUE.A에 들어갔던 메시지도 취소되며 생산자에게는 예외가 발생합니다.
따라서 생산자 입장에서는 "내가 보낸 메시지가 모든 타겟 데스티네이션에 완벽하게 도달했거나, 아니면 아예 어느 곳에도 도달하지 않았다"는 강력한 정합성을 신뢰할 수 있습니다.
3. 소비자(Consumer) 관점의 트랜잭션 범위
생산자에서 브로커까지의 전달이 완벽하게 완료되어 각 타겟 큐와 토픽에 메시지가 적재되었다면, 이제 소비자들의 차례입니다. 여기서 기억해야 할 절대 원칙은 다음과 같습니다.
분기된 이후 각 소비자의 트랜잭션은 철저하게 독립적(Independent)입니다.
- 소비자 A (TARGET.QUEUE.A 구독): 메시지를 정상적으로 수신하고 비즈니스 로직을 처리한 뒤 DB에 커밋하고 메시지를
ACK처리했습니다. 트랜잭션이 성공적으로 종료되었습니다. - 소비자 B (TARGET.TOPIC.B 구독): 동일한 메시지를 수신하여 처리하던 중 NullPointerException이 발생했습니다. 메시지 소비 트랜잭션은 롤백(Rollback)되고 메시지는 재처리(Redelivery) 큐나 DLQ(Dead Letter Queue)로 이동합니다.
소비자 B의 실패와 롤백은 소비자 A의 성공에 아무런 영향도 미치지 않습니다. 분기점(Broker)을 지나는 순간 메시지는 완전히 별개의 생명주기를 가진 복제본이 되기 때문입니다. 이는 시스템 간의 결합도를 낮추고 장애를 격리(Fault Isolation)하는 마이크로서비스 아키텍처(MSA)의 핵심 원칙과도 잘 부합합니다.
4. 주의해야 할 엣지 케이스 (Edge Cases)
이론적으로 완벽해 보이지만, 실제 운영 환경에서는 몇 가지 주의해야 할 예외 상황이 존재합니다.
A. 브로커의 메모리 및 디스크 리소스 초과
만약 TARGET.QUEUE.A에는 메시지가 계속 쌓여서 설정된 큐 메모리 한도(Memory Limit)를 꽉 채웠고, TARGET.TOPIC.B는 텅 비어있다고 가정해 봅시다. 생산자가 Composite Destination으로 메시지를 보낼 때, ActiveMQ의 Producer Flow Control 기능이 켜져 있다면 생산자의 send()나 commit() 메소드는 큐 A의 공간이 확보될 때까지 무한정 블로킹(Blocking)되거나 타임아웃 예외를 발생시킵니다.
즉, 단 하나의 느린 소비자(Slow Consumer)나 타겟 큐의 병목이 전체 분기 트랜잭션(생산자의 발행)을 지연시킬 수 있다는 점을 아키텍처 설계 시 반드시 고려해야 합니다.
B. 토픽(Topic) 타겟의 영속성 문제
분기 대상 중 하나가 Topic일 경우, Topic의 특성을 주의해야 합니다. 트랜잭션이 완벽히 커밋되어 타겟 Topic으로 메시지가 복사되었더라도, 해당 시점에 연결된 Durable Subscriber(영속성 구독자)가 없다면 JMS 스펙에 따라 그 메시지는 허공으로 날아가 버립니다. 이는 트랜잭션의 실패가 아니라 Topic이라는 자료구조의 본질적인 특성입니다. 유실을 막으려면 반드시 구독자를 Durable 상태로 연결해야 합니다.
5. 요약 및 마무리
Composite Destinations를 활용한 메시지 분기 시 트랜잭션 범위는 다음과 같이 요약할 수 있습니다.
- Producer $\rightarrow$ Broker (입력 구간): 단일 트랜잭션으로 묶입니다. 모든 타겟 목적지로의 복사 및 저장이 원자적(Atomic)으로 보장됩니다.
- Broker $\rightarrow$ Consumers (출력 구간): 각 물리적 목적지에 연결된 소비자들의 트랜잭션은 철저히 독립적으로 동작하며 서로에게 영향을 주지 않습니다.
이러한 특성을 이해하면, 데이터의 일관성을 잃지 않으면서도 레거시 시스템과 신규 시스템에 동시에 이벤트를 브로드캐스팅하는 등 유연하고 견고한 아키텍처를 설계할 수 있습니다.
성공적인 메시징 기반 시스템 구축을 위해서는 단순한 라우팅 설정을 넘어, 장애 발생 시나리오와 각 컴포넌트 간의 트랜잭션 경계를 명확히 그리는 것이 무엇보다 중요합니다.
'1. 개발 > 1.8. ActiveMQ' 카테고리의 다른 글
| STOMP 프로토콜이 웹소켓(WebSocket)과 궁합이 좋은 이유는? (0) | 2026.03.14 |
|---|---|
| JMS와 AMQP 프로토콜 간의 타입 매핑 방식은? (0) | 2026.03.13 |
| 가상 토픽 사용 시 물리적 큐의 명명 규칙은? (0) | 2026.03.11 |
| 가상 토픽(Virtual Topics)이 팬아웃(Fan-out) 성능을 올리는 원리는? (0) | 2026.03.10 |
| Topic 모델에서 '지속성 구독자(Durable Subscriber)'의 원리는? (0) | 2026.03.10 |