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

DLQ로 가기 전 지수 백오프(Exponential Backoff)의 최대 지연 시간 설정?

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

DLQ로 가기 전 지수 백오프(Exponential Backoff)의 최대 지연 시간 설정?

엔터프라이즈 환경에서 분산 시스템을 운영하다 보면, 데이터베이스의 일시적인 락(Lock) 타임아웃이나 외부 API 서버의 순간적인 네트워크 단절과 같은 '일시적 장애(Transient Failure)'를 매일같이 마주하게 됩니다.

이때 컨슈머(Consumer)가 메시지 처리에 실패했다고 해서 그 즉시 메시지를 데드 레터 큐(DLQ)로 던져버리는 것은 매우 성급한 아키텍처입니다. 일시적 장애는 아주 짧은 시간만 기다려주면 스스로 복구되는 경우가 많기 때문입니다. 반대로, 실패하자마자 0.001초의 틈도 없이 즉각적으로 재시도를 반복하는 것 역시 시스템에 '재시도 폭풍(Retry Storm)'을 일으켜 장애를 더욱 악화시킵니다.

이러한 문제를 우아하게 해결하고 브로커와 클라이언트 시스템 모두를 보호하기 위해 도입하는 필수적인 라우팅 기법이 바로 '지수 백오프(Exponential Backoff)''최대 지연 시간(Maximum Redelivery Delay)' 설정입니다.

이 가이드에서는 메시지가 최후의 보루인 DLQ로 떨어지기 전, 브로커가 컨슈머에게 다시 메시지를 밀어주는 재전송 간격을 어떻게 최적화하고 통제해야 하는지 그 아키텍처적 원리를 상세히 해부합니다.


1. 지수 백오프(Exponential Backoff)의 개념과 도입 이유

메시지 처리 실패 시 브로커가 다시 메시지를 전달하는 것을 재전송(Redelivery)이라고 합니다. 이때 재전송을 얼마나 기다렸다가 할 것인지를 결정하는 정책이 필요합니다.

  • 고정 지연(Fixed Delay)의 한계: 매번 1초씩 기다렸다가 재시도한다고 가정해 보겠습니다. 만약 타겟 DB가 재부팅 중이라 10초의 복구 시간이 필요하다면, 앞선 9번의 재시도는 DB에 무의미한 커넥션 시도만 유발하며 허무하게 실패 횟수를 소진해 버립니다.
  • 지수 백오프의 원리: 재시도 횟수가 늘어날수록 기다리는 대기 시간을 이전보다 배수 단위로 팍팍 늘려가는 영리한 방식입니다. 처음 실패했을 때는 "잠깐의 네트워크 튐 현상인가?" 하고 1초만 기다려봅니다. 또 실패하면 "DB가 무거운 작업을 하나 보네?" 하고 2초를 기다립니다. 또 실패하면 4초, 그다음은 8초, 16초로 대기 시간을 늘립니다. 시스템이 스스로 회복할 수 있는 충분한 '숨 고르기' 시간을 벌어주는 강력한 패턴입니다.

2. 지수 증가의 함정과 '최대 지연 시간'의 절대적 필요성

지수 백오프는 훌륭하지만, 이 방식에는 시스템을 좀먹는 치명적인 함정이 숨어있습니다. 대기 시간이 무한정 길어질 수 있다는 점입니다.

만약 재시도를 10번까지 허용해 두었고 대기 시간이 계속 2배씩 늘어난다면 어떻게 될까요? 처음엔 1초, 2초, 4초로 귀엽게 늘어나던 시간이 6번째에는 약 1분, 9번째에는 수십 분, 나중에는 몇 시간이 지난 뒤에야 마지막 재시도를 수행하는 황당한 상황이 발생합니다.

메시지의 실시간성이 생명인 비즈니스에서 한 시간 뒤에 재시도하는 것은 아무런 의미가 없습니다. 오히려 컨슈머 시스템의 스레드를 낭비하고 브로커의 메모리를 불필요하게 점유할 뿐입니다.

이를 원천 차단하기 위해 반드시 설정해야 하는 마지노선이 바로 maximumRedeliveryDelay (최대 지연 시간) 입니다.
이 값을 예를 들어 60초(60000ms)로 설정해 두면, 대기 시간이 1초 -> 2초 -> 4초 -> 8초 -> 16초 -> 32초 -> 64초로 넘어가는 시점에 지수 증가를 강제로 멈춥니다. 그 이후의 재시도부터는 무조건 최대 지연 시간인 60초만 기다린 후 재시도하게 됩니다. 시스템의 응답성과 복구 한계 시간을 아키텍트가 직접 통제하는 핵심 안전장치입니다.


3. Redelivery Policy 핵심 설정 가이드 (Spring Boot & ActiveMQ)

