🔥 Visibility Timeout: 메시지가 보이지 않는 이유

#SQS#Visibility Timeout#AWS#메시지 큐
1192자
15분

SQS visibility timeout 타임라인 도식, 메시지를 받은 뒤 30초 동안 숨고 삭제되면 끝나며 만료되면 다시 보이게 된다

처음 SQS 소비자를 붙였을 때, 메시지가 분명히 남아 있는데 ReceiveMessage 가 빈 응답을 돌려준 순간이 있었다. 콘솔에는 아직 처리하지 않은 메시지가 있었고, 로그에는 방금 받은 작업이 끝나지 않았다고 적혀 있었다. 나는 큐가 비었다고 착각했지만, 실제로는 메시지가 사라진 게 아니라 잠깐 숨어 있었을 뿐이다.

visibility timeout 은 전달 계약과 DeleteMessage 사이를 설명할 때 빠질 수 없는 설정이다. DeleteMessage 의미는 SQS란 무엇인가 에서 다뤘고, 전달 계약 차이는 Standard vs FIFO 에서, 실패 메시지 격리는 Dead Letter Queue: 실패 메시지의 격리 에서 따로 정리했다. 이번 글은 visibility timeout 하나만 다룬다.

visibility timeout 은 잠금이 아니라 처리 시간 슬롯이다

visible 구간과 in-flight 구간을 나란히 둔 SQS 상태 도식, 받은 메시지는 숨고 삭제되기 전까지 in-flight 로 남는다

visibility timeout 은 메시지를 잠그는 기능이 아니다. 소비자가 받은 메시지를 일정 시간 다른 소비자에게서 숨겨 두는 처리 시간 슬롯이다.

AWS 문서 기준으로 큐 attribute VisibilityTimeout 기본값은 30초다. 설정 범위는 0초에서 12시간, 즉 43,200초다 (AWS Developer Guide). 이 시간 동안 메시지는 in-flight 상태가 된다. 받은 뒤 아직 삭제하지 않은 메시지라는 뜻이다.

이 값을 잠금으로 오해하면 운영 판단이 어긋난다. 잠금이라면 소비자 A 가 받는 순간 다른 소비자 B 는 절대 같은 메시지를 건드리지 못해야 한다. SQS 는 그렇게 약속하지 않는다. 메시지를 잃어버리지 않기 위해 잠깐 숨겨 둘 뿐이다. 그래서 이 시간은 소유권이 아니라 재시도까지 남겨 둔 유예 시간에 가깝다.

예를 들어 주문 확인 메일 전송이 보통 5초 안에 끝난다면 30초 기본값으로도 충분하다. 반대로 외부 API 세 곳을 차례로 부르는 작업이 90초 걸린다면 30초는 짧다. 이 경우 메시지는 아직 처리 중인데도 다시 보이게 된다.

단서는 분명하다. 시간을 길게 잡는다고 안전해지지 않는다. 너무 길면 실패한 메시지가 다시 나오기까지 오래 기다려야 하고, 너무 짧으면 아직 끝나지 않은 메시지가 다시 나와 중복 처리가 늘어난다.

ReceiveMessage 는 수신 순간부터 그 시간을 깎는다

ReceiveMessage 요청 시점 도식, 큐 기본 30초와 요청별 VisibilityTimeout 값이 받은 메시지에만 적용되는 차이를 보여준다

ReceiveMessage 는 메시지를 건네는 순간부터 visibility timeout 을 깎기 시작한다. 응답을 받은 뒤 애플리케이션이 무엇을 하든 시간은 이미 지나간다.

ReceiveMessage 요청에 VisibilityTimeout 을 넣지 않으면 큐 기본값을 쓴다. 넣으면 그 응답으로 받은 메시지들에만 다른 시간을 적용한다 (ReceiveMessage). 메시지별 설정 범위도 0초에서 43,200초다. 큐 기본값은 그대로 두고, 이번 호출에만 다른 숫자를 얹는 방식이다.

이 차이는 작업 종류가 섞인 큐에서 유용하다. 예를 들어 대부분 작업은 10초 안에 끝나지만, 이미지 압축처럼 가끔 2분 걸리는 작업이 있다면 큐 전체를 2분으로 늘릴 필요는 없다. 짧은 작업은 기본 30초로 받고, 긴 작업을 받는 소비자만 ReceiveMessage 에서 120초를 요청하면 된다.

