비영속성 메시지(Non-persistent)가 메모리 부족 시 디스크로 스왑되는 과정은?
엔터프라이즈 메시징 시스템을 설계할 때, 데이터의 중요도와 요구되는 처리 속도에 따라 메시지의 전송 모드(Delivery Mode)를 결정하게 됩니다. 결제나 주문 데이터처럼 절대 유실되어서는 안 되는 정보는 디스크에 동기화되는 영속성(Persistent) 모드를 사용하고, 실시간 센서 데이터나 단순 알림처럼 유실되어도 무방하며 극강의 처리 속도가 필요한 정보는 메모리에서만 처리되는 비영속성(Non-persistent) 모드를 사용하는 것이 표준 아키텍처입니다.
하지만 여기서 많은 엔지니어들이 치명적인 오해를 범합니다. "비영속성 메시지는 메모리에만 상주하므로 디스크 I/O를 전혀 발생시키지 않을 것이다"라는 착각입니다. 컨슈머(Consumer)의 처리 속도가 프로듀서(Producer)의 발행 속도를 따라가지 못해(Slow Consumer 현상) 큐(Queue)에 메시지가 계속 쌓여 브로커의 메모리 한계치에 도달한다면, 브로커는 시스템 전체가 OutOfMemory(OOM)로 붕괴되는 것을 막기 위해 비영속성 메시지들을 강제로 디스크로 밀어내는 스왑(Swap) 및 페이징(Paging) 작업을 시작합니다.
본 가이드에서는 비영속성 메시지가 디스크로 오프로드되는 내부 메커니즘과 이로 인해 발생하는 파급 효과, 그리고 이를 방어하기 위한 아키텍처 설계 기법을 상세히 분석합니다.

