Competing Consumer Pattern
"어떻게 하면 소비자들 사이에서 엄청난 수의 비동기 메시지를 배분할 수 있을까?"
가장 간단한 접근 방식은 소비자들이 서로 경쟁하도록 하는 것이다. 이를 경쟁 소비자 패턴이라 한다.
- 소비자 패턴 동작 방식
- 한 명 이상의 프로듀서가 대기열에 메시지를 추가한다. 이러한 메시지는 수행해야 하는 작업과 같다.
- 이 대기열에서 메시지나 작업을 처리하도록 여러 소비자 인스턴스가 설정된다.
- 각 소비자는 메시지를 검색하고 처리하기 위해 경쟁한다.
- 한 소비자가 메시지를 성공적으로 청구하면 다른 소비자는 메시지를 사용할 수 없게 된다.
- 처리 후 소비자는 메시지를 승인하고 대기열에서 제거한다.
중요한 점은 메시지가 한 명의 소비자에 의해서만 처리되도록 하는 것이다. 다시 말해, 소비자가 사용한 메시지를 다른 소비자가 사용할 수 없어야 한다.
플랫폼마다 다른 방식으로 이를 처리한다.
- RabbitMQ: 프리페치 횟수를 사용한다. 소비자는 프리페치 횟수를 설정하여 승인되지 않은 메시지 수를 제한할 수 있다. 소비자가 메시지를 받으면 "전송 중"으로 간주되어 다른 소비자에게 전달되지 않는다.
- AWS SQS: 소비자가 메시지를 수신하면 SQS는 가시성 시간제한을 설정한다. 이 시간 동안 메시지는 다른 소비자에게 숨겨진다. 처리 후 소비자는 메시지를 삭제한다. 메시지가 삭제되기 전에 시간제한이 만료되면 다른 소비자에게 메시지가 다시 표시된다.
Retry Messages Pattern
"메시지 큐를 사용하여 실패한 트랜잭션을 어떻게 다시 시도하나요?"
이는 일시적인 오류를 처리하는 일반적인 패턴이다. 메시지 큐를 사용하여 재시도 메커니즘을 구현하는 일반적인 접근 방식은 크게 세 가지로 나뉜다: (결제 처리를 예로 든다.)
- 메인 큐: 새 결제 거래가 큐에 대기하는 곳입니다.
- 데드 레터 큐: 여러 번 처리에 실패한 메시지에 대한 별도의 큐다.
- 재시도 큐: 지연이 있는 재시도가 예약되는 큐다. 이 큐는 기본 큐를 사용할 수도 있으므로 선택 사항이다.
- 메시지 재시도 패턴
- 소비자 또는 결제 처리자가 기본 대기열에서 메시지를 받는다. 결제 거래를 처리하려고 시도한다.
- 처리에 실패하면 메시지 메타데이터에 저장된 재시도 횟수를 확인한다.
- 재시도 횟수가 최대 재시도 횟수보다 적으면 횟수를 증가시키고 메시지를 다시 대기열에 넣는다.
- 재시도 횟수가 최대 재시도 횟수를 초과하면 메시지를 DLQ로 이동한다.
- 재시도의 경우 지연이 있는 기본 큐에 직접 다시 대기하거나 시간 기반 트리거가 있는 별도의 재시도 큐를 사용할 수 있다.
- 마지막으로 재시도 시도가 모두 끝난 메시지가 있는지 DLQ를 모니터링한다.
이 패턴을 따를 때 염두에 두어야 할 몇 가지 모범 사례는 아래와 같다.
- 지수 백오프: 시스템에 과부하가 걸리지 않도록 재시도 사이의 지연 시간을 지수적으로 늘린다.
- 멱등성: 결제 처리자가 경제적 문제가 생기지 않고 안정적으로 결제를 재시도할 수 있도록 보장해야 한다.
- 메시지 TTL: 메시지에 대한 전체 TTL을 설정하여 매우 오래된 거래가 처리되지 않도록 한다.
- 재시도 제한: 최대 재시도 횟수에 대한 값을 설정한다.
- 오류 유형: 일시적 오류(재시도 가능)와 영구 오류(DLQ로 직접 연결)를 구분한다.
Async Request Response Pattern
"메시지 큐로 요청-응답 통신을 처리하는 방법은 무엇인가요?"
먼저 이것이 왜 필요한지 이해해야 한다. 메시지 큐를 사용하면 두 서비스가 서로 비동기적으로(요청-응답) 대화하도록 할 수 있다. 이 접근 방식은 시간이 오래 걸리는 작업을 처리하는 것에 매우 중요하다. 또한 동기 호출이 없다는 것은 단일 서비스가 애플리케이션을 다운시킬 가능성이 적다는 것을 의미한다.
하지만 문제가 있다. "요청자와 응답자 인스턴스가 여러 개 있으면 어떻게 될까?" 동기식 REST API 호출에서는 하나의 요청자 인스턴스가 항상 하나의 응답자 인스턴스에만 연결되므로 이는 크게 중요하지 않다. 이는 시간적 결함이 있기 때문이다.
시간적 결함은 요청을 보낸 시점에 반드시 응답을 받아야만 처리가 완료되는 구조를 의미한다.
비동기 요청-응답의 경우에는 그렇지 않다. 요청을 한 요청자 인스턴스가 최종적으로 응답을 받는 인스턴스가 아닐 수도 있다. 응답이 돌아올 때쯤이면 다운되었거나 사용할 수 없을 수도 있다. 그렇다면 여러 임시 인스턴스에서 요청과 응답을 어떻게 연관시킬 수 있을까?
보통 사용하는 방식은 상관 ID(Correlation ID)를 사용하는 방법이다. 예를 들어 주문 서비스와 결제 서비스가 비동기 요청-응답 모델로 상호작용한다고 가정해 보자.
- 고객이 주문하면 주문 서비스의 한 인스턴스가 요청을 처리한다. 이때, 주문에 대한 고유한 상관 ID를 생성하고, 그와 관련된 데이터를 데이터베이스, 분산 캐시, 혹은 로컬 인스턴스 수준의 해시맵에 저장한다.
- 이후 결제 서비스로 결제 요청 메시지를 보낼 때 상관 ID도 함께 보낸다.
- 결제 서비스(어떤 특정 인스턴스)가 결제를 처리한 후, 응답 메시지를 응답 큐에 보낸다. 이 응답 메시지에는 동일한 상관 ID가 포함되어 있다.
- 주문 서비스(같은 인스턴스일 수도 있고, 다른 인스턴스일 수도 있음)는 이 응답 메시지를 수신하고 메시지 안에 있는 상관 ID를 이용해 원래 주문 요청과 응답을 매칭하고 필요한 후속 조치를 수행한다.
이때 "그냥 상관 ID 대신 그냥 주문 ID를 써도 되지 않나?" 하는 의문을 가질 수도 있다. 이론적으로 주문 ID만으로도 같은 기능을 수행할 수 있다. 하지만 아래와 같은 이유로 주문 ID만으로는 부족할 수 있다.
- 응답 메시지에 주문 ID를 넣기 어려운 경우: 메시지를 처리하는 중간 서비스가 주문 ID를 모를 수 있다.
- 하나의 요청에 다수의 비즈니스 ID가 엮일 수 있다.
- 오직 시스템 간 요청 흐름 추적만 필요한 경우
- 비동기 메시지 처리에서의 매칭: 비동기 메시지는 순서 보장이 없기 때문에, 어떤 응답이 어떤 요청에 대한 것인지 정확히 식별하려면 상관 ID가 유용하다.
'개발' 카테고리의 다른 글
선언형과 명령형 프로그래밍 정리 (0) | 2025.05.15 |
---|---|
이벤트 소싱 정리 (0) | 2025.05.14 |
레디스 클러스터 정리 (1) | 2025.05.12 |
레디스 센티널 정리 (0) | 2025.05.09 |
레디스 복제 정리 (0) | 2025.05.08 |