본문 바로가기
1. 개발/1.8. ActiveMQ

XA 트랜잭션 시 'Prepared' 상태의 메시지가 브로커에 남았을 때 해결법은?

by 엉짱 2026. 3. 27.
반응형

XA 트랜잭션 시 'Prepared' 상태의 메시지가 브로커에 남았을 때 해결법은?

엔터프라이즈 마이크로서비스(MSA) 환경이나 레거시 시스템에서 서로 다른 데이터베이스와 메시지 브로커의 데이터 정합성을 완벽하게 맞추기 위해 가장 고전적이고 강력하게 사용되는 기술이 바로 XA 트랜잭션(분산 트랜잭션)입니다.

XA 트랜잭션은 트랜잭션 매니저(TM, Transaction Manager)의 지휘 아래 2PC(Two-Phase Commit, 2단계 커밋) 방식으로 동작합니다. 하지만 이 거대한 아키텍처에는 치명적인 약점이 존재합니다. 트랜잭션 매니저가 장애를 일으키면, 브로커 내부에 처리되지도 폐기되지도 못한 'Prepared(준비됨)' 상태의 좀비 메시지들이 무한정 남게 된다는 것입니다.

이 가이드에서는 브로커 내부에 악성 종양처럼 자리 잡은 'Prepared' 상태의 트랜잭션(In-doubt Transaction)이 발생하는 근본적인 원인과, 이를 안전하게 감지하고 수동 또는 자동으로 해결하는 트러블슈팅 전략을 상세히 분석합니다.


1. 2PC 아키텍처와 'Prepared' 상태의 덫

XA 트랜잭션이 메시지 브로커(예: ActiveMQ)와 데이터베이스를 묶어서 처리할 때, 2단계 커밋은 다음과 같이 진행됩니다.

  • Phase 1 (Prepare - 준비 단계): 트랜잭션 매니저(TM)가 브로커와 DB에게 "데이터를 기록할 준비를 하라"고 명령합니다. 이때 브로커는 메시지를 저널에 기록하고 락(Lock)을 걸어둔 채, TM에게 "준비 완료(Prepared)" 응답을 보냅니다.
  • Phase 2 (Commit - 커밋 단계): 모든 자원이 준비되었다고 응답하면, TM은 최종적으로 "커밋(Commit)" 명령을 내려 트랜잭션을 확정 짓습니다.

장애 발생 시나리오 (좀비 상태의 탄생):
브로커가 Phase 1을 마치고 'Prepared' 상태로 대기 중인데, 하필 그 찰나의 순간에 트랜잭션 매니저(TM) 서버의 전원이 차단되거나 네트워크 케이블이 단절되었다고 가정해 보겠습니다.
브로커는 TM으로부터 최종 커밋 명령을 받지도, 롤백 명령을 받지도 못한 채 낙동강 오리알 신세가 됩니다. 이 상태의 트랜잭션을 'In-doubt(의심스러운) 트랜잭션'이라고 부르며, 브로커 내부에서는 영원히 'Prepared' 상태로 잠들어버립니다.


2. 'Prepared' 메시지가 인프라에 미치는 치명적 영향

이 좀비 메시지들은 단순히 큐에 안 보이는 수준을 넘어 시스템 자원을 심각하게 갉아먹습니다.

  • 리소스 점유 (Resource Hold): 브로커는 이 메시지가 언제 커밋될지 모르기 때문에 메모리와 디스크의 트랜잭션 저널 공간을 계속 점유하고 있어야 합니다. 가비지 컬렉션(GC)이나 저널 컴팩션(Compaction) 대상에서도 영구히 제외되어 디스크 고갈 장애를 유발합니다.
  • 락 경합 (Lock Contention): 엄격한 순서 보장이 필요한 큐(Exclusive Queue) 환경에서는, 맨 앞에 있는 메시지가 Prepared 상태로 멈춰있으면 뒤따라오는 수천 개의 정상 메시지들조차 라우팅되지 못하고 전체 파이프라인이 블로킹(Blocking)되는 대참사가 발생할 수 있습니다.
  • 유령 데이터 현상: 관리자 콘솔에서 큐의 깊이(Queue Depth)를 보면 분명히 메시지가 수십 개 쌓여 있는데, 정작 컨슈머 애플리케이션은 메시지를 1건도 가져오지 못하는 기이한 현상이 나타납니다.

3. 해결법 1: 트랜잭션 매니저(TM)를 통한 자동 복구 (정석)

가장 안전하고 원칙적인 해결책은 죽어버린 트랜잭션 매니저(Atomikos, Bitronix, Narayana 등)를 되살리는 것입니다.

