왜 SQS 장애를 '일부러' 내야 할까?
프로덕션에서 SQS 큐에 메시지를 보내거나 받지 못하는 상황이 발생하면 어떻게 될까요? IAM 정책 오설정, 네트워크 파티션, 잘못된 배포, 일시적 서비스 장애 등 원인은 다양하지만, 애플리케이션 입장에서는 똑같이 AccessDenied 에러가 터지기 시작합니다.
문제는 대부분의 팀이 이 상황을 한 번도 테스트해보지 않았다는 점이에요. 회로 차단기(Circuit Breaker)를 달아놨는데 실제로 열리는지, 로컬 버퍼링이 동작하는지, 컨슈머가 백로그를 감당하는지 — 코드만 보고 "아마 되겠지" 하고 넘어가는 경우가 많습니다.
핵심 관점: 이 실험의 목적은 "SQS가 잘 동작하는지" 검증하는 게 아닙니다. "SQS가 죽었을 때 우리 애플리케이션이 어떻게 반응하는지, 그리고 우리가 그걸 인지할 수 있는지" 를 확인하는 겁니다. 회복 메커니즘과 관측 가능성을 동시에 시험하는 셈이죠.
이 글에서는 AWS Fault Injection Service(AWS FIS)와 AWS Systems Manager(SSM) Automation을 활용해 SQS 접근을 단계적으로 차단하고 복구하는 실험을 구성하는 방법을 다룹니다. 국내에서도 카오스 엔지니어링 도입을 고민하는 팀이 늘고 있는데, SQS처럼 관리형 서비스에 의존하는 아키텍처라면 이 실험이 좋은 출발점이 됩니다.

실험 구조와 핵심 코드
전체 아키텍처
실험은 다음 흐름으로 진행됩니다.
- SQS 큐에
FIS-Ready: True태그를 붙여 실험 대상 범위를 한정 - SSM Automation 문서가 해당 큐의 리소스 정책에 데이터 플레인 작업만 거부하는 Deny 문을 추가
- AWS FIS 실험 템플릿이 2분 → 5분 → 7분 → 15분으로 점진적으로 차단 시간을 늘리며 복구 구간을 사이에 배치
- CloudWatch 메트릭으로 프로듀서/컨슈머/큐의 반응을 관찰
⚠️ 절대 하면 안 되는 것: sqs:* 전체 Deny
여기서 정말 중요한 함정이 있어요. IAM 정책 평가에서 명시적 Deny는 모든 Allow를 이깁니다. sqs:*를 Deny하면 큐 관리 작업(SetQueueAttributes, AddPermission, RemovePermission)까지 막혀서, Deny를 적용한 역할조차 스스로 정책을 제거할 수 없는 락아웃 상태에 빠집니다.
반드시 데이터 플레인 작업만 Deny하세요:
{
"Sid": "FISTemporaryDeny",
"Effect": "Deny",
"Principal": "*",
"Action": [
"sqs:SendMessage",
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:ChangeMessageVisibility",
"sqs:PurgeQueue"
],
"Resource": "arn:aws:sqs:ap-northeast-2:123456789012:my-queue"
}
팁:
aws:CurrentTime에DateLessThan조건을 걸면 Deny가 자동 만료됩니다. 클린업 단계가 실패해도 지정 시각이 지나면 자동으로 해제되니 안전장치로 좋아요.
특정 역할로만 차단하기 (Principal 스코프)
"Principal": "*"는 해당 큐의 모든 호출자(관리자, 카나리, 다른 컨슈머)를 차단합니다. 공유 큐에서는 실험 대상 앱 외의 트래픽까지 막히죠. 특정 애플리케이션만 노리고 싶다면 IAM 역할 ARN으로 스코프를 좁히세요:
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/order-producer-role"
}
역할 ARN은 해당 역할의 모든 세션과 매칭되므로, 앱의 호출만 정확히 차단할 수 있습니다.
FIS 실험 템플릿의 4단계 점진적 장애
| 단계 | 지속 시간 | 드러나는 것 |
|---|---|---|
| Impair 1 | 2분 | Fail-fast 동작, 회로 차단기 활성화 여부 |
| Recover | 3분 | 버퍼 재전송, 메트릭 정상화 |
| Impair 2 | 5분 | 큐 백로그 누적 |
| Recover | 3분 | 백로그 소진 속도 |
| Impair 3 | 7분 | 스레드풀/메모리 압박 |
| Recover | 2분 | 큰 백로그 상황에서의 복구 |
| Impair 4 | 15분 | 장기 장애 시 시스템 한계 |
스톱 컨디션은 '고객 영향' 지표로 걸어라
여기가 많은 분들이 실수하는 부분이에요. ApproximateAgeOfOldestMessage나 NumberOfMessagesSent 같은 큐 메트릭으로 알람을 걸면 안 됩니다. 이 메트릭들은 실험 중에 움직이는 게 정상이거든요. 그걸로 알람을 걸면 첫 2분 단계에서 바로 실험이 중단돼요.
대신 회복탄력성 메커니즘이 정상이라면 건강하게 유지되어야 하는 신호로 걸어야 합니다:
- 애플리케이션 에러율 (예: 분당 실패 주문 수) — 가장 직접적인 고객 영향 지표
- ALB
HTTPCode_Target_5XX_Count— 비즈니스 메트릭이 없을 때의 프록시 - DLQ 깊이 (
ApproximateNumberOfMessagesVisible) — 영구 실패 신호
예를 들어 가설이 "복구 후 2분 내 정상화"라면, 알람은 분당 실패 주문 수 > 10, 1분 기간 2회 연속 정도로 잡으면 됩니다. 회로 차단기가 열리는 순간의 스파이크는 넘기고, 가설 윈도우를 벗어난 실패는 실험을 중단시키는 거죠.
aws fis start-experiment \
--experiment-template-id EXT1234567890 \
--tags Purpose=resilience-test

