이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Producer, broker, consumer는 각각 어떤 책임을 가져야 하는가?
  • Queue가 성능과 장애 격리를 개선하면서 어떤 운영 복잡도를 추가하는가?
  • 발행 성공, 전달 성공, 처리 성공을 어떻게 구분하고 관측해야 하는가?

개요

Queue 설계의 기본은 producer가 메시지를 넣고 consumer가 꺼낸다는 설명으로 끝나지 않는다. 실전에서는 발행, 저장, 전달, 처리, ack, 재처리 중 어디까지 성공했는지를 구분해야 한다.

Producer가 broker에 메시지를 보냈다고 해서 업무가 끝난 것은 아니다. Broker가 메시지를 보관했는지, consumer가 받았는지, DB 변경까지 끝났는지, ack가 언제 이루어졌는지에 따라 유실과 중복의 위치가 달라진다.

이 문서는 queue의 구성 요소를 책임 경계와 실패 경로 중심으로 정리한다.

책임 분리

구성 요소책임책임이 아닌 것
Producer업무 이벤트를 메시지로 만들고 발행 결과를 기록한다consumer의 업무 처리 성공 보장
Broker메시지를 보관하고 consumer에게 전달한다메시지 의미 검증과 업무 보정
Consumer메시지를 처리하고 idempotency와 ack를 관리한다producer transaction을 되돌리는 것
DLQ반복 실패 메시지를 격리한다실패 원인 분석과 재처리 판단 자동화
Monitoringlag, 처리율, 실패율을 보여준다잘못된 업무 계약 수정

책임을 섞으면 장애 때 판단이 느려진다. 예를 들어 producer 로그만 보고 “처리 완료”라고 말하면 consumer 실패를 놓친다.

발행 성공과 처리 성공

결제 완료 후 포인트 적립 메시지를 발행한다고 하자.

13:00:00 Order API가 order-paid 메시지 발행 요청
13:00:01 Broker append 성공
13:00:05 Point Consumer poll
13:00:06 Point DB insert 실패
13:00:06 Consumer retry 예약

이 흐름에서 producer 관점은 성공이지만 업무 관점은 아직 실패 상태다. 따라서 메시지 처리 상태를 별도로 남기지 않으면 포인트 누락을 추적할 수 없다.

메시지에 담을 정보

메시지는 consumer가 재처리와 추적을 할 수 있을 만큼 충분해야 하지만, producer의 DB row 전체 복사본이 되어서는 안 된다.

{
  "messageId": "msg-20260630-0001",
  "eventType": "OrderPaid",
  "eventVersion": 1,
  "aggregateType": "Order",
  "aggregateId": "ord-1001",
  "occurredAt": "2026-06-30T10:00:00+09:00",
  "correlationId": "req-7788"
}

messageId는 중복 방어에 쓰이고, aggregateId는 순서와 조회 기준이 되며, correlationId는 사용자 요청과 비동기 처리를 연결한다.

Payload 크기 판단

메시지 payload는 두 방향의 trade-off를 가진다.

방식장점비용
id만 담기메시지가 작고 최신 DB를 다시 읽는다consumer가 producer DB나 API에 의존한다
snapshot 담기consumer가 독립적으로 처리하기 쉽다stale snapshot과 schema 변경 비용이 커진다
필요한 필드만 담기결합과 크기를 조절한다필드 의미와 version 관리가 필요하다

알림처럼 발송 시점의 주문자 이메일이 중요하면 snapshot이 유리할 수 있다. 정산처럼 최신 상태 검증이 중요하면 id를 기준으로 다시 조회해야 할 수 있다.

처리량 계산

Queue는 평균 TPS보다 peak와 fanout을 봐야 한다.

결제 peak = 500 TPS
주문당 발행 메시지 = 4개
발행량 = 2,000 msg/s
consumer 1대 처리량 = 120 msg/s
필요 consumer = 17대
여유율 30% 적용 = 23대

이 계산은 출발점일 뿐이다. 실제 병목은 consumer 수가 아니라 broker partition 수, 외부 API rate limit, DB connection pool, 같은 주문 id에 몰리는 순서 보장 요구일 수 있다.

Ack 위치

Consumer는 언제 ack할지 정해야 한다.

1. 메시지 수신 직후 ack
2. 업무 처리 성공 후 ack
3. DB transaction commit 후 ack
4. 외부 API 호출 후 ack

