SQS Visibility Timeout 완벽 이해: 메시지 중복 처리 원인과 해결법

SQS 큐에서 메시지가 두 개의 컨슈머에 의해 동시에 처리되는 현상을 프로덕션에서 처음 마주치면, 대부분 애플리케이션 코드나 멱등성 로직을 먼저 의심한다. 하지만 실제 원인은 대부분 더 단순한 곳에 있다 — Visibility Timeout이 처리 시간보다 짧게 설정되어 있는 것이다. 이 설정 하나가 SQS 중복 처리 문제의 가장 흔한 원인이다.

TL;DR — SQS Visibility Timeout 핵심 요약

항목내용
Visibility Timeout 역할메시지를 수신한 후 다른 컨슈머에게 보이지 않도록 숨기는 시간
기본값30초
설정 범위0초 ~ 12시간
중복 처리 발생 조건처리 시간 > Visibility Timeout
권장 설정최대 처리 시간의 6배 이상 (AWS 권장)
런타임 연장 방법ChangeMessageVisibility API 호출
삭제 타이밍처리 완료 후 반드시 DeleteMessage 호출

SQS Visibility Timeout 동작 원리

SQS는 메시지 브로커가 아니라 분산 큐다. 컨슈머가 메시지를 ReceiveMessage로 가져가도 메시지는 큐에서 즉시 삭제되지 않는다. 대신 Visibility Timeout 동안 다른 컨슈머에게 보이지 않는 상태(invisible)로 전환된다. 컨슈머가 처리를 완료하고 DeleteMessage를 호출해야 비로소 큐에서 제거된다.

Visibility Timeout이 만료되기 전에 DeleteMessage가 호출되지 않으면, SQS는 해당 메시지를 다시 visible 상태로 되돌린다. 이 순간 다른 컨슈머(또는 동일 컨슈머의 다른 인스턴스)가 동일 메시지를 다시 수신할 수 있게 된다. 이것이 중복 처리의 정확한 메커니즘이다.

sequenceDiagram participant Q as SQS 큐 participant A as 컨슈머 A participant B as 컨슈머 B Q->>A: ReceiveMessage (메시지 전달) Note over Q: Visibility Timeout 시작 Note over Q: 메시지 Invisible 상태 alt 정상 처리 (타임아웃 전 완료) A->>A: 메시지 처리 중 A->>Q: DeleteMessage 호출 Note over Q: 메시지 영구 삭제 else 중복 처리 (타임아웃 만료) A->>A: 메시지 처리 중 (느림) Note over Q: Visibility Timeout 만료! Note over Q: 메시지 다시 Visible Q->>B: ReceiveMessage (동일 메시지 전달) Note over A,B: 중복 처리 발생 end
  1. ReceiveMessage: 컨슈머 A가 메시지를 수신하면 Visibility Timeout 타이머가 시작된다.
  2. Invisible 상태: Visibility Timeout 동안 다른 컨슈머에게 메시지가 보이지 않는다.
  3. 타임아웃 만료 시나리오: 처리가 완료되기 전에 타임아웃이 만료되면 메시지가 다시 visible 상태로 전환된다.
  4. 중복 수신: 컨슈머 B가 동일 메시지를 수신하여 중복 처리가 발생한다.
  5. 정상 시나리오: 타임아웃 만료 전에 DeleteMessage를 호출하면 메시지가 큐에서 영구 삭제된다.

중복 처리 문제 — 실제 진단 패턴

증상은 명확하다. 동일한 MessageId가 애플리케이션 로그에 두 번 이상 나타나고, 두 처리 시작 시각의 차이가 Visibility Timeout 설정값과 거의 일치한다. 이 패턴을 확인하는 순간 원인이 특정된다.

흔한 오진은 SQS의 'at-least-once delivery' 보장을 탓하는 것이다. 물론 SQS 표준 큐는 at-least-once를 보장하므로 극히 드물게 중복이 발생할 수 있다. 하지만 규칙적인 간격으로 중복이 발생한다면 Visibility Timeout 문제다. at-least-once로 인한 중복은 예측 불가능한 타이밍에 발생한다.

Visibility Timeout은 '처리 중' 표시판과 같다. 표시판이 너무 빨리 내려가면 다른 작업자가 같은 일을 시작한다. 표시판이 내려가기 전에 일을 끝내고 '완료' 도장(DeleteMessage)을 찍어야 한다.

Step 1: 현재 Visibility Timeout 설정 확인

먼저 큐의 현재 설정값을 확인한다. 콘솔에서 보이는 값이 실제 적용된 값인지 CLI로 검증하는 것이 더 확실하다 — 특히 IaC 도구로 관리되는 환경에서 콘솔 수정이 덮어쓰여지는 경우가 있기 때문이다.

