ActiveMQ와 JMS 운영
JMS Queue·Topic의 전달 흐름과 ACK, Redelivery, DLQ, Prefetch를 이메일 Worker 사례로 이해한다.
이 문서의 목차
Overview
ActiveMQ는 Java Message Service(JMS) API를 구현하는 Message Broker다. Producer와 Consumer 사이에 Queue 또는 Topic을 두어 처리 시점을 분리하고, 일시적인 Consumer 장애를 Message 보관과 재전달로 흡수한다.
한 문장 설명: ActiveMQ는 작업 요청을 Broker에 맡기고 Consumer가 ACK할 때까지 전달 상태를 관리하는 비동기 완충 지대다.
왜 필요한가
메일 발송처럼 외부 SMTP와 Network 상태에 따라 시간이 달라지는 작업을 HTTP 요청 안에서 끝내려 하면 사용자 응답과 장애 범위가 커진다. Producer는 검증된 발송 요청을 빠르게 Queue에 넣고, Consumer Worker는 자신의 처리량으로 꺼내 발송한다. Traffic Spike가 와도 Queue Depth가 완충하며 실패 작업은 Retry와 DLQ로 분리할 수 있다.
핵심 원리
- Queue: 하나의 Message는 경쟁 Consumer 중 하나가 처리한다. 작업 분산에 적합하다.
- Topic: 활성 Subscription마다 Message 사본을 받는다. Event Broadcast에 적합하다. Offline 구독 보존에는 Durable Subscription 설정이 필요하다.
- Persistent Message: Broker 재시작 후에도 복구하도록 Store에 기록한다. Non-persistent는 빠를 수 있지만 장애 시 잃을 수 있다.
- ACK: Consumer가 처리 완료를 Broker에 알리는 경계다. ACK 전 장애는 재전달될 수 있다.
JMS의 AUTO_ACKNOWLEDGE는 Listener가 정상 반환한 뒤 Container가 확인하는 일반적 방식이다. CLIENT_ACKNOWLEDGE는 Application이 명시적으로 확인하지만 Session에서 누적된 Message까지 확인될 수 있어 범위를 이해해야 한다. Transacted Session은 Commit 때 수신과 발신을 함께 확정하고 Rollback 시 Redelivery한다.
내부 동작
flowchart LR; API[요청 API] -->|JMS send| Q[(ActiveMQ Queue)]; Q --> W1[메일 Worker 1]; Q --> W2[메일 Worker 2]; W1 --> SMTP[외부 SMTP]; W1 -->|성공 ACK| Q; W1 -->|실패 Rollback| R[Redelivery]; R -->|한도 초과| D[(DLQ)]
Broker는 Destination별 Enqueue/Dequeue와 Consumer 상태를 관리한다. Consumer의 Prefetch는 ACK 전 미리 가져올 Message 수다. 값이 크면 처리량은 좋아질 수 있지만 느린 Consumer 하나가 Message를 많이 선점해 다른 Consumer가 놀 수 있다. 처리 시간이 길거나 편차가 크면 작은 Prefetch와 Consumer 동시성을 함께 시험한다.
Example
@JmsListener(destination = "mail.request")
public void send(MailCommand command) {
if (processedEventRepository.exists(command.eventId())) return;
smtpSender.send(command.to(), command.templateId());
processedEventRepository.save(command.eventId());
}원문의 도메인별 발송 Buffer와 Listener 흐름을 일반화한 예다. 실제로는 DB 기록과 SMTP 발송을 하나의 Local Transaction으로 묶을 수 없으므로 멱등성 Key, 발송 상태, 재시도 가능 오류와 영구 오류 분류가 필요하다. Password, Broker URL, 실제 발신 주소는 코드나 Wiki에 넣지 않는다.
실무에서 발생하는 문제
Redelivery와 Poison Message
Consumer가 예외를 내거나 Transaction을 Rollback하면 Message가 다시 전달된다. 계속 실패하는 Message가 Queue 앞에서 반복되면 정상 작업까지 방해하므로 Redelivery 횟수와 Backoff를 제한하고 DLQ로 이동한다. DLQ는 쓰레기통이 아니라 원인, Payload Schema, 재처리 승인 여부를 조사하는 별도 업무 Queue다.
Slow Consumer와 Memory
Producer가 Consumer보다 빠르면 Pending Message와 Store 사용량이 증가한다. 모든 Message를 JVM Memory에만 둔다는 단순 설명은 부정확하지만, Pending Cursor·Prefetch·큰 Payload·Producer Flow Control 설정에 따라 Heap과 Disk 압력이 함께 나타날 수 있다. Memory Limit만 늘리기보다 생산률과 소비율, Store 한도, Paging/Cursor를 본다.
Broker 단일 장애점
원문의 “한 대가 죽으면 전체가 멈춘다”는 우려는 배치 구조에 따라 맞을 수 있다. Shared Store/Replicated LevelDB 같은 구형 구성이나 ActiveMQ Artemis의 HA, Client Failover URI 등 제품·버전별 방식이 다르다. 현재 Version과 지원되는 HA 구성을 확인하고 실제 Broker 종료 Test로 RTO와 중복을 측정한다.
Trade-off
ActiveMQ는 JMS와 Spring 통합이 간단하고 전통적인 Enterprise Queue 요구를 잘 처리한다. 반면 대규모 Replay와 Partition 기반 수평 처리에는 Kafka의 Log 모델이 더 자연스럽다. RabbitMQ는 Exchange Routing, SQS는 관리형 운영이 장점이다. Migration은 제품 이름이 아니라 Ordering, Replay, ACK, DLQ, Transaction, 운영 인력과 기존 Client 호환성으로 결정한다.
흔한 오해
- ACK는 업무 결과가 정확히 한 번 발생했음을 보장하지 않는다. ACK 응답 유실로 Message가 다시 올 수 있다.
- Persistent Message라고 외부 SMTP 발송 결과까지 복구되는 것은 아니다.
- Consumer 수를 늘리면 순서 범위가 달라지고 하위 SMTP/DB Capacity를 넘을 수 있다.
- ActiveMQ가 항상 Memory에만 저장되고 Kafka가 항상 Disk라서 안전하다는 단순 비교는 실제 Store와 설정을 놓친다.
Production Considerations
Enqueue/Dequeue Rate, Queue Depth, Oldest Message Age, In-flight, Redelivery, Expired, DLQ, Consumer 수, Store/Temp/Memory 사용률을 본다. Broker와 Consumer 재시작, Network 단절, DLQ Replay를 훈련한다. Alert는 Queue Depth 단독보다 증가 속도와 Oldest Age, Consumer 부재를 함께 사용한다.
Interview Questions / Follow-up Questions
- Queue와 Topic, Durable Subscription의 차이는 무엇인가?
- AUTO/CLIENT/Transacted ACK의 완료 경계는 어디인가?
- Prefetch가 처리량과 공정성에 미치는 영향은?
- 후속: SMTP 발송 성공 직후 Consumer가 죽으면 중복 발송을 어떻게 줄일 것인가?
- 후속: DLQ Message를 재처리하기 전 어떤 정보를 확인할 것인가?
Related Topics
비동기 처리 설계, Message Broker 선택, Kafka, SQS·SNS의 전달·재생·운영 모델을 비교한다.
SOURCE REFERENCES
이 문서의 근거
본문은 Dev Atlas 안에서 완결되며, 검증이 필요할 때만 원문을 확인할 수 있습니다.