🔥 Dead Letter Queue: 실패 메시지의 격리
강의 목차

운영 중이던 큐 하나에서 ApproximateReceiveCount 가 50까지 올라간 메시지를 본 적이 있다. 소비자는 실패했고 DeleteMessage 는 끝내 호출되지 않았다. 메시지는 visibility timeout 이 끝날 때마다 다시 나왔고, 장애는 끝났는데 큐는 끝나지 않았다. 그때 붙인 것이 DLQ 였다. Lambda 비동기 destination DLQ 나 EventBridge target DLQ 에서도 같은 단어가 나오지만, SQS DLQ 는 호출자 설정이 아니라 source 큐 자체의 attribute 다.
DLQ 는 특수 큐가 아니라 source 큐의 attribute 다

DLQ 는 별도 서비스가 아니다. 일반 SQS 큐 하나를 만들고 source 큐의 RedrivePolicy 가 그 ARN 을 가리키면 그 큐가 DLQ 역할을 한다. AWS 문서도 source queue 와 dead-letter queue 관계를 이 방식으로 설명한다 (AWS Developer Guide).
핵심 속성은 두 필드다. deadLetterTargetArn 과 maxReceiveCount 다.
{
"deadLetterTargetArn": "arn:aws:sqs:ap-northeast-2:123456789012:orders-dlq",
"maxReceiveCount": 5
}{
"deadLetterTargetArn": "arn:aws:sqs:ap-northeast-2:123456789012:orders-dlq",
"maxReceiveCount": 5
}큐 유형은 반드시 맞춰야 한다. Standard source 는 Standard DLQ 만 쓴다. FIFO source 는 FIFO DLQ 만 쓴다. Standard 와 FIFO 차이 에서 본 전달 계약을 DLQ 도 그대로 따른다.
maxReceiveCount 는 실패를 격리로 바꾼다

maxReceiveCount 는 몇 번까지 다시 받게 둘지 정하는 숫자다. 메시지가 ReceiveMessage 로 전달된 뒤 삭제되지 않고 visibility timeout 만료를 반복하면 ApproximateReceiveCount 가 올라간다. 그 값이 임계값을 넘으면 SQS 가 메시지를 source 큐에서 DLQ 로 옮긴다.
숫자는 실패 성격에 맞춰 잡는다. 일시적 오류를 몇 번 흡수하고 싶다면 5에서 10이 흔하다. 한 번 실패하면 거의 항상 코드 버그나 데이터 오류로 이어지는 작업이라면 1에서 3이 더 낫다. 값을 너무 낮게 두면 일시 장애가 곧바로 DLQ 적재로 바뀐다. 값을 너무 높게 두면 같은 poison message 가 다른 메시지를 오래 막는다.
Standard 큐에는 한 가지 특성이 더 있다. AWS 문서는 maxReceiveCount 가 3보다 큰 Standard 큐에서, 삭제되지 않은 메시지를 세 번 이상 받으면 SQS 가 그 메시지를 큐 뒤쪽으로 보낸다고 적는다. 오래 실패하는 메시지 하나가 나머지 메시지를 계속 막지 않게 하려는 동작이다.
DLQ 의 보관 기간은 source 보다 길게 둔다
DLQ 로 옮겼다고 메시지가 영구 보관되는 것은 아니다. retention period 가 끝나면 SQS 가 DLQ 안 메시지도 같이 지운다. 이 숫자를 source 큐와 같게 두면 원인을 보기 전에 메시지가 사라질 수 있다.
여기서 Standard 와 FIFO 는 또 다르다. Standard 는 원래 enqueue timestamp 를 유지한다. source 큐에서 1일을 보낸 메시지가 DLQ 로 옮겨지면, DLQ 에서 새로 4일을 받는 것이 아니라 남은 기간만 산다. FIFO 는 DLQ 로 옮길 때 SQS 가 enqueue timestamp 를 새로 기록한다. 그래서 DLQ retention 은 source 보다 길게 두는 편이 안전하다 (AWS Developer Guide).
redrive 는 DLQ 에서 source 로 다시 보내는 별도 작업이다