aws sqs get-queue-attributes \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
  --attribute-names VisibilityTimeout ApproximateNumberOfMessagesNotVisible

ApproximateNumberOfMessagesNotVisible은 현재 invisible 상태인 메시지 수다. 이 값이 비정상적으로 높다면 처리가 지연되고 있거나 타임아웃 만료로 인해 메시지가 재순환되고 있다는 신호다.

Step 2: 실제 처리 시간 측정

Visibility Timeout을 얼마로 설정해야 하는지 알려면 실제 처리 시간의 분포를 알아야 한다. 평균이 아니라 99th percentile 처리 시간을 기준으로 설정해야 한다. 평균 기반으로 설정하면 처리가 느린 케이스에서 반드시 중복이 발생한다.

CloudWatch에서 Lambda를 컨슈머로 사용하는 경우 Duration 메트릭의 p99 값을 확인한다.

aws cloudwatch get-metric-statistics \
  --namespace AWS/Lambda \
  --metric-name Duration \
  --dimensions Name=FunctionName,Value=my-sqs-processor \
  --start-time 2024-01-01T00:00:00Z \
  --end-time 2024-01-02T00:00:00Z \
  --period 86400 \
  --statistics Maximum

Step 3: Visibility Timeout 조정

p99 처리 시간을 파악했다면 Visibility Timeout을 그 값의 충분한 배수로 설정한다. AWS 문서는 처리 시간보다 충분히 크게 설정할 것을 권장한다. 처리 중 네트워크 지연, 외부 API 호출, GC pause 등을 감안하면 여유 있게 설정하는 것이 안전하다.

aws sqs set-queue-attributes \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
  --attributes VisibilityTimeout=300

위 예시는 Visibility Timeout을 300초(5분)로 설정한다. 설정 범위는 0초에서 43200초(12시간)까지다.

Step 4: 런타임 중 Visibility Timeout 연장

처리 시간이 가변적이라면 고정된 Visibility Timeout만으로는 부족하다. 처리 중간에 ChangeMessageVisibility를 호출해 타임아웃을 연장할 수 있다. 이 방법은 처리가 진행 중임을 SQS에 주기적으로 알리는 heartbeat 패턴으로 활용된다.

aws sqs change-message-visibility \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
  --receipt-handle AQEBwJnKyrHigUMZj6reyYjze... \
  --visibility-timeout 120

--receipt-handleReceiveMessage 응답에서 반환된 값이다. MessageId가 아님에 주의한다. Receipt handle은 각 수신 시도마다 새로 발급되며, 이전 handle로 visibility를 변경하면 오류가 발생한다.

sequenceDiagram participant Q as SQS 큐 participant C as 컨슈머 participant T as Heartbeat 타이머 Q->>C: ReceiveMessage (receipt handle 발급) Note over C: 처리 시작 C->>T: Heartbeat 타이머 시작 loop 처리 진행 중 T->>C: 타임아웃 임박 알림 C->>Q: ChangeMessageVisibility (타임아웃 연장) Note over Q: Visibility Timeout 갱신 end alt 처리 성공 C->>Q: DeleteMessage Note over Q: 메시지 영구 삭제 else 처리 실패 Note over C: DeleteMessage 미호출 Note over Q: 타임아웃 만료 후 재visible end
  1. ReceiveMessage: 메시지 수신과 동시에 receipt handle이 발급된다.
  2. Heartbeat 루프: 처리 중 주기적으로 ChangeMessageVisibility를 호출해 타임아웃을 연장한다. 호출 간격은 현재 남은 Visibility Timeout보다 짧아야 한다.
  3. 처리 완료: DeleteMessage로 메시지를 영구 삭제한다.
  4. 처리 실패: DeleteMessage를 호출하지 않으면 타임아웃 만료 후 메시지가 재처리 대상이 된다.

Dead Letter Queue와 Visibility Timeout의 관계

Visibility Timeout 만료로 메시지가 재큐잉될 때마다 ApproximateReceiveCount가 증가한다. DLQ(Dead Letter Queue)의 maxReceiveCount는 이 카운터를 기준으로 동작한다. Visibility Timeout이 너무 짧으면 실제로 처리 가능한 메시지가 maxReceiveCount를 초과해 DLQ로 이동하는 상황이 발생한다. 처리 로직에는 문제가 없는데 DLQ 메시지가 쌓인다면 이 패턴을 의심해야 한다.

aws sqs get-queue-attributes \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
  --attribute-names RedrivePolicy ApproximateNumberOfMessagesNotVisible

Lambda 컨슈머 사용 시 주의사항

