🎯 분산 시스템의 난제: 'Exactly-once'는 어떻게 신화에서 현실이 되는가?

메시징 시스템에서 배달 보장 수준(Delivery Guarantees)은 크게 세 가지로 나뉩니다.
- At-most-once (최대 한 번): 메시지가 유실될 수 있지만, 중복은 없습니다.
- At-least-once (최소 한 번): 메시지 유실은 없지만, 중복이 발생할 수 있습니다.
- Exactly-once (정확히 한 번): 메시지 유실도 없고, 중복도 없습니다.
모두가 3번을 원하지만, 네트워크는 불완전하고 서버는 언제든 죽을 수 있습니다. 결론부터 말씀드리면, 네트워크 수준에서의 순수한 Exactly-once는 물리적으로 불가능합니다. 하지만 우리는 아키텍처 설계를 통해 '결과적으로' Exactly-once와 동일한 효과를 낼 수 있습니다. 그 비밀을 파헤쳐 보죠.
1. 이론적 배경: '두 장군의 문제 (Two Generals' Problem)'
왜 Exactly-once가 어려울까요? 이는 고전적인 '두 장군의 문제'로 설명됩니다. 두 부대가 적군을 동시에 공격해야 승리하는데, 전령(네트워크)이 중간에 붙잡힐 수 있는 상황에서 양측이 완벽하게 합의에 도달할 방법은 수학적으로 존재하지 않습니다.
우리가 메시지를 보낼 때도 마찬가지입니다.
- Producer가 메시지를 보내고 Broker가 받았지만, ACK(확인 응답)를 보내는 도중에 네트워크가 끊긴다면?
- Producer는 성공 여부를 알 수 없어 메시지를 재전송(Retry)하게 되고, 결과적으로 중복이 발생합니다.
이 확률적 불확실성을 상쇄하기 위해 우리는 상태(State)와 트랜잭션(Transaction)이라는 도구를 사용합니다.
2. 메커니즘 1: 2단계 커밋 (Two-Phase Commit, 2PC)와 XA 트랜잭션
전통적인 엔터프라이즈 환경(ActiveMQ Classic 포함)에서 사용하는 가장 확실한 방법은 2PC입니다. 메시지 브로커와 데이터베이스(DB)를 하나의 원자적 작업 단위로 묶는 것이죠.
2단계 과정:
- Prepare Phase: 트랜잭션 관리자(Coordinator)가 브로커와 DB에게 "준비됐어?"라고 묻습니다. 모두가 "Yes"라고 답하면 데이터를 임시 영역에 기록합니다.
- Commit Phase: 모든 참여자가 준비되었다면 최종적으로 "Commit!"을 외쳐 데이터를 확정합니다.
하지만 2PC는 지독하게 느립니다. 모든 참여자가 응답할 때까지 자원을 잠그기(Locking) 때문입니다. 성능을 중시하는 현대의 MSA 환경에서는 점차 외면받고 있지만, 금융권처럼 데이터 무결성이 생명인 곳에서는 여전히 '표준'의 위치를 지키고 있습니다.
3. 메커니즘 2: 브로커 수준의 중복 제거 (Idempotent Producer)
현대의 ActiveMQ Artemis나 Kafka가 사용하는 방식입니다. 메시지 자체에 고유 식별자(Sequence ID 또는 UUID)를 부여하는 전략입니다.
Artemis의 _AMQ_DUPL_ID 활용
Artemis는 메시지 헤더에 _AMQ_DUPL_ID라는 속성을 지원합니다. 브로커는 이 ID를 메모리와 저널(Journal)에 저장해 둡니다.
- Producer가 동일한 ID의 메시지를 다시 보내면, 브로커는 저널을 확인합니다.
- 이미 처리된 ID라면? 메시지를 조용히 버리고(Discard) 성공 응답만 보냅니다.
즉, 같은 메시지를 몇 번을 호출해도 시스템의 상태는 처음 한 번 처리했을 때와 동일하게 유지됩니다.
4. 메커니즘 3: 컨슈머의 멱등성 보장 (The Final Defense)
사실 브로커가 아무리 잘해도, 메시지를 받는 Consumer 단에서 장애가 나면 Exactly-once는 깨집니다. 메시지를 다 처리했는데 브로커에게 "다 읽었어(ACK)"라고 말하기 직전에 서버가 꺼진다면? 브로커는 메시지를 다시 보낼 것이고, 컨슈머는 중복 처리를 하게 됩니다.
진정한 빌런급 개발자는 브로커를 믿지 않습니다. 대신 컨슈머를 멱등하게 설계합니다.
- Unique Key 활용: DB의
Unique Key제약 조건을 활용해 중복 Insert를 원천 차단합니다. - 상태 체크 (Read-before-Write): 작업을 수행하기 전, 이미 처리된 ID인지 DB나 Redis에서 먼저 조회합니다.
- 로그 기록 (Inbox Pattern): 처리한 메시지 ID를 별도의 테이블에 기록해 두고 대조합니다.
5. 비용과 성능의 트레이드오프: "공짜는 없다"
Exactly-once를 구현하면 성능(Throughput)은 필연적으로 하락합니다. 지연 시간(Latency) 신뢰성 사이에는 반비례 관계가 성립합니다.
신뢰성을 높이기 위해 ACK를 기다리고, 저널에 ID를 기록하고, 분산 트랜잭션을 수행할수록 시스템은 무거워집니다. 따라서 실무에서는 모든 곳에 Exactly-once를 적용하는 것이 아니라, '결제' 같은 치명적인 곳에만 적용하고 일반적인 '로그 수집' 등에는 At-least-once를 선택하는 지혜가 필요합니다.
'1. 개발 > 1.8. ActiveMQ' 카테고리의 다른 글
| 가상 토픽(Virtual Topics)이 팬아웃(Fan-out) 성능을 올리는 원리는? (0) | 2026.03.10 |
|---|---|
| Topic 모델에서 '지속성 구독자(Durable Subscriber)'의 원리는? (0) | 2026.03.10 |
| MOM(Message Oriented Middleware)으로서 ActiveMQ의 위치는? (0) | 2026.03.10 |
| Artemis가 HornetQ를 흡수한 기술적 이유는? (0) | 2026.03.10 |
| ActiveMQ Classic의 탄생 배경과 초기 설계 철학은? (0) | 2026.02.28 |