Foundry
메시징
중급
핵심

DLQ와 재시도 설계

실패 한 건이 큐 전체를 막지 않게 하는 출구 설계

DLQ와 재시도 설계

실패는 두 종류다

  • 일시 실패: 타임아웃, 커넥션 끊김, 5xx → 재시도로 해결된다
  • 영구 실패: 스키마 불일치, 없는 사용자, 검증 오류 → 몇 번을 재시도해도 같은 결과(poison message)
  • 구분 없이 무한 재시도하면 한 건이 뒤의 모든 메시지를 막는다(head-of-line blocking)

재시도 파라미터

요소권장
간격지수 백오프 (1s, 2s, 4s, 8s)
지터랜덤 오프셋을 더해 동시 재시도 몰림 방지
한도3~5회, 초과 시 DLQ로 이동
위치즉시 재시도는 짧게, 긴 대기는 별도 재시도 토픽과 지연 큐로

DLQ 운영

  • DLQ는 쓰레기통이 아니라 알림 대상이다. 유입 건수에 경보를 붙인다
  • 원본 메시지와 함께 실패 원인, 스택트레이스, 시도 횟수를 남긴다
  • 재처리는 소비자가 멱등하다는 전제에서만 안전하다

실무 포인트

  • 처리 시간이 가시성 타임아웃이나 폴 간격보다 길면 정상 처리 중인데도 재전송이 일어난다
  • 하류 서비스 전체 장애 시 전건이 DLQ로 쏟아진다. 서킷 브레이커로 유입을 먼저 끊는다
  • 재시도 대상이 아닌 예외는 코드에서 명시적으로 구분해 즉시 DLQ로 보낸다
면접에서 이렇게 나옵니다

Q.재시도 횟수 제한이 없으면 어떤 문제가 생기나요?

처리할 수 없는 메시지가 앞을 막아 뒤의 정상 메시지도 처리되지 않습니다.

문제내용
큐 정체같은 메시지를 무한 재시도하며 진행이 멈춘다
랙 증가뒤의 메시지가 계속 쌓인다
자원 낭비실패할 것이 뻔한 처리를 반복한다
외부 부담상대에게 같은 실패 요청을 계속 보낸다
로그 폭증같은 오류가 초당 수백 건

원인이 영구적일 때 특히 문제가 됩니다. 메시지 형식이 잘못됐거나 참조하는 데이터가 삭제된 경우라면 몇 번을 시도해도 성공하지 않습니다.

대응내용
횟수 상한몇 번 실패하면 실패 큐로 보낸다
지수 백오프간격을 늘려 부담을 줄인다
실패 유형 구분일시적인 것만 재시도한다
건너뛰기순서가 중요하지 않으면 넘기고 진행한다
알림실패 큐에 쌓이면 사람이 보게 한다

흔한 실수: 재시도를 늘려 안전하다고 느끼는 것. 유실은 막지만 막힌 큐 때문에 뒤의 모든 메시지가 지연됩니다. 하나를 살리려다 전체를 멈추는 거래입니다.

Q.일시 실패와 영구 실패를 코드에서 어떻게 구분하시겠어요?

다시 하면 될 수 있는가로 나눕니다. 그리고 그 판정을 코드에 명시합니다.

분류처리
일시네트워크 오류, 타임아웃, 5xx, 잠금 경합, 한도 초과재시도
영구형식 오류, 필수 값 누락, 4xx, 없는 대상 참조즉시 실패 큐로
애매잔액 부족, 상태 충돌업무 규칙에 따라 판단

구현은 예외를 두 종류로 나누는 방식이 깔끔합니다. 재시도 가능 예외와 불가 예외를 정의하고, 소비자 바깥의 공통 처리에서 그 종류를 보고 분기합니다. 그러면 개별 처리 코드가 재시도 정책을 몰라도 됩니다.

세 번째 분류가 판단이 필요한 자리입니다. 잔액 부족은 지금 실패지만 나중에 충전하면 성공할 수 있습니다. 그런 경우 짧은 재시도로는 해결되지 않으므로, 지연 큐에 넣어 몇 시간 뒤 재시도하거나 사용자에게 알리고 종료하는 편이 낫습니다.

흔한 실수: 모든 예외를 같게 다루는 것. 형식이 잘못된 메시지를 100번 재시도하면 실패가 확실한 처리에 자원을 쓰고 큐를 막습니다. 반대로 타임아웃을 영구 실패로 보면 회복 가능한 것을 버립니다.

Q.DLQ에 쌓인 메시지를 재처리할 때 주의할 점은?

주의내용
원인을 먼저 고친다그대로 넣으면 다시 실패 큐로 온다
멱등성 확인일부는 이미 처리됐을 수 있다
순서원래 순서가 필요했다면 그 순서대로 넣어야 한다
시점 의존오래된 메시지가 지금 유효한지 본다
유입 속도한꺼번에 넣으면 정상 처리까지 밀린다
원본 보존재처리 전에 사본을 남긴다

네 번째가 자주 간과됩니다. 3일 전 "재고 차감" 메시지를 지금 처리하면 이미 취소된 주문의 재고를 깎을 수 있습니다. 시간이 지나 의미가 없어진 메시지는 재처리하지 않아야 합니다.

다섯 번째도 실무에서 사고를 만듭니다. 실패 큐에 10만 건이 쌓였는데 한 번에 되돌리면 그것이 부하가 되어 정상 처리도 막힙니다. 속도를 제한해 조금씩 넣습니다.

절차로 정리하면 이렇습니다.

순서하는 일
1실패 원인을 분류한다. 같은 원인끼리 묶인다
2원인을 고치고 배포한다
3소수로 시험 재처리한다
4성공을 확인하고 속도를 제한해 나머지를 넣는다
5유효하지 않은 것은 재처리하지 않고 기록만 남긴다

흔한 실수: 실패 큐를 자동 재처리로 연결하는 것. 원인이 그대로면 무한 순환이 됩니다.

Q.지수 백오프에 지터를 추가하는 이유는 무엇인가요?

여러 클라이언트가 같은 시점에 동시에 재시도하는 것을 막기 위해서입니다.

지터가 없으면 이렇게 됩니다.

시점상황
0초장애로 1,000개 요청이 동시에 실패한다
1초 후1,000개가 동시에 재시도한다
3초 후다시 1,000개가 동시에

간격은 늘어나지만 몰리는 것은 그대로입니다. 회복하려는 상대에게 주기적으로 같은 크기의 충격을 줍니다.

지터는 간격에 무작위 편차를 더해 요청을 흩뿌립니다. 1초 대기가 아니라 0에서 1초 사이의 무작위 시간으로 기다리면, 1,000개가 그 구간에 퍼집니다.

방식내용
전체 무작위0 에서 계산된 간격 사이의 무작위 값
절반 고정간격의 절반 + 나머지 절반 안의 무작위 값
상한간격이 무한히 커지지 않게 최댓값을 둔다

같은 원리가 캐시 만료에도 적용됩니다. 만료 시각을 흩뿌려 동시 만료를 막는 것과 같은 문제입니다.

흔한 실수: 지수 백오프만으로 충분하다고 보는 것. 간격을 늘리는 것은 한 클라이언트의 부담을 줄이고, 지터는 여러 클라이언트가 겹치는 것을 막습니다. 서로 다른 문제를 해결합니다.

먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.

읽었으면 문제로 확인해보세요

메시징 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.