티스토리 뷰
이번 글에서는 동기 방식 통신의 한계를 해결하기 위해 메시지 큐를 도입 + 메세지 유실이 없는 이야기를 소개하겠습니다 🚀
1. 문제 배경📚

프로젝트 구조상 통계 서버와 API 서버가 분리되어 있었고, 처음에는 이 두 서버 간의 통신을 동기 방식의 API 호출로 처리하고 있었습니다. 그러나 이 방식은 여러 가지 문제를 야기했습니다.
가장 큰 문제는 한쪽 서버에 장애나 지연이 발생하면 다른 쪽 서버도 그 영향을 직접적으로 받는다는 점이었습니다.
예를 들어, 통계 서버에 문제가 생기면 API 서버의 요청도 지연되거나 실패하게 되어 시스템 전체의 안정성이 떨어질 수밖에 없었습니다.
또한, 속도 측면에서도 비효율적이었습니다.
API 서버가 통계 처리 결과를 기다리는 동안 스레드가 블로킹되고, 이는 곧 리소스 낭비로 이어졌습니다.
서버 간 강한 의존성으로 인해 유지보수와 확장성에도 제약이 따랐습니다.
이러한 문제를 해결하기 위해 서버 간 직접 통신 대신 중간에 메시지 큐를 두고, 비동기 방식으로 전환하게 되었습니다.
2. 비동기 + 큐 도입 🕐
[ 어떤 큐 사용하지? ]
비동기 큐를 도입하기 전, 어떤 방식의 큐를 사용할 것인지 고민이 필요하였습니다.
크게 RabbitMQ, Kafka, AWS SQS 3가지 방식의 큐가 있습니다.
RabbitMQ는 큐 서버를 별도로 구성하고 운영해야 하므로 인프라 관리 부담이 늘어나는 단점이 있었고,
Kafka는 러닝 커브가 높고 초당 수천 TPS 이상의 대규모 트래픽 처리에 적합한 시스템이기 때문에 현재 규모의 트래픽에서는 과한 선택이었습니다.
반면, SQS는 AWS에서 제공하는 완전 관리형 서비스로 별도 인프라 운영 없이 아마존 웹에서 쉽게 큐를 생성하고 관리할 수 있는 장점이 있었고, 기존 인프라가 AWS에 구성되어 있어 연동도 자연스러웠습니다. 다만, 메시지 유실을 방지하려면 DLQ 구성이나 수신 측의 예외 처리, 재시도 로직을 직접 보완해야 한다는 점은 고려해야 했습니다.
이런 이유로 최종적으로 AWS SQS를 선택하게 되었습니다.
[ AWS SQS 도입 ]

API 서버와 통계 서버 간에 비동기 방식으로 진행하기 위해 각각 SQS 큐를 하나씩 구성하고, 서로의 큐를 구독하여 메시지를 수신하도록 했습니다. 큐를 통해 전달받은 메시지를 비동기적으로 처리가 되어, 다음과 같은 문제들을 해결할 수 있었습니다.
- 요청 처리 속도 개선
- 서버 간 강한 결합도 감소
- 시스템 유연성 향상
- 한쪽 서버의 장애가 다른 서버에 영향을 주는 문제 제거
결과적으로, 각 서버는 서로의 상태를 몰라도 독립적으로 동작할 수 있게 되었고, 시스템은 보다 유연하고 안정적으로 개선되었습니다.
3. 메세지 유실 방지 🕐
Producer, Consumer, AWS SQS 간에 메시지 유실이 발생할 수 있는 주요 케이스는 다음과 같습니다.
- Producer → SQS 보낼 때 네트워크 오류 및 타임아웃 발생하는 경우
- SQS 권한 또는 IAM 정책 오류
- SQS 메세지 수신했지만 Consumer 처리 실패
- Consumer 서버 재기동 시 재기동 사이에 SQS 에 메시지가 적재될 경우
- 개발자의 오타로 잘못된 ARN 으로 메세지를 보내는 경우
[ 메세지 재시도 전략 ]

SQS 설정에서 "배달 못한 편지 대기열" 부분에 최대 수신 수를 설정할 수 있습니다.
1로 설정하면 한 번 수신하고 실패하면 끝입니다.
3 이상으로 설정하여 SQS → Consumer 에서 메세지가 실패하더라도 재시도를 할 수 있도록 합니다.
[ DLQ 도입 ]