1. 메모리 한계와 브로커의 자기 방어 기제 (Self-Defense Mechanism)
메시지 브로커(ActiveMQ, Artemis 등)는 구동 시 JVM(Java Virtual Machine) 힙(Heap) 메모리와 내부 시스템 메모리의 최대 사용 한계치(Memory Limit)를 할당받습니다. 비영속성 메시지는 정상적인 상황에서는 이 메모리 공간 내에 상주하며 초고속으로 라우팅됩니다.
그러나 시스템 장애나 트래픽 폭주로 인해 큐에 비영속성 메시지가 누적되어 메모리 사용량이 설정된 임계치(예: 전체 할당량의 70%)에 도달하면, 브로커는 생존을 위한 자기 방어 기제를 발동합니다. 이때 브로커가 취할 수 있는 대표적인 두 가지 액션은 다음과 같습니다.
- 생산자 차단 (Producer Flow Control): 더 이상 메모리가 꽉 차는 것을 막기 위해 메시지를 보내는 프로듀서의 TCP 연결을 블로킹하거나 예외를 반환합니다.
- 디스크 스왑/페이징 (Swapping/Paging): 유입되는 메시지를 계속 수용하기 위해, 현재 힙 메모리를 차지하고 있는 오래된 비영속성 메시지들을 찾아 임시 디스크 공간으로 쫓아냅니다.
가용성(Availability)을 최우선으로 설정한 시스템에서는 프로듀서를 차단하기보다 두 번째 방식인 디스크 스왑 방식을 주로 채택하게 되며, 이때부터 성능의 역전 현상이 발생합니다.
2. ActiveMQ Classic의 'Temp Storage' 스왑 메커니즘
ActiveMQ 5.x (Classic) 버전에서는 비영속성 메시지의 메모리 관리와 스왑 공간을 제어하기 위해 activemq.xml 내의 <systemUsage> 블록을 활용합니다.
- 동작 원리 (Page Out):
- 비영속성 메시지는 1차적으로
<memoryUsage>제한 내에서 힙 메모리에 적재됩니다. - 메모리가 한계에 다다르면, 브로커는 백그라운드 스레드를 깨워 메모리에 있는 비영속성 메시지들을
<tempUsage>로 지정된 디스크 임시 디렉토리(보통data/tmp_storage)로 직렬화(Serialization)하여 기록(Spooling)하기 시작합니다. - 디스크로 안전하게 복사된 메시지의 원본은 힙 메모리에서 가비지 컬렉션(GC) 대상이 되어 삭제됩니다. 대신 브로커 메모리에는 해당 메시지가 임시 디스크의 어느 위치(Offset)에 저장되어 있는지 가리키는 아주 작은 크기의 '참조 포인터(Reference Pointer)'만 남게 됩니다.
- 복구 및 전달 시점 (Page In):
- 이후 컨슈머가 큐의 메시지를 처리하여 메모리에 빈 공간이 생기면, 브로커는 포인터를 참조하여 임시 디스크(
tmp_storage)에서 대기 중인 메시지를 다시 JVM 메모리로 역직렬화하여 끌어올립니다. 이 과정을 페이지 인(Page In)이라고 하며, 이후 컨슈머에게 최종 전달됩니다.
3. ActiveMQ Artemis의 'Paging' 메커니즘
차세대 브로커인 ActiveMQ Artemis는 대용량 메시지 처리에 특화된 훨씬 진보된 페이징(Paging) 아키텍처를 도입했습니다.
- 동작 원리:
broker.xml의<address-setting>블록에서 특정 주소(Address)에 대한address-full-policy가PAGE로 설정되어 있을 때 작동합니다.- 해당 주소에 할당된
max-size-bytes(메모리 한계)를 초과하는 순간, 브로커는 즉시 해당 주소를 페이징 모드(Paging Mode)로 전환합니다. - 가장 큰 차이점은 페이징 모드에 돌입한 이후입니다. 새롭게 유입되는 모든 메시지(비영속성 포함)는 아예 JVM 힙 메모리를 거치지 않고, 직접 디스크의
data/paging디렉토리에 순차적인 페이지 파일(Sequential Page File, 예:000000001.page) 형태로 스트리밍되어 기록됩니다.
- 구조적 이점:
- 기존 Classic 버전처럼 힙 메모리에 이미 올라간 객체를 다시 직렬화하여 디스크로 내리는 오버헤드를 제거했습니다. 진입점부터 바로 파일로 기록하므로 JVM의 가비지 컬렉션(GC) 부하를 극적으로 줄이고 시스템의 안정성을 높입니다.
4. 디스크 스왑 발생 시의 치명적인 성능 저하 (Anti-Patterns)
"어차피 디스크의 임시 공간으로 잠시 대피시키는 것이라면 데이터 유실도 없고 안전한 것 아닌가?"라고 오판할 수 있습니다. 하지만 이 스왑 과정이 시작되는 순간, 시스템은 다음과 같은 치명적인 성능 병목의 늪에 빠지게 됩니다.
- 디스크 I/O 병목의 역설:
비영속성 메시지를 사용하는 유일한 이유는 '디스크 I/O를 회피하여 극강의 속도를 얻기 위함'이었습니다. 그러나 브로커 메모리 임계치를 넘는 순간, 가장 느린 매체인 디스크(HDD/SSD)를 읽고 쓰는 작업(I/O Bound)으로 강제 전환됩니다. 페이징이 시작되면 비영속성 메시지 처리 속도가 영속성 메시지를 처리할 때와 다를 바 없거나, 구조적 오버헤드로 인해 오히려 더 느려지는 기현상이 발생합니다. - 페이징 스래싱 (Paging Thrashing):
컨슈머의 처리 속도가 여전히 느린 상태에서 큐가 계속 꽉 차 있다면, 브로커는 생산자가 보낸 메시지를 디스크에 쓰고(Page Out), 컨슈머가 요청할 때마다 디스크에서 메시지를 읽어오는(Page In) 작업을 무한 반복하게 됩니다. 이로 인해 브로커 서버의 CPU I/O Wait 수치와 디스크 대역폭 사용률이 100%에 도달하며 전체 시스템이 마비됩니다. - 메모리 단편화 및 Stop-the-World GC:
메모리와 디스크 사이를 오가며 수백만 개의 메시지 객체가 생성되고 소멸하는 과정 자체는 JVM 힙 메모리에 막대한 압박을 가합니다. 심할 경우 가비지 컬렉터가 메모리를 정리하기 위해 애플리케이션의 모든 스레드를 멈추는 'Stop-the-World' 현상이 발생하여, 수 초에서 수십 초간 브로커가 어떠한 네트워크 요청도 받지 못하는 블랙아웃(Blackout) 장애로 이어집니다.
5. 아키텍처적 모범 사례 및 방어 전략 (Best Practices)
비영속성 메시지가 디스크로 스왑되는 상황은 시스템 설계가 실패했음을 알리는 '비정상 상태(Abnormal State)'로 간주해야 합니다. 이를 예방하기 위해 다음 세 가지 방어 전략을 반드시 인프라 아키텍처에 반영해야 합니다.
- 적극적인 만료 시간(TTL, Time-To-Live) 부여:
비영속성 메시지(실시간 로그, 현재 온도 센서 등)는 대체로 시간이 지나면 비즈니스적 가치가 급격히 떨어집니다. 생산자(Producer)가 메시지를 발행할 때 짧은 TTL(예: 10초~60초)을 강제로 부여하십시오. 컨슈머가 밀리더라도 브로커가 수명이 다한 메시지를 큐에서 스스로 삭제(Discard)하게 만들어 메모리 고갈을 원천적으로 방어해야 합니다. - 생산자 흐름 제어 (Producer Flow Control) 활성화:
디스크 스왑이라는 끔찍한 대가를 치르기보다는, 메모리가 꽉 찼을 때 브로커가 생산자의 전송 속도를 강제로 늦추거나 에러를 반환(address-full-policy="FAIL")하여 시스템 전반에 백프레셔(Backpressure)를 유발하는 것이 전체 인프라의 붕괴를 막는 훨씬 안전한 방법입니다. - 임시 스토리지(Temp Storage) 모니터링 경보 구축:
시스템 모니터링(APM, Zabbix, Prometheus 등)을 구축할 때 JVM 힙 메모리 사용량만 주시해서는 안 됩니다. 브로커 서버의 물리적 디스크 레이어에서tmp_storage(Classic) 또는paging(Artemis) 디렉토리의 용량 증가 추이를 모니터링 지표로 추가하고 알람을 설정하십시오. 해당 디렉토리의 크기가 0바이트를 초과하여 커지기 시작하는 순간이 바로 디스크 스왑 병목이 시작되었음을 의미하는 가장 정확한 신호입니다.
결론적으로, 비영속성(Non-persistent) 전송 모드는 메시지가 "절대 디스크에 기록되지 않음"을 기술적으로 완벽히 보장하는 것이 아니라, "정상적인 가용 메모리 한도 내에서는 최대한 메모리에만 머무르도록 동작함"을 의미할 뿐입니다. 극단적인 트래픽 병목 상황에서는 비영속성 메시지조차 디스크 스왑이라는 가장 무거운 대가를 치르게 된다는 아키텍처의 이면을 명심하고 시스템 설계에 임하시길 바랍니다.
'1. 개발 > 1.8. ActiveMQ' 카테고리의 다른 글
| 'Filter'와 'Divert'를 조합한 정교한 메시지 라우팅 설계법은? (0) | 2026.03.19 |
|---|---|
| Artemis의 'Divert' 기능을 이용한 메시지 변환 및 복제 시나리오는? (0) | 2026.03.19 |
| 서버 측 큐 브라우징(Queue Browsing)과 컨슈밍의 차이는? (0) | 2026.03.18 |
| 'JMSReplyTo' 헤더를 이용한 동적 응답 큐 생성법은? (0) | 2026.03.18 |
| 메시지 헤더의 'JMSCorrelationID'를 이용한 RPC 패턴 구현법은? (0) | 2026.03.18 |