SQS 예약 발송에서 visibility timeout과 중복 처리를 점검한 경험

목차
예약 발송을 SQS로 처리하면서 배운 점은 메시지 처리 실패와 중복 수신을 따로 봐야 한다는 것이었습니다. DLQ에 들어온 결과만 보고 DB 격리 수준을 바꾸면 문제의 일부만 건드릴 수 있습니다.
이 글을 다시 검토하면서 이전 설명의 오류를 찾았습니다. 서로 다른 기본키 100과 101을 각각 갱신하는 두 트랜잭션만으로는 제시했던 순환 대기가 만들어지지 않습니다. “인접한 기본키라 넥스트 키 락이 충돌했다”는 설명과 이를 입증하지 못하는 로그 예시는 삭제했습니다. 과거 실패율·MTTD 수치도 재현 자료와 연결할 수 없어 개선의 증거로 사용하지 않겠습니다.
처리 시간보다 짧은 visibility timeout
제 경력 기록에 남아 있는 사례는 처리에 약 45초가 필요한 작업에 visibility timeout이 30초로 설정되어 있던 상황입니다. 메시지를 받으면 큐에서 바로 삭제되는 것이 아니라 일정 시간 다른 소비자에게 보이지 않게 됩니다. 그 사이에 처리를 완료하고 삭제하지 않으면 다시 보일 수 있습니 다.
| 시점 | 첫 번째 소비자 | 큐에서의 상태 |
|---|---|---|
| 0초 | 메시지 수신, 처리 시작 | 일시적으로 숨김 |
| 30초 | 아직 처리 중 | visibility 만료, 재수신 가능 |
| 45초 | 작업 완료 | 다른 소비자도 이미 처리 중일 수 있음 |
당시 timeout을 90초로 조정했습니다. 다만 90초는 보편적인 권장값이 아닙니다. 처리 시간 분포와 외부 API timeout, 삭제에 걸리는 시간까지 보고 정해야 합니다. 변동이 큰 작업은 처리 진행에 맞춰 ChangeMessageVisibility로 연장하되 무한 연장하지 않도록 상한을 둡니다. AWS visibility timeout 문서에 수신·삭제·재노출의 관계가 설명되어 있습니다.
timeout을 늘려도 Standard SQS의 중복 전달 가능성은 사라지지 않습니다. 실패한 작업의 재시작이 늦어진다는 비용도 있습니다.
발송과 DB 커밋 사이의 틈
다음은 실패 지점을 보여 주는 의사 코드입 니다. 실제 서비스의 구현을 공개한 코드는 아닙니다.
메시지 수신
업무 키로 처리 상태 확인 및 실행 권한 선점
외부 발송 API 호출
발송 결과 저장
SQS 메시지 삭제외부 발송은 성공했는데 결과 저장이 실패하면 다음 소비자는 발송 성공 여부를 알 수 없습니다. DB 트랜잭션을 길게 잡아도 외부 발송까지 원자적으로 되돌릴 수는 없습니다.
외부 API가 멱등성 키를 지원하면 같은 업무에는 같은 키를 사용해야 합니다. 지원하지 않으면 발송 결과 조회나 대사가 필요하고, 모호한 결과를 바로 재발송할지 사람이 확인할지 정책을 정해야 합니다. DB에는 업무 식별자에 유일성 제약을 두거나 조건부 UPDATE로 실행 권한을 확보합니다. 단순히 처리 완료 플래그를 먼저 SELECT하는 것만으로는 두 소비자의 동시 실행을 막지 못합니다.
실행 선점 후 소비자가 죽는 경우도 있으므로 PROCESSING 상태에는 소유자·만료 시각·복구 정책이 필요합니다. “처리 중”을 영구히 남겨 두면 중복 대신 누락을 만들 수 있습니다.
DB 데드락은 별도로 확인합니다
데드락 오류가 관찰되면 전체 deadlock report에서 각 트랜잭션이 보유한 락과 기다리는 락을 연결해야 합니다. 기본키 동등 조건, 범위 조건, 보조 인덱스, 외래키 검증은 락 범위가 다릅니다.
REPEATABLE READ에서도 존재하는 유일 키의 정확한 한 행 조회·갱신은 일반적으로 레코드 락을 사용합니다. READ COMMITTED에서도 외래키·중복 키 검사 등의 gap locking과 데드락 가능성이 남습니다. 격리 수준 변경은 순환 대기의 보편적인 해결책이 아닙니다. MySQL InnoDB 락 문서를 기준으로 실제 SQL과 실행 계획을 함께 확인해야 합니다.
외부 API를 기다리는 동안 DB 락을 붙잡는 구조라면, 짧은 상태 전이 트랜잭션과 외부 호출을 분리하는 방안을 검토합니다. 이때 앞서 설명한 불확실한 외부 성공을 처리하는 설계가 반드시 같이 있어야 합니다.
DLQ는 재발송 버튼이 아닙니다
DLQ에는 반복 실 패한 메시지가 남습니다. 소비자의 receive count가 정책의 maxReceiveCount에 도달하는 과정과 애플리케이션 내부 재시도를 혼동하지 않아야 합니다.
운영에서는 원본 큐의 오래된 메시지와 DLQ의 보이는 메시지 수를 함께 관찰합니다. DLQ 재처리는 원인을 고친 뒤 소량부터 시작하고, 원래 업무 키를 유지하며 외부 성공 여부를 대사합니다. 자동 재처리 횟수와 속도에는 제한을 둡니다.
제가 이 경험에서 가져온 판단 순서는 처리 시간과 visibility 설정을 먼저 대조하고, 중복 수신을 견디는 업무 처리 경계를 확인한 뒤, DB 오류가 있다면 실제 락 대기 관계를 따로 추적하는 것입니다. 큐 설정 하나와 트랜잭션 어노테이션 하나가 발송의 정확성을 대신해 주지는 않습니다.