Lambda를 SQS 트리거로 사용할 때 Visibility Timeout 설정에 특별히 주의해야 한다. Lambda 함수의 최대 실행 시간(timeout)과 SQS 큐의 Visibility Timeout이 정렬되어 있지 않으면 문제가 발생한다.

Lambda가 SQS를 폴링할 때 내부적으로 배치 단위로 메시지를 수신한다. 배치 내 일부 메시지 처리가 Lambda timeout으로 실패하면, 해당 메시지들은 Visibility Timeout이 만료된 후 재처리 대상이 된다. AWS 문서는 SQS 큐의 Visibility Timeout을 Lambda 함수 timeout의 6배 이상으로 설정할 것을 권장한다.

# Lambda 함수 timeout 확인
aws lambda get-function-configuration \
  --function-name my-sqs-processor \
  --query 'Timeout'

IAM 권한 — 최소 권한 원칙 적용

컨슈머가 Visibility Timeout을 런타임에 변경하려면 sqs:ChangeMessageVisibility 권한이 필요하다. 아래는 SQS 컨슈머에게 필요한 최소 권한 정책이다.

🔽 SQS 컨슈머 최소 권한 IAM 정책 (클릭하여 펼치기)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "sqs:ReceiveMessage",
        "sqs:DeleteMessage",
        "sqs:ChangeMessageVisibility",
        "sqs:GetQueueAttributes"
      ],
      "Resource": "arn:aws:sqs:us-east-1:123456789012:my-queue"
    }
  ]
}

SQS FIFO 큐에서의 Visibility Timeout

FIFO 큐도 동일한 Visibility Timeout 메커니즘을 사용한다. 다만 FIFO 큐는 Message Group ID 단위로 순서를 보장하므로, 특정 그룹의 메시지가 invisible 상태인 동안 동일 그룹의 다른 메시지는 수신되지 않는다. Visibility Timeout 만료로 메시지가 재visible 상태가 되면 해당 그룹의 처리가 재개된다. 이 특성 때문에 FIFO 큐에서 Visibility Timeout이 너무 짧으면 특정 Message Group의 처리가 전체적으로 지연되는 현상이 나타날 수 있다.

SQS Visibility Timeout 설정 진단 체크리스트

graph TD A[중복 처리 발생 확인] --> B{중복 간격이 규칙적인가?} B -->|예| C[Visibility Timeout 문제] B -->|아니오| D[at-least-once 중복
멱등성 로직 검토] C --> E[현재 Visibility Timeout 확인
get-queue-attributes] E --> F[p99 처리 시간 측정] F --> G{처리 시간 > Visibility Timeout?} G -->|예| H[Visibility Timeout 증가
set-queue-attributes] G -->|아니오| I[처리 시간 가변성 확인] H --> J{처리 시간이 가변적인가?} I --> J J -->|예| K[ChangeMessageVisibility
Heartbeat 패턴 적용] J -->|아니오| L[Lambda timeout과
정렬 여부 확인] K --> M[DLQ 메시지 수 모니터링] L --> M M --> N[문제 해결 완료]

마무리 및 다음 단계

SQS 중복 처리 문제는 대부분 Visibility Timeout과 실제 처리 시간의 불일치에서 시작된다. 핵심 원칙은 단순하다: Visibility Timeout > 최대 처리 시간. 처리 시간이 가변적이라면 ChangeMessageVisibility heartbeat 패턴을 적용하고, Lambda 컨슈머라면 함수 timeout과 큐 Visibility Timeout을 함께 검토해야 한다.

다음 단계로 아래 AWS 공식 문서를 참고한다:

핵심 용어 정리 (Glossary)

용어설명
Visibility Timeout메시지 수신 후 다른 컨슈머에게 보이지 않는 시간. 0~12시간 설정 가능.
Receipt HandleReceiveMessage 호출 시 발급되는 고유 식별자. DeleteMessage 및 ChangeMessageVisibility에 사용. MessageId와 다름.
ApproximateReceiveCount메시지가 수신된 횟수. DLQ maxReceiveCount 판단 기준.
Dead Letter Queue (DLQ)maxReceiveCount 초과 메시지가 이동하는 별도 큐. 처리 실패 메시지 격리에 사용.
At-least-once DeliverySQS 표준 큐의 전달 보장 방식. 드물게 중복 전달이 발생할 수 있으므로 컨슈머 멱등성 설계가 필요.

댓글

이 블로그의 인기 게시물

EC2 SSH 연결 시간 초과: 확인해야 할 보안 그룹(Security Group) 규칙

EC2 SSH 연결 타임아웃 완전 해결 가이드: Security Group 인바운드 규칙부터 라우팅까지

IAM User vs IAM Role 차이점 완전 정리 — EC2에서 S3 접근 시 무엇을 써야 하는가