실제 운영 환경에서 이 지수 백오프와 최대 지연 시간을 조율하기 위해 설정해야 하는 4가지 핵심 파라미터는 다음과 같습니다.

  • useExponentialBackOff: 지수 백오프 사용 여부를 켭니다 (true).
  • initialRedeliveryDelay: 최초 실패 후 첫 번째 재시도를 하기 전 기다릴 기초 대기 시간입니다. (예: 1000ms)
  • backOffMultiplier: 다음 재시도 대기 시간을 계산할 때 곱할 배수입니다. 보통 2.0 또는 3.0을 사용합니다.
  • maximumRedeliveryDelay: 지수 증가가 멈추는 상한선(최대 지연 시간)입니다. (예: 60000ms)
  • maximumRedeliveries: DLQ로 보내기 전까지 허용할 총 재시도 횟수입니다. (예: 5회)

Spring Boot 프레임워크 환경이라면 ActiveMQConnectionFactory를 빈(Bean)으로 등록할 때 이 정책을 다음과 같이 주입할 수 있습니다.

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.RedeliveryPolicy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ActiveMQConfig {

    @Bean
    public ActiveMQConnectionFactory connectionFactory() {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory();
        factory.setBrokerURL("tcp://localhost:61616");

        // 재전송 정책(Redelivery Policy) 인스턴스 생성
        RedeliveryPolicy policy = new RedeliveryPolicy();

        // 지수 백오프 활성화
        policy.setUseExponentialBackOff(true);
        // 초기 대기 시간 1초
        policy.setInitialRedeliveryDelay(1000);
        // 대기 시간 증가 배수 2배
        policy.setBackOffMultiplier(2.0);
        // ★ 최대 지연 시간 30초 캡(Cap) 적용
        policy.setMaximumRedeliveryDelay(30000);
        // 최대 재시도 횟수 5회 (이후 DLQ 이동)
        policy.setMaximumRedeliveries(5);

        factory.setRedeliveryPolicy(policy);
        return factory;
    }
}

이렇게 설정하면 재시도 대기 시간은 1초 -> 2초 -> 4초 -> 8초 -> 16초 로 진행되며, 마지막 5번째 시도 후에도 실패하면 비로소 브로커는 이 메시지를 포기하고 DLQ로 라우팅합니다.


4. 아키텍처적 트레이드오프: 스레드 블로킹(Thread Blocking)의 위험성

지수 백오프를 적용할 때 인프라 엔지니어가 반드시 명심해야 할 치명적인 아키텍처적 부작용이 있습니다. 바로 컨슈머 스레드의 블로킹 현상입니다.

ActiveMQ Classic 환경에서 클라이언트가 메시지 처리에 실패하여 Exception을 던지면, JMS 세션(Session)은 지정된 대기 시간(예: 8초) 동안 해당 컨슈머 스레드를 Thread.sleep() 상태로 재워버립니다.
만약 동시 처리 스레드(Concurrent Consumers)를 10개로 설정해 두었는데, 10개의 스레드가 모두 일시적 장애를 겪어 백오프 대기 상태에 빠진다면 어떻게 될까요? 컨슈머 애플리케이션은 아무 일도 하지 못하고 완전히 얼어붙게(Freeze) 됩니다. 새로 큐에 들어오는 정상적인 메시지조차 스레드가 없어 처리하지 못하는 연쇄 장애가 발생합니다.

해결 모범 사례:

  1. 재시도 횟수의 최소화: 브로커 레벨의 재전송 횟수는 3~5회 정도로 짧게 가져가고 최대 지연 시간도 10초 이내로 타이트하게 잡아야 스레드 고갈을 막을 수 있습니다.
  2. 논블로킹 지연 큐(Delayed Queue)로의 전환: 최신 아키텍처에서는 브로커의 기본 Redelivery 기능에 전적으로 의존하지 않습니다. 처리에 실패한 메시지는 즉시 컨슈머 스레드를 반환(ACK 처리)하되, 브로커 내부에 있는 별도의 '재시도 전용 지연 큐(Retry Delay Queue)'로 메시지를 다시 발행(Publish)하는 방식을 취합니다. 이 방식을 사용하면 대기 시간 동안 컨슈머 스레드가 블로킹되지 않아 시스템의 처리량(Throughput)을 방어할 수 있습니다.

요약하자면 지수 백오프는 일시적 장애 상황에서 시스템이 스스로 회복할 수 있는 황금 같은 시간을 벌어주지만, 최대 지연 시간(Maximum Delay)이라는 확실한 제동 장치 없이는 시스템의 실시간성을 갉아먹는 독이 될 수 있습니다. 비즈니스 도메인의 허용 지연 시간을 면밀히 분석하여 최적의 백오프 곡선을 설계하시기 바랍니다.

반응형