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

'maxReconnectAttempts' 설정에 따른 클라이언트 무한 루프 방지법?

by 엉짱 2026. 4. 9.
반응형

'maxReconnectAttempts' 설정에 따른 클라이언트 무한 루프 방지법?

엔터프라이즈 환경에서 메시지 브로커(ActiveMQ 등)와 통신하는 클라이언트 애플리케이션을 설계할 때, 네트워크 단절이나 브로커 서버의 다운은 언제든 발생할 수 있는 상수입니다. 이를 방어하기 위해 클라이언트 측에서는 Failover Transport를 사용하여 브로커가 다시 살아날 때까지 자동으로 재연결(Reconnect)을 시도하는 복원력(Resilience) 아키텍처를 구성합니다.

하지만 이 '자동 재연결'의 본능은 시스템의 맥락에 따라 가장 든든한 아군이 되기도 하고, 애플리케이션 서버를 통째로 마비시키는 치명적인 '무한 루프(Infinite Loop)' 폭탄이 되기도 합니다.

본 가이드에서는 클라이언트 애플리케이션이 브로커 장애 시 끝없는 대기 상태에 빠지는 물리적 원인을 해부하고, 이를 통제하는 가장 확실한 안전밸브인 maxReconnectAttempts 파라미터의 아키텍처적 의미와 최적화 전략을 상세히 분석합니다.


1. Failover Transport의 재연결 본능과 무한 루프의 위험성

ActiveMQ 클라이언트 라이브러리의 failover:// URI는 매우 강력하지만, 기본적으로 영원히(무한대로) 재연결을 시도하도록 설계되어 있습니다. (기본값: 무제한)

네트워크 스위치가 1~2초 정도 깜빡이는 일시적인 순단 현상이라면 이 무한 재연결 기능이 백그라운드에서 조용히 시스템을 복구해 줍니다. 하지만 브로커 서버 하드웨어가 완전히 망가져 복구에 수 시간 이상이 걸리는 '진짜 장애' 상황이 발생하면 어떻게 될까요?

  • 스레드 블로킹(Thread Blocking): 클라이언트가 브로커로 메시지를 보내기 위해 producer.send() 메서드를 호출하는 순간, 이 스레드는 연결이 복구될 때까지 멈춰서(Blocking) 반환되지 않습니다.
  • 로그 폭수(Log Flooding): 백그라운드에서는 쉴 새 없이 브로커로 TCP SYN 패킷을 날리고 연결 실패 예외(Exception) 로그를 초당 수백 줄씩 쏟아내며 디스크 I/O를 갉아먹습니다.
  • WAS(웹 애플리케이션 서버)의 붕괴: 가장 치명적인 시나리오입니다. Tomcat이나 Spring Boot와 같이 사용자의 HTTP 요청을 받는 환경에서 브로커에 메시지를 비동기로 던진다고 가정해 봅시다.
    클라이언트의 스레드들이 브로커의 응답을 기다리며 무한 대기 상태에 빠지면, WAS의 스레드 풀(Thread Pool)은 순식간에 고갈됩니다. 결국 브로커의 장애가 웹 서버의 타임아웃과 전면 마비로 이어지는 끔찍한 연쇄 붕괴(Cascading Failure)가 발생합니다.

2. 안전밸브의 도입: 'maxReconnectAttempts' 설정

이러한 무한 루프와 스레드 고갈을 원천 차단하기 위해, 브로커 연결 URI에 maxReconnectAttempts 파라미터를 명시적으로 부여하여 재연결 시도의 '상한선'을 설정해야 합니다.

이 파라미터에 들어가는 숫자는 클라이언트의 운명을 결정짓는 매우 중요한 아키텍처적 결단입니다.

A. -1 또는 0 (무한대 지정 시의 의미)
일부 구버전에서는 0이, 최신 버전에서는 -1이 '무한 재연결'을 의미합니다. (버전별 레퍼런스 확인 필수).
이 설정은 사용자 응답이 필요 없는 독립적인 백그라운드 배치 프로세스나 데몬(Daemon) 워커에 적합합니다. 브로커가 며칠 동안 죽어있더라도 프로세스가 종료되지 않고 조용히 기다리다가, 브로커가 살아나면 묵묵히 밀린 작업을 재개해야 하는 아키텍처에서만 제한적으로 사용해야 합니다.

