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

브로커 내부 스케줄러가 사용하는 'Job Scheduler' 데이터베이스 구조는?

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

브로커 내부 스케줄러가 사용하는 'Job Scheduler' 데이터베이스 구조는?

엔터프라이즈 메시지 브로커에서 '지연 전송(Delayed Delivery)'이나 '예약 발송(Scheduled Message)'을 처리하는 것은 일반적인 큐(Queue) 처리와는 완전히 다른 아키텍처적 도전을 요구합니다.

일반 큐는 들어온 순서대로 나가는 선입선출(FIFO) 구조에 최적화되어 있지만, 예약 메시지는 '시간'이라는 축을 기준으로 정렬되고 검색되어야 하기 때문입니다. ActiveMQ Classic과 같은 브로커는 이를 해결하기 위해 메인 메시지 스토어와는 물리적으로 분리된, 시간 기반 탐색에 최적화된 'Job Scheduler' 전용 데이터베이스 구조를 내장하고 있습니다.

이 가이드에서는 브로커가 예약된 수십만 건의 메시지를 어떻게 저장하고 제때 꺼내어 발송하는지, 그 내부 데이터베이스 구조와 동작 원리를 상세히 해부합니다.


1. 일반 큐 저장소(KahaDB)와의 분리

브로커가 예약 메시지를 일반 큐와 동일한 공간에 저장하지 않는 이유는 극심한 탐색 비용 때문입니다.

만약 10만 개의 메시지가 쌓인 일반 큐 안에 1시간 뒤에 발송할 메시지가 무작위로 섞여 있다면, 브로커는 매초마다 10만 개의 메시지를 전부 뒤져서 발송 시간이 된 메시지를 찾아내야 합니다. 이는 브로커의 CPU와 디스크 I/O를 낭비하는 최악의 방식입니다.

따라서 브로커는 예약 기능이 활성화되면 기존 저장소와는 완전히 분리된 예약 전용 스토리지(Job Scheduler Store)를 디스크에 별도로 생성합니다. 이 저장소는 오직 '실행 예정 시간'을 기준으로 데이터를 쓰고 읽는 데 특화된 구조를 가집니다.


2. 핵심 자료구조: B-Tree 인덱스 (B-Tree Indexing)

Job Scheduler 데이터베이스의 심장부는 관계형 데이터베이스(RDBMS)의 인덱스 구조로도 널리 쓰이는 B-Tree (Balanced Tree) 자료구조입니다.

  • 키(Key)의 구성: B-Tree 인덱스의 키 값은 메시지가 고유하게 가진 ID가 아니라, 바로 '실행되어야 할 타임스탬프(예정 시간)'입니다.
  • 값(Value)의 구성: 실제 메시지의 본문이 저장된 디스크 파일의 위치(Offset) 정보를 담은 참조(Reference) 포인터입니다.
  • 탐색의 효율성: B-Tree 구조는 트리 내의 데이터가 항상 시간순으로 정렬된 상태를 유지하도록 보장합니다. 브로커 내부의 백그라운드 스케줄러 스레드는 큐 전체를 스캔할 필요 없이, B-Tree의 가장 왼쪽(시간이 가장 빠른) 노드만 지속적으로 확인하면 됩니다. 가장 빠른 노드의 시간이 아직 도래하지 않았다면, 그 뒤에 있는 수백만 개의 예약 메시지는 쳐다볼 필요조차 없으므로 탐색 성능이 극대화됩니다.

3. 고속 쓰기를 위한 저널 파일 (Journal / Append-Only Log)

인덱스가 B-Tree로 관리된다면, 메시지의 무거운 페이로드(본문) 데이터는 어디에 저장될까요? 잦은 B-Tree 노드의 분할과 병합 과정에 무거운 본문 데이터를 함께 움직이는 것은 디스크 부하를 가중시킵니다.

  • 브로커는 본문 저장을 위해 저널(Journal) 파일이라는 순차적 쓰기 전용 로그 파일을 사용합니다.
  • 클라이언트가 예약 메시지를 전송하면, 브로커는 즉시 저널 파일의 맨 끝에 메시지 본문을 있는 그대로 이어 붙여 기록(Append)합니다. 디스크의 헤더를 앞뒤로 움직일 필요 없이 끝에만 쓰면 되므로 쓰기 속도가 매우 빠릅니다.
  • 저널에 쓰기가 완료되면, 해당 메시지가 기록된 저널 파일 번호와 오프셋 위치를 B-Tree 인덱스에 값(Value)으로 추가하여 구조를 업데이트합니다.