실험 중 관찰 포인트와 주의사항
⚠️ 이 실험이 검증하지 않는 것
Deny 정책이 반환하는 건 AccessDenied(403), 즉 재시도 불가능한 에러입니다. 따라서 이 실험은 "우리 코드가 403을 인지하고 멈추는가"를 검증하지, "백오프와 재시도 경로가 제대로 동작하는가"를 검증하지는 않습니다. 재시도/백오프를 시험하려면 스로틀링이나 타임아웃 같은 재시도 가능한 장애를 별도로 주입해야 해요.
프로듀서 측 관찰
| 단계 | 건강한 반응 | 비건강 반응 | 볼 신호 |
|---|---|---|---|
| 첫 SendMessage 실패 | AccessDenied 인지 후 fail-fast | 크래시/행/스레드 블로킹 | NumberOfMessagesSent → 0 |
| 연속 3~5회 실패 | 회로 차단기 오픈, 부하 차단 또는 버퍼링 | 무한 재시도 | 회로 차단기 상태 메트릭 |
| 전송 포기 | durable fallback 저장소에 보관 또는 알람 | 조용히 드롭(영구 손실) | failed send / dropped 카운터 |
컨슈머 측 관찰
| 단계 | 건강한 반응 | 비건강 반응 | 볼 신호 |
|---|---|---|---|
| 첫 Receive/Delete 실패 | 폴 루프 백오프 | 컨슈머 루프 크래시 | NumberOfMessagesReceived 감소 |
| 백로그 누적 | 큐 깊이 기반 스케일링 | 무한 증가, 유휴 루프 | ApproximateNumberOfMessagesVisible 평탄화 |
| 재배달 | 멱등 처리 | 중복 부수효과 | 중복 쓰기 메트릭 |
🚨 DLQ는 장애 중에 채워지지 않는다
많이 오해하는 부분인데, DLQ는 maxReceiveCount 기반으로 동작합니다. 메시지가 N번 수신되고도 삭제되지 않아야 DLQ로 이동하는데, ReceiveMessage 자체가 차단되면 수신 카운트가 증가하지 않아요. 즉 DLQ는 복구 시점에 봐야 할 것이지 장애 중에 볼 게 아닙니다.
복구 시 흔한 실패 패턴
- 회로 차단기가 안 닫힘: stale failure state에 갇혀 수동 재시작이 필요한 경우
- 백로그 소진이 멈춤: 속도가 스파이크 후 0으로 떨어지고 그대로 — 컨슈머가 압도되었거나 stuck
- DLQ 메시지가 retention 기간을 넘겨 삭제됨: 복구 후 반드시 DLQ 잔여 메시지 확인
한국 개발 생태계에서의 적용 맥락
국내 SI/스타트업 환경에서는 대부분 단일 계정에 공유 큐를 쓰는 경우가 많아서, "Principal": "*" Deny를 그대로 적용하면 옆 팀 서비스까지 영향을 받을 수 있습니다. 반드시 targetPrincipalArn 파라미터로 스코프를 좁히고, 비프로덕션 환경에서 먼저 돌려보세요. 또 CloudWatch 대시보드가 제대로 갖춰지지 않은 경우가 많은데, 이 실험은 관측 가능성 자체를 시험하는 것이므로 대시보드와 알람을 먼저 정비하는 걸 권장합니다.

정리: 가설 → 실험 → 수정의 사이클
이 실험에서 가장 중요한 산출물은 "통과/실패"가 아니라 "가설과 실제의 간극" 입니다. 그 간극이 바로 여러분 시스템이 손봐야 할 지점이에요.
실무 적용 순서
- 비프로덕션 환경에서 2분 단계부터 시작하세요. 깨지는 것부터 고칩니다.
- 전체 시퀀스(2→5→7→15분)를 돌리고, 애플리케이션이 진화할 때마다 반복하세요.
- 확장 단계: 부분 실패(일부 큐만), 컨슈머 측만 차단(
ReceiveMessage/DeleteMessage만 Deny), EC2 종료나 네트워크 지연과의 복합 장애로 확장하세요.
반드시 갖춰야 할 4가지 패턴
- 재시도: 재시도 가능한 에러(스로틀링, 타임아웃, 일시적 5xx)만 지수 백오프 + 지터로. 403 같은 비재시도 에러는 즉시 fail-fast.
- 회로 차단기: 연속 실패 임계치 후 오픈, 복구 타임아웃 후 half-open. 대체 경로로 조용히 전환하지 말 것 — fallback은 장애 시에만 쓰여서 같이 죽는 경우가 많습니다.
- DLQ: 모든 큐에 설정.
maxReceiveCount는 3~5가 일반적. DLQ retention을 소스 큐보다 길게 잡아 조사 시간을 확보하세요. - 모니터링: 큐 메트릭 + 애플리케이션 메트릭(에러율, 회로 차단기 상태, DLQ 깊이, E2E 처리 지연)을 한 대시보드에.
한 줄 요약: SQS가 죽었을 때 여러분 앱이 조용히 메시지를 드롭하고 있지는 않나요? 이 실험은 그 질문에 대한 답을 코드가 아니라 관측된 사실로 돌려줍니다.