수신 직후 ack는 중복은 줄일 수 있지만 처리 중 실패하면 유실된다. 처리 성공 후 ack는 유실 가능성을 줄이지만 consumer 재시작 시 중복 처리가 발생할 수 있다. 그래서 대부분의 업무 consumer는 중복을 허용하고 idempotency로 막는 방향을 선택한다.

Consumer Idempotency

Consumer는 같은 메시지를 두 번 받아도 업무 결과가 한 번만 반영되게 만들어야 한다.

create table processed_message (
    message_id varchar(100) primary key,
    processed_at timestamp not null
);
@Transactional
public void grantPoint(OrderPaid event) {
    if (processedMessageRepository.exists(event.messageId())) {
        return;
    }
    pointRepository.save(PointGrant.from(event));
    processedMessageRepository.save(new ProcessedMessage(event.messageId()));
}

중요한 것은 exists와 업무 변경과 처리 기록이 같은 transaction 경계 안에 있어야 한다는 점이다. 그렇지 않으면 처리 기록만 남거나 포인트만 적립되는 간극이 생긴다.

Queue 분리 기준

모든 메시지를 하나의 queue에 넣으면 구조는 단순하지만 장애 격리가 어렵다.

분리 기준분리할 이유예시
업무 중요도중요 작업이 낮은 작업 뒤에 밀리지 않게 한다정산과 마케팅 알림 분리
처리 속도느린 consumer가 빠른 작업을 막지 않게 한다이미지 처리와 이메일 분리
실패 성격retry 정책과 DLQ owner를 다르게 둔다외부 API 실패와 데이터 검증 실패 분리
순서 요구같은 aggregate 단위 순서를 지킨다주문 상태 변경 이벤트

분리할수록 장애 격리는 좋아지지만 topic, queue, consumer, dashboard, alert가 늘어난다.

관측 지표

Queue 운영에서 최소 지표는 다음이다.

  • publish success rate
  • publish failure count
  • consumer lag
  • processing latency
  • retry count
  • DLQ count
  • consumer error rate
  • oldest message age

lag만 보면 부족하다. lag가 낮아도 consumer가 잘못된 데이터를 만들 수 있고, DLQ가 늘어도 lag는 정상일 수 있다.

위험 신호

  • producer 로그만 보고 업무 완료로 판단한다.
  • consumer가 중복 메시지를 받지 않는다고 가정한다.
  • ack를 언제 하는지 코드와 문서에 드러나지 않는다.
  • 모든 메시지가 하나의 queue, 하나의 retry 정책, 하나의 DLQ를 공유한다.
  • oldest message age를 보지 않아 오래된 메시지가 조용히 방치된다.

개인 프로젝트 기준

  • message id와 correlation id를 넣는다.
  • consumer 처리 성공 로그와 실패 로그를 분리한다.
  • 중복 메시지 테스트를 하나 만든다.
  • queue를 하나만 쓰더라도 분리 조건을 문서에 남긴다.
  • 재처리 방법이 수동이어도 절차를 적는다.

기업 운영 기준

  • producer, broker, consumer 지표를 한 dashboard에서 연결한다.
  • queue별 owner, SLO, DLQ 처리 시간을 둔다.
  • 장애 때 producer throttle과 consumer scale out 기준을 정한다.
  • 메시지 schema 변경 시 old consumer 호환성을 검증한다.
  • 처리 성공률과 업무 결과 정합성을 별도 지표로 본다.

확인 질문

확인 질문

  • 확인 질문: producer 발행 성공과 consumer 처리 성공을 구분해야 하는 이유는 무엇인가?
    • 답변: broker에 저장된 메시지가 업무 DB 변경까지 성공했다는 뜻은 아니므로 누락, 중복, 실패 위치를 따로 추적해야 한다.
  • 확인 질문: ack를 업무 처리 후에 하면 어떤 비용이 생기는가?
    • 답변: 유실 가능성은 줄지만 consumer 재시작이나 ack 실패 때 같은 메시지가 다시 처리될 수 있어 idempotency가 필요하다.
  • 확인 질문: queue를 분리할 때 얻는 것과 잃는 것은 무엇인가?
    • 답변: 장애 격리와 정책 분리는 좋아지지만 운영할 topic, consumer, alert, 재처리 절차가 늘어난다.

참고 문서