B. 0 (빠른 실패 - Fail Fast)
재연결 시도를 아예 하지 않겠다는 의미입니다.
사용자가 웹 화면에서 버튼을 눌렀을 때 동기식으로 즉각적인 피드백(예: "현재 시스템 지연으로 처리가 불가합니다")을 주어야 하는 API 서버 환경에서 매우 유용합니다. 브로커가 죽어있으면 스레드를 대기시키지 않고 즉시 JMSException을 발생시켜, 애플리케이션이 에러를 빠르게 삼키고 사용자에게 안내 메시지를 띄울 수 있도록 제어권을 반환합니다.

C. N (양수 지정 - 타협과 유연성)
maxReconnectAttempts=3 과 같이 양수를 지정하는 것이 일반적인 엔터프라이즈 WAS 환경의 모범 사례입니다.
일시적인 네트워크 순단은 3번의 재시도 안에서 부드럽게 복구하고, 3번을 모두 실패하면 더 이상 무의미한 루프를 돌지 않고 즉시 예외(Exception)를 던져 스레드를 해방(Release)시킵니다. 시스템의 복원력과 WAS의 안정성이라는 두 마리 토끼를 잡는 최적의 타협점입니다.


3. 지수 백오프(Exponential Backoff)와의 필수 결합

단순히 재시도 횟수만 제한한다고 해서 인프라가 완벽해지는 것은 아닙니다. maxReconnectAttempts는 반드시 시간 간격 튜닝과 결합되어야 진정한 효과를 발휘합니다.

재시도를 3번 하더라도, 0.1초 만에 3번을 다 소진해 버리면 장애 복구 시간을 벌어주지 못합니다. 반대로 클라이언트 수만 대가 일정한 간격으로 동시에 재연결을 시도하면, 갓 부팅된 브로커 서버가 '재연결 폭풍(Reconnection Storm)'을 맞고 다시 죽어버립니다.

이를 방지하기 위해 다음의 파라미터들을 조합하여 점진적으로 대기 시간을 늘려가는 지수 백오프(Exponential Backoff) 아키텍처를 완성해야 합니다.

  • initialReconnectDelay=1000: 첫 연결 실패 후 1초(1000ms)를 대기합니다.
  • useExponentialBackOff=true: 실패할 때마다 대기 시간을 지수 배수(기본 2배)로 늘립니다. (1초 -> 2초 -> 4초 대기)
  • maxReconnectDelay=30000: 대기 시간이 아무리 길어져도 최대 30초까지만 기다리도록 상한선을 긋습니다.

4. 실무 환경을 위한 최적화된 URI 설정 가이드 (Best Practices)

앞서 설명한 방어 로직들을 종합하여, 웹 애플리케이션 서버(WAS)가 브로커의 장애로부터 자신을 완벽하게 보호할 수 있는 실무 최적화 연결 URI를 구성해 보겠습니다.

failover:(tcp://broker1:61616,tcp://broker2:61616)?randomize=false&maxReconnectAttempts=5&startupMaxReconnectAttempts=2&timeout=3000&initialReconnectDelay=1000&useExponentialBackOff=true

[핵심 파라미터 해부]

  1. maxReconnectAttempts=5: 운용 중 연결이 끊기면 최대 5번만 점진적으로 재연결을 시도하고, 실패 시 깔끔하게 Exception을 던져 스레드 풀을 보호합니다.
  2. startupMaxReconnectAttempts=2: (매우 중요) 애플리케이션이 '최초로 기동(Startup)'될 때 브로커가 죽어있을 경우의 시도 횟수입니다. 애플리케이션 서버를 배포하여 띄울 때 브로커가 점검 중이라고 해서 WAS 부팅 자체가 무한정 멈춰버리는(Hang) 현상을 막기 위해, 기동 시에는 단 2번만 시도하고 빠르게 부팅을 마무리하도록 분리 설정합니다.
  3. timeout=3000: send() 메서드를 호출했을 때 3초(3000ms) 안에 브로커로 데이터가 넘어가지 않으면 즉시 타임아웃 예외를 발생시켜 클라이언트의 로직 대기를 강제로 끊어냅니다.

결론적으로 클라이언트의 페일오버는 "끝까지 포기하지 않는 것"이 미덕이 아닙니다. 인프라 아키텍처에서는 "언제 포기하고 에러를 내뱉을 것인가"를 정교하게 설계하는 것이 전체 시스템의 붕괴를 막는 핵심입니다. maxReconnectAttempts와 백오프 설정을 통해 브로커 장애가 애플리케이션의 장애로 전이되는 것을 완벽하게 차단하는 견고한 방화벽을 구축하시기 바랍니다.

반응형