4. 예약 메시지의 라이프사이클 (Execution Flow)

이러한 B-Tree와 저널 파일의 조합 위에서 예약 메시지는 다음과 같은 생명주기를 거칩니다.

  1. 저장 단계: 예약 메시지가 유입되면 디스크 저널에 기록되고, 실행 시간을 키값으로 하여 B-Tree 인덱스에 등록됩니다.
  2. 대기 및 폴링(Polling) 단계: 브로커 내부의 타이머 스레드가 정해진 주기(예: 1초 단위)마다 B-Tree의 최상단(가장 임박한) 노드를 확인하며 대기합니다.
  3. 발송(Dispatch) 단계: 현재 시간이 B-Tree 노드에 기록된 실행 시간을 지나는 순간, 타이머 스레드는 해당 노드를 인덱스에서 즉시 삭제합니다. 그리고 포인터를 따라 저널 파일에서 원본 메시지를 읽어 들인 뒤, 메시지가 원래 가야 할 실제 목적지 큐(Target Queue)로 메시지를 복사하여 밀어 넣습니다.
  4. 소비 및 정리 단계: 목적지 큐로 들어간 메시지는 일반 메시지와 동일하게 컨슈머에게 전달되어 비즈니스 로직을 탑니다. 디스크 저널에 남아있던 원본 예약 데이터는 인덱스에서 지워져 더 이상 참조되지 않으므로, 백그라운드 가비지 컬렉션(GC) 프로세스에 의해 일괄 삭제되거나 파일이 정리됩니다.

5. 아키텍처 설계 시 치명적인 주의사항 (Anti-Patterns)

시간 기반의 B-Tree 스케줄러 데이터베이스는 우수하지만, 이를 오남용하면 브로커 전체를 다운시킬 수 있는 시한폭탄이 됩니다.

  • 인덱스 비대화 (B-Tree Bloat): 수개월 뒤에 발송할 메시지 수백만 건을 브로커에 한 번에 밀어 넣으면, B-Tree의 깊이가 끝없이 깊어지고 노드 분할 연산이 끊임없이 발생합니다. 이는 브로커의 JVM 힙 메모리를 심각하게 고갈시키며, 예약과 상관없는 일반 실시간 메시지의 처리 성능까지 연쇄적으로 무너뜨립니다.
  • 동시 폭발 (Thundering Herd): 이벤트 프로모션 등을 위해 "자정(00시 00분)에 10만 건 일괄 발송"과 같이 동일한 타임스탬프에 막대한 양의 메시지를 예약하는 패턴은 매우 위험합니다. 타이머 스레드가 자정을 인식하는 순간, B-Tree에서 10만 개의 노드를 동시에 읽어와 디스크 저널에서 페이로드를 찾고 메인 큐로 복사하는 엄청난 디스크 I/O 스파이크가 발생하여 브로커가 멈출 수 있습니다. 예약 시간은 반드시 애플리케이션 단에서 초 단위로 난수(Random)를 부여하여 분산시켜야 합니다.

6. 결론

메시지 브로커의 Job Scheduler는 시간 기반의 B-Tree 인덱스와 고속 순차 쓰기 저널을 결합하여 지연 전송의 마법을 부립니다.

하지만 본질적으로 메시지 브로커는 초고속 라우팅 엔진이지 장기 데이터를 보관하는 데이터베이스가 아닙니다. 장기적인 배치 스케줄링은 외부 RDBMS나 전용 배치 프레임워크에 맡기고, 브로커의 내부 스케줄러는 짧은 지연(수 분~수 시간 내)과 재시도(Retry Backoff) 로직을 처리하는 보조적인 수단으로만 한정하여 사용하는 것이 가장 튼튼하고 안전한 메시징 아키텍처 설계입니다.

반응형