source 에서 DLQ 로 가는 규칙과, DLQ 에서 다시 꺼내는 작업은 다른 기능이다. 앞은 RedrivePolicy 다. 뒤는 redrive task 다.
콘솔에서는 Start DLQ redrive 로 시작한다. API 에서는 StartMessageMoveTask, ListMessageMoveTasks, CancelMessageMoveTask 를 쓴다. 기본 목적지는 원래 source 큐다. 같은 타입이라면 다른 큐를 목적지로 둘 수도 있다. 이 작업은 비동기이고, 큐당 동시에 하나만 돈다. 콘솔은 system optimized 속도와 사용자 지정 속도를 같이 노출하며, 사용자 지정 최대값은 초당 500 메시지다 (AWS Developer Guide).
시점도 구분해 둘 필요가 있다. 표준 큐의 콘솔 redrive to source 는 2021년 12월 1일에 먼저 나왔고 (AWS What's New), StartMessageMoveTask 계열 SDK·CLI API 는 2023년 6월 8일에 나왔다 (AWS News Blog). 장애 복구 자동화를 짤 때는 둘을 같은 기능으로 보면 안 된다.
RedriveAllowPolicy 는 DLQ 쪽 화이트리스트다

RedriveAllowPolicy 는 DLQ 큐의 attribute 다. 어떤 source 큐가 이 큐를 자기 DLQ 로 지정할 수 있는지 정한다. source 쪽 손잡이가 RedrivePolicy 라면, DLQ 쪽 손잡이는 RedriveAllowPolicy 다.
{
"redrivePermission": "byQueue",
"sourceQueueArns": [
"arn:aws:sqs:ap-northeast-2:123456789012:orders"
]
}{
"redrivePermission": "byQueue",
"sourceQueueArns": [
"arn:aws:sqs:ap-northeast-2:123456789012:orders"
]
}선택지는 셋이다. allowAll, denyAll, byQueue 다. byQueue 를 쓰면 ARN 기준으로 최대 10개 source 큐를 적는다. IaC 환경에서는 이 속성이 특히 유용하다. 이름이 비슷한 큐가 많을수록, 의도하지 않은 source 가 공용 DLQ 를 가리키는 실수를 막아야 하기 때문이다. 관련 파라미터는 SetQueueAttributes API 문서에도 정리돼 있다 (SetQueueAttributes).
DLQ 가 답이 아닌 경우가 세 가지 있다

-
일시적 의존성 장애는 DLQ 보다 재시도 설정이 먼저다. 데이터베이스가 30초 멈췄다면 앞에서 살펴본 SQS 큐의 추상에서 본 visibility timeout 과 소비자 backoff 로 흡수하는 편이 낫다.
-
코드 버그는 DLQ 가 고쳐 주지 않는다. DLQ 는 실패 메시지를 격리할 뿐이다. 결국 코드를 고치고 redrive 할지, 메시지를 폐기할지 운영자가 정해야 한다.
-
DLQ 를 읽지 않으면 와이어링 자체가 의미가 없다. 알람도 없고 분류 규칙도 없으면, DLQ 에 쌓인 실패 메시지를 누구도 다시 들여다보지 않는다.
FIFO 에서는 한 가지를 더 본다. 순서 자체가 업무 규칙이면 DLQ 이동이 그 순서를 바꿀 수 있다. AWS 문서도 정확한 순서를 깨면 안 되는 경우에는 FIFO 큐와 DLQ 조합을 신중히 보라고 적는다.
관리형이 감추는 비용은 운영 판단이다
DLQ 를 붙였다고 실패 처리가 끝나는 것은 아니다. 운영자는 최소 다섯 가지를 직접 맡는다. maxReceiveCount 값 결정, DLQ retention 설정, ApproximateNumberOfMessagesVisible 알람, ApproximateAgeOfOldestMessage 알람, redrive 시점 판단이다.
비용도 남는다. DLQ 도 그냥 SQS 큐다. 메시지가 source 에서 DLQ 로 이동하고, 나중에 다시 redrive 되면 그만큼 큐 요청이 더 생긴다. 실패 메시지를 본문 기준으로 분류할지, 오류 코드 기준으로 나눌지, 새 consumer 로 별도 처리할지도 사용자 몫이다. 관리형이 없앤 것은 큐 서버 운영이지, 실패 해석과 복구 판단이 아니다.
지금은 DLQ 를 실패 보관함이 아니라 실패 분리 장치로 본다. 큐에 붙였다는 사실보다, 그 뒤에 누가 메트릭을 보고 언제 다시 보낼지를 먼저 적게 된다.