그 다음은 DLQ(Dead Letter Queue) 도입하는 것입니다.
DLQ는 소프트웨어 시스템에서 오류로 인해 처리할 수 없는 메시지를 임시로 저장하는 특수한 유형의 메시지 대기열입니다.
SQS에서 메시지를 수신한 이후 Consumer 측의 처리 실패, 네트워크 오류, 타임아웃 등으로 인해 처리되지 못한 메시지는 DLQ 로 이동합니다. DLQ에는 Lambda가 연결되어 있어, 메시지가 들어오면 트리거되어 리드라이브 정책(재처리)에 따라 원래 큐로 다시 전송을 시도합니다. 이 과정에서 최대 재시도 횟수를 초과하면, 해당 메시지는 2차 DLQ로 이동됩니다.
2차 DLQ에 메시지가 쌓이면 Lambda를 통해 Slack 알림을 전송하여 운영자가 문제 상황을 즉시 인지할 수 있도록 했습니다.
운영자는 2차 DLQ에 쌓인 메시지를 바탕으로 문제 원인을 분석하고 수동 조치를 수행함으로써 메시지 유실 가능성을 최소화했습니다.
하지만 DLQ는 SQS로 메시지가 정상적으로 도달한 이후의 실패 상황에 대응하는 구조이기 때문에, Producer → SQS로 메시지를 전송하는 단계에서 발생하는 네트워크 오류나 예외 상황으로 인해 메시지가 아예 SQS에 도달하지 못하고 유실되는 경우에는 대응이 불가능합니다.
이러한 한계로 인해, Producer 측의 메시지 전송 실패에 대한 보완이 추가로 필요한 상황이었습니다.
[ SNS + Transactional Outbox Pattern 도입 ]

Producer 측에서 메시지 전송 중 유실되는 문제를 해결하기 위해 Transactional Outbox 패턴을 도입하였습니다.
Producer는 메시지를 먼저 Outbox 테이블에 저장한 뒤, 이후에 SQS로 전송하는 안정적인 전송 플로우를 구성하였습니다.
이 구조를 통해 SQS로 메시지를 전송하기 전까지 데이터를 안전하게 보관할 수 있고,
전송이 성공한 경우에는 해당 메시지를 DB에서 삭제하여 중복 전송을 방지할 수 있도록 했습니다.
삭제 작업의 신뢰성을 높이기 위해 두 가지 방식의 이중 안전 장치를 적용했습니다:
- SQS 전송 성공 응답을 받은 경우, 해당 메시지를 즉시 DB에서 삭제
- 동시에, 삭제 전용 SQS 큐를 별도로 구성하여 메시지 전송 시 삭제 요청 메시지를 함께 발행하고,
Producer가 이 큐의 리스너 역할을 하여 해당 메시지를 수신한 후 DB 레코드를 삭제
이와 같은 설계를 통해 삭제 실패나 메시지 중복 전송 가능성을 최소화했습니다.
스케쥴러
하지만 네트워크 오류나 일시적인 장애로 인해 SQS 전송이 실패하는 경우, 메시지는 SQS에 도달하지 못하고 Outbox 테이블에 그대로 남게 됩니다.
이러한 상황을 대비하여, 주기적으로 Outbox 테이블을 조회하는 스케줄러를 별도로 운영합니다. 해당 스케줄러는 전송되지 않은 메시지를 확인하고 SQS로 재전송함으로써, 일시적인 오류가 발생하더라도 메시지 유실 없이 안정적으로 복구될 수 있도록 보완했습니다.
SNS 도입 이유
또한, Producer가 두 개의 SQS로 각각 메시지를 전송하는 구조는 확장성 저하 및 관리 복잡도 증가라는 문제를 유발했습니다.
이를 개선하기 위해 AWS SNS(Simple Notification Service) 를 도입하여 구조를 단순화했습니다.
SNS 주제(Topic)에 두 개의 SQS를 구독시키고, Producer는 오직 SNS에만 메시지를 발행하도록 하여,
확장성과 관리 편의성을 동시에 확보할 수 있었습니다.
4. 결론 😊
결과적으로, 재시도 전략 + DLQ + SNS + Transactional Outbox Pattern 를 결합한 메시지 처리 구조를 통해,
메시지 유실을 방지하고, 장애에 강하며 운영이 용이한 비동기 아키텍처를 구축할 수 있었습니다.
'∙Infra' 카테고리의 다른 글
EC2에서 디스크 용량 부족 문제로 발생한 API 오류 해결하기 (0) | 2024.09.10 |
---|---|
Nginx와 헬스체크를 활용한 무중단 배포하기 (2) | 2024.09.02 |
Nginx에서 SSL 인증서 갱신하기 (0) | 2023.11.10 |
Nginx 다중 도메인 서버 설정하기 (0) | 2023.11.07 |
NGINX + HTTPS 적용하기 (0) | 2022.05.10 |