이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Message Queue와 Event Stream은 메시지를 보관하고 소비하는 방식이 어떻게 다른가?
- 작업 분배, 이벤트 기록, replay, 여러 consumer group 중 무엇이 기술 선택을 가르는가?
- Java/Spring 서비스에서 Kafka를 큐처럼 썼을 때 어떤 장애와 운영 착각이 생기는가?
- 개인 프로젝트에서는 Kafka 없이도 어떤 요소를 남겨야 stream 사고를 연습할 수 있는가?
개요
message queue는 처리해야 할 작업을 소비자에게 나눠 주는 모델에 가깝다.
event stream은 이미 일어난 사건을 append-only log로 남기고, 여러 소비자가 각자 offset을 가지고 읽는 모델에 가깝다.
둘 다 비동기 처리에 쓰이지만 설계 질문은 다르다.
queue를 고를 때는 “누가 이 작업을 처리하면 끝인가?”를 묻는다.
stream을 고를 때는 “이 사건을 누가, 언제부터, 다시 읽을 수 있어야 하는가?”를 묻는다.
Kafka를 도입했는데도 큐처럼만 쓰면 replay, retention, consumer group의 장점을 놓치고 운영 난이도만 가져오게 된다.
Queue 모델
queue 모델의 중심은 작업 분배다.
producer -> queue -> worker A
-> worker B
-> worker C하나의 작업은 보통 한 worker가 가져가 처리한다.
메일 발송, 이미지 리사이즈, 영수증 PDF 생성처럼 “한 번 수행하면 끝나는 작업”에 잘 맞는다.
중요한 판단은 다음과 같다.
- 작업이 중복 실행돼도 안전한가?
- 실패한 작업을 몇 번 재시도할 것인가?
- 처리하지 못한 작업을 어디에 보관할 것인가?
- 큐 길이가 늘면 worker를 얼마나 늘릴 것인가?
queue에서는 메시지 자체보다 작업 완료 여부가 더 중요하다.
그래서 ack, visibility timeout, retry count, dead-letter queue가 핵심 운영 단위가 된다.
Stream 모델
stream 모델의 중심은 사건 기록이다.
order.events topic
offset 101: OrderCreated
offset 102: OrderPaid
offset 103: OrderCanceled
settlement-service group -> offset 102까지 읽음
recommendation group -> offset 101까지 읽음
analytics group -> offset 103까지 읽음같은 event log를 여러 consumer group이 독립적으로 읽을 수 있다.
이미 처리한 구간도 retention 안에 있으면 다시 읽을 수 있다.
새로운 소비자가 나중에 생겨도 과거 이벤트부터 따라잡을 수 있다.
Kafka의 강점은 이 지점에 있다.
Kafka 메시지는 “사라지는 작업 지시”가 아니라 “다른 시스템이 따라 읽는 사실의 기록”에 가깝다.
선택 기준
기술 이름보다 질문을 먼저 정해야 한다.
| 질문 | Queue에 가까운 선택 | Stream에 가까운 선택 |
|---|---|---|
| 목적 | 작업 분배 | 사건 기록과 전달 |
| 소비자 | 하나의 작업을 한 worker가 처리 | 여러 group이 같은 event를 독립적으로 처리 |
| 재처리 | 실패 작업 위주 | offset/range 단위 replay |
| 보관 | 짧은 보관 또는 처리 후 제거 | retention 동안 log 보관 |
| 순서 | 대체로 작업별 독립 | partition key 기준 순서 |
| 확장 | worker 수 증가 | partition 수와 group concurrency 조정 |
메일 발송은 queue 모델로 충분한 경우가 많다.
주문 결제 완료 이벤트를 정산, 알림, 추천, 분석이 모두 써야 한다면 stream 모델이 자연스럽다.
Kafka를 큐처럼 쓸 때의 문제
Kafka로도 작업 큐를 만들 수는 있다.
하지만 다음 질문에 답하지 못하면 Kafka의 운영 부담만 커진다.
- retention 동안 메시지를 다시 읽어도 안전한가?
- 같은 topic을 다른 group이 읽어도 business 의미가 유지되는가?
- 메시지 key가 순서와 병렬성을 설명하는가?
- consumer가 같은 메시지를 두 번 받아도 결과가 안전한가?
예를 들어 email.send topic을 만들고 단순 발송 작업만 넣는다면 Kafka가 꼭 필요하지 않을 수 있다.
반대로 order.events topic에 OrderPaid를 남기고 정산, 배송, 분석이 각각 읽는다면 Kafka의 log 모델이 의미가 있다.
Java/Spring 관점
Spring에서 queue와 stream의 차이는 코드 모양보다 데이터 계약에서 드러난다.
record OrderPaidEvent(
String eventId,
String orderId,
String paymentId,
long paidAmount,
Instant occurredAt
) {
}stream event는 event id, aggregate id, event type, occurred time을 가져야 한다.
작업 큐 메시지는 worker가 수행할 command에 가까울 수 있다.
record SendEmailCommand(
String commandId,
String recipient,
String templateId
) {
}OrderPaidEvent는 이미 발생한 사실이다.
SendEmailCommand는 앞으로 수행할 작업 지시다.
이 둘을 구분하지 않으면 consumer가 event를 수정하거나 취소하려고 하면서 의미가 흐려진다.
장애 해석
queue 장애는 “작업이 완료되지 않았다”로 해석하는 경우가 많다.
stream 장애는 “어떤 consumer group이 어느 offset 이후를 아직 반영하지 못했다”로 해석해야 한다.
topic: order.events
partition: 3
latest offset: 91200
settlement-service committed offset: 90800
lag: 400이 숫자는 결제 API가 실패했다는 뜻이 아니다.
정산 consumer가 400개 event만큼 뒤처졌다는 뜻이다.
따라서 장애 보고도 “API 정상/비정상”이 아니라 “정산 데이터 최신성이 몇 분 늦는가”로 작성해야 한다.
운영 지표
- queue depth: 처리 대기 작업 수를 본다.
- worker failure rate: 작업 처리 실패율을 본다.
- consumer lag: stream consumer group이 log 끝에서 얼마나 뒤처졌는지 본다.
- retention remaining: replay 가능한 시간이 얼마나 남았는지 본다.
- duplicate event count: 재전달 또는 replay가 side effect를 만들었는지 본다.
Kafka를 stream으로 쓴다면 consumer lag와 retention을 함께 봐야 한다.
lag가 retention을 넘으면 다시 읽을 근거가 사라질 수 있다.
개인 프로젝트 기준
처음부터 Kafka를 띄우지 않아도 된다.
작은 프로젝트에서는 DB table로 stream 사고를 연습할 수 있다.
CREATE TABLE order_events (
event_id varchar(50) PRIMARY KEY,
aggregate_id varchar(50) NOT NULL,
event_type varchar(50) NOT NULL,
payload jsonb NOT NULL,
occurred_at timestamp NOT NULL
);중요한 것은 event를 command와 구분하고, 다시 읽었을 때 consumer가 안전한지 확인하는 것이다.
Kafka를 붙일 때는 이 table의 event_id, aggregate_id, occurred_at이 topic key와 payload 계약으로 이어져야 한다.
위험 신호!
- “Kafka는 빠른 queue”라고만 설명한다.
- topic retention과 replay 요구를 정하지 않는다.
- event와 command 이름을 섞어 쓴다.
- consumer group별 지연을 보지 않고 topic 전체 장애로만 표현한다.
- 메시지 key 없이 여러 partition에 보내고 전체 순서를 기대한다.
확인 질문
확인 질문
- 같은 메시지를 여러 서비스가 독립적으로 읽어야 하는가?
- 그렇다면 queue보다 event stream 요구에 가깝다.
- 메시지를 나중에 처음부터 다시 읽어야 하는가?
- 그렇다면 retention, offset, replay 절차가 설계에 포함되어야 한다.
- 이 메시지는 이미 발생한 사실인가, 앞으로 수행할 작업인가?
- 사실이면 event, 지시면 command로 이름과 payload를 구분한다.
참고 문서
- Apache Kafka Documentation
- Spring for Apache Kafka Reference
- Martin Kleppmann, Designing Data-Intensive Applications
- Martin Fowler, What do you mean by Event-Driven?