트랜잭션 매니저는 자체적으로 로컬 디스크에 '트랜잭션 로그'를 기록하고 있습니다. TM 서버가 재부팅되면 가장 먼저 자신의 로그를 읽어 들이는 복구 프로세스(Recovery Process)를 시작합니다.
TM은 로그를 분석하여 "아, 내가 브로커에게 Prepare까지만 시키고 죽었구나"라는 사실을 인지합니다. 이후 브로커에 다시 네트워크 접속을 맺고 백그라운드에서 조용히 커밋(또는 롤백) 명령을 재발송하여 인프라의 정합성을 스스로 치유합니다.

따라서 XA 트랜잭션을 운영하는 아키텍트라면, TM 서버가 띄워져 있는 컨테이너나 팟(Pod)이 재시작되더라도 트랜잭션 로그 파일이 유지되도록 반드시 영구 볼륨(Persistent Volume)을 마운트해 두어야 합니다.


4. 해결법 2: JMX를 이용한 수동 휴리스틱(Heuristic) 개입

TM 서버의 디스크가 물리적으로 파손되어 트랜잭션 로그가 완전히 날아갔거나, 복구 프로세스가 꼬여서 자동 치유가 불가능한 최악의 상황이 발생할 수 있습니다. 이때는 시스템 관리자가 직접 브로커에 개입하여 트랜잭션을 강제로 끊어내야 합니다. 이를 휴리스틱(Heuristic) 결정이라고 합니다.

ActiveMQ 인프라에서는 JMX(Java Management Extensions) 기능을 활용하여 이 작업을 수행합니다.

A. In-doubt 트랜잭션 식별하기
JConsole이나 VisualVM 같은 JMX 모니터링 도구를 통해 브로커에 접속합니다. org.apache.activemq 도메인 하위의 Broker MBean을 살펴보면 현재 진행 중이거나 멈춰있는 트랜잭션 목록을 배열 형태로 반환하는 오퍼레이션(예: recoverPreparedTransactions())이 있습니다. 이 명령을 실행하면 현재 브로커에 고립된 XA 트랜잭션들의 고유 식별자인 XID(Transaction ID) 목록이 텍스트 형태로 출력됩니다.

B. 강제 롤백 또는 커밋 수행
식별된 XID를 복사한 뒤, 동일한 JMX MBean에서 제공하는 다음 두 가지 오퍼레이션 중 하나를 선택하여 실행합니다.

  • rollbackPreparedTransaction(String xid): 가장 권장되는 안전한 조치입니다. 브로커에 잡혀있던 메시지 기록과 락을 완전히 취소하고 버립니다. (데이터베이스 측 관리자도 동일하게 DB의 Prepared 트랜잭션을 롤백해야 정합성이 맞습니다.)
  • commitPreparedTransaction(String xid): 비즈니스 로직상 해당 메시지가 반드시 큐에 들어가야 한다고 확신할 때만 사용하는 강제 커밋 명령입니다.

이러한 수동 개입은 최후의 수단이며, 데이터 정합성이 깨질 위험을 내포하고 있으므로 반드시 DB 관리자와 브로커 관리자가 상황을 교차 검증한 후 동시에 조치를 취해야 합니다.


5. 아키텍처 관점의 예방책 및 대안 설계

애초에 이런 끔찍한 좀비 상태를 겪지 않으려면 시스템 아키텍처를 설계할 때 다음 원칙을 고려해야 합니다.

  • TM 타임아웃의 최소화: 트랜잭션 매니저의 설정 파일에 타임아웃 값을 비즈니스 허용 범위 내에서 최대한 타이트하게(예: 30초~1분) 설정하여, 락이 무한정 길어지는 것을 방지해야 합니다.
  • XA 트랜잭션의 회피 (Modern Architecture): 2PC 기반의 XA 트랜잭션은 완벽한 정합성을 제공하지만, 이처럼 클러스터 전체를 마비시키는 '단일 장애점(SPOF)'을 만들고 시스템의 처리 속도(Throughput)를 극단적으로 저하시킵니다. 현대의 MSA 환경에서는 이 거대한 XA 트랜잭션을 버리고 결과적 일관성(Eventual Consistency)을 추구하는 방향으로 선회하고 있습니다.

분산 시스템에서 완벽함을 추구하는 XA 트랜잭션은 역설적으로 가장 다루기 까다로운 장애를 만들어냅니다. 'Prepared' 상태의 위험성을 인지하고 JMX 기반의 강력한 모니터링 체계를 갖추시길 바랍니다.

반응형