단서는 두 가지다. 첫째, 이 값은 메시지를 받은 뒤에야 효력을 갖는다. 큐에 쌓여 있는 동안의 보관 시간과는 관계가 없다. 둘째, 처음 받은 순간부터 시간이 줄어드니 네트워크 지연과 애플리케이션 시작 시간도 같이 계산해야 한다.

ChangeMessageVisibility 는 작업 시간이 흔들릴 때만 쓴다

heartbeat 연장 도식, 처음 2분을 주고 1분마다 2분씩 늘려 in-flight 시간이 계단식으로 길어지는 모습을 보여준다

ChangeMessageVisibility 는 작업 시간이 흔들릴 때 쓰는 연장 장치다. 처음부터 긴 시간을 고정하는 대신, 아직 처리 중일 때만 조금씩 늘리는 방식이다.

AWS 문서는 처리 시간이 정확하지 않을 때 heartbeat 패턴을 권한다. 예시도 단순하다. 처음 2분을 주고, 아직 작업 중이면 1분마다 2분씩 더 연장한다 (AWS Developer Guide). ChangeMessageVisibilityChangeMessageVisibilityBatch 는 이 연장에 쓰는 API 다. VisibilityTimeout 매개변수 범위는 0초에서 43,200초다 (ChangeMessageVisibility).

이 방식이 필요한 이유는 두 숫자가 서로 다른 뜻을 가지기 때문이다. 큐 기본값은 평소 처리 시간에 맞춘 값이다. 연장 호출은 지금 이 메시지가 예상보다 오래 걸린다는 신호다. 둘을 분리해야 재시도 지연과 중복 처리 사이에서 균형을 잡을 수 있다.

예를 들어 PDF 생성 작업이 보통 40초지만, 입력 데이터가 크면 3분 걸릴 수 있다고 하자. 큐 기본값을 3분으로 잡으면 실패한 보통 작업도 3분 뒤에야 다시 나온다. 이 경우 기본값은 1분으로 두고, 아직 끝나지 않은 메시지만 30초나 1분 단위로 늘리는 편이 낫다. 반대로 처리를 포기하기로 했다면 VisibilityTimeout 을 0초로 바꿔 즉시 다시 보이게 할 수도 있다.

단서는 상한이다. 12시간 최대는 첫 ReceiveMessage 시점부터 계산한다. 연장 호출을 여러 번 보내도 12시간 시계를 다시 시작하지 않는다. 긴 작업이 몇 시간씩 이어지고 종료 시점도 읽기 어렵다면, SQS 위에서 heartbeat 를 계속 보내는 설계가 맞는지부터 다시 물어야 한다.

시간이 끝나면 같은 메시지가 다시 나온다

소비자 A 가 받은 메시지가 30초를 넘겨 다시 보이게 되고 소비자 B 가 같은 메시지를 받는 중복 처리 도식

visibility timeout 이 끝나면 메시지는 다시 보인다. 이때 같은 메시지가 다시 실행될 수 있다. at-least-once 전달이 운영에서 문제로 드러나는 순간이다.

소비자 A 가 메시지를 받고 30초 안에 지우지 못했다고 하자. 31초쯤 다른 소비자 B 가 같은 메시지를 다시 받을 수 있다. A 가 조금 늦게 끝나더라도 B 가 이미 같은 작업을 시작했을 수 있다. 그래서 visibility timeout 은 중복 전달을 막는 장치가 아니라, 중복 전달 시점을 늦추는 장치다.

이 재노출은 DLQ 와도 바로 닿는다. 메시지를 다시 받을 때마다 ApproximateReceiveCount 는 올라간다. 그 값이 source 큐의 maxReceiveCount 를 넘으면 Dead Letter Queue: 실패 메시지의 격리 에서 설명한 경로로 이동한다. DLQ 는 반복 실패한 메시지를 따로 옮겨 두는 큐다. visibility timeout 은 그 전에 몇 번 다시 받을지를 정한다.

FIFO 도 같은 원리로 움직이지만 비용의 방향이 다르다. 같은 MessageGroupId 안에서는 앞 메시지가 in-flight 인 동안 뒤 메시지가 나오지 않는다 (AWS Developer Guide). 앞 메시지 처리 시간이 길거나 timeout 값이 과하면, 그 그룹 뒤쪽 작업도 같이 멈춘다.

단서는 삭제 시점이다. 처리 성공 직후 DeleteMessage 를 빠뜨리면 작업은 끝났는데 메시지는 다시 나온다. 소비자 코드는 비즈니스 로직뿐 아니라 삭제 호출까지 포함해 한 묶음으로 다뤄야 한다.

visibility timeout 으로 해결되지 않는 경우가 있다

긴 트랜스코딩 작업과 중복 결제 작업을 나란히 둔 비교 도식, timeout 조정만으로는 해결되지 않는 두 경우를 보여준다

visibility timeout 숫자를 바꾼다고 모든 작업이 편해지지는 않는다. SQS 가 맞지 않는 경우는 꽤 분명하다.

  1. 처리 시간이 크게 흔들리는 긴 작업이다. 긴 영상 트랜스코딩처럼 어떤 메시지는 5분, 어떤 메시지는 40분 걸리면 고정 timeout 값으로 균형을 잡기 어렵다. 짧게 두면 중복 처리가 늘고, 길게 두면 실패 재시도가 늦어진다. heartbeat 를 1분마다 보내야 할 정도라면 작업을 더 작은 단계로 나누거나 Step Functions 같은 다른 방식을 검토해야 한다.

  2. 중복 처리 자체가 업무 규칙과 충돌하는 작업이다. 카드 결제, 쿠폰 소진, 재고 차감처럼 한 번 더 실행되는 순간 손실이 나는 작업은 visibility timeout 만으로 막을 수 없다. 이런 작업은 소비자 쪽 idempotency key, 데이터베이스 제약 조건, 별도 상태 테이블 같은 장치가 먼저다. 이런 장치를 둘 수 없다면 SQS 선택부터 다시 생각해야 한다.

이 두 경우의 공통점은 timeout 조정이 본질을 바꾸지 못한다는 점이다. SQS 는 재시도 시점을 제어할 뿐이고, 작업 시간을 예측하게 해 주거나 중복 실행을 불가능하게 만들지는 않는다.

관리형이 감추는 비용은 운영 판단이다

운영자가 직접 맡는 항목 도식, IAM 권한 선택과 VPC endpoint 여부와 CloudWatch NotVisible 메트릭 추적과 idempotency 설계를 한 화면에 비교한다

관리형이 없애는 것은 큐 서버 운영이다. visibility timeout 값 결정, 연장 주기 선택, 관측, 중복 처리 방어는 그대로 사용자 몫이다.

권한부터 직접 고른다. 소비자는 ReceiveMessage, DeleteMessage, ChangeMessageVisibility 권한이 있어야 한다. private subnet 에 둘 때는 VPC endpoint 경로도 점검해야 한다. 관측도 남는다. ApproximateNumberOfMessagesNotVisible 이 계속 높게 머물면 처리 시간이 길거나 삭제가 늦다는 뜻일 수 있다. ApproximateAgeOfOldestMessage 와 DLQ 적재 수도 함께 확인해야 원인을 좁힐 수 있다.

비용도 판단에서 생긴다. 30초 기본값을 10분으로 올리면 실패 복구가 느려진다. 반대로 5초로 줄이면 같은 메시지를 두 번 처리할 가능성이 커진다. heartbeat 를 넣으면 네트워크 호출이 늘고, idempotency 저장소를 두면 데이터 모델이 더 복잡해질 수 있다. 관리형 서비스가 대신하지 않는 비용은 이런 숫자 선택 비용이다.

지금은 visibility timeout 을 숨김 옵션이 아니라 재시도 시각을 정하는 설정으로 이해한다. 이렇게 두면 SQS 안에서 풀 문제와 다른 도구로 넘길 문제를 더 분명하게 나눌 수 있다.

YouTube 영상

채널 보기
숫자 하나가 AI 모델의 운명을 바꾼다? | 선형대수학
행렬의 가장 중요한 연산 - 행렬 곱셈 | 선형대수학
트라이(Trie)를 이용한 자동 완성 알고리즘 | Trie 자료구조 이야기
벡터의 정의와 덧셈 연산 | 선형대수학
숫자 하나가 AI 모델의 운명을 바꾼다? | 선형대수학
내적의 기하학적 의미와 코사인 유사도 원리 | 선형대수학
스칼라 곱셈과 내적의 기하학적 의미 | 선형대수학
Trie 자료구조 파이썬 구현: Search와 Starts With 연산 | Trie 자료구조 이야기