분산 메시지 큐란?
분산 메시지 큐(Distributed Message Queue)는 여러 시스템(서버, 애플리케이션 등)이 비동기적으로 데이터를 주고받을 수 있도록 해주는 중간 시스템입니다. "분산"이란 말에서 알 수 있듯이, 이 메시지 큐는 하나의 서버가 아니라 여러 서버에 걸쳐 분산되어 동작
대표적인 분산 메시지 큐 시스템
| Apache Kafka | 대용량 처리, 고속 처리, 로그 중심, pub-sub 모델 |
| RabbitMQ | 신뢰성 높은 전송, 복잡한 라우팅 지원, AMQP 프로토콜 |
| Amazon SQS | AWS의 완전관리형 큐 서비스 |
| Redis Streams | Redis 기반의 경량 메시지 큐 역할 가능 |
장점
- 결합도 완화 : 컴포넌트 사이의 강한 결합이 사라짐
- 규모 확장성 개선 : 생산자와 소비자 시스켐 규모를 트래픽 부하에 맞게 독립적으로 확장 가능
- 가용성 개선 : 시스템의 특정 컴포넌트에 장애가 발생해도 다른 컴포넌트는 큐와 계속 상호작용 가능
- 성능 개선 : 메시지 큐를 사용하면 비동기 통신으로 즉시 응답이 가능해짐. 병렬성을 통해 처리량 향상이 가능함.
메시지 큐와 이벤트 스트리밍 플랫폼 차이
메시지 큐란?
하나의 작업을 다른 서비스에 "위임"하는 도구.
"하나의 생산자 → 하나의 소비자" 구조가 일반적.
- 주로 작업 처리용 (비동기 처리, 워커 패턴)
- 메시지를 꺼내면 큐에서 사라짐 (보통 1회 소비)
- 일시적인 데이터 (중요하지 않으면 메시지가 유실될 수도 있음)
예:
- A 서비스가 메시지 큐에 "이메일 전송 요청"을 넣고
- B 서비스(소비자)가 꺼내서 이메일을 전송하고 끝
✅ 이벤트 스트리밍 플랫폼이란?
"이벤트 로그"를 스트림처럼 저장하고 여러 소비자가 구독해가는 플랫폼
- 데이터가 지속적으로 생성되고 흘러가는 시스템에 적합
- 이벤트는 디스크에 장기 저장 가능
- 한 이벤트를 여러 소비자 그룹이 독립적으로 소비 가능
예:
- Kafka에 사용자 활동 로그가 들어오면
- A 시스템은 실시간 대시보드
- B 시스템은 사용자 분석용 DB 적재
- C 시스템은 이상 징후 탐지
모두 같은 이벤트 스트림을 따로 읽음
| 항목 | 메시지 큐 (Message Queue) | 이벤트 스트리밍 플랫폼 (Event Streaming Platform) |
| 주 목적 | 비동기 작업 처리 (작업 위임) | 이벤트 로그 저장 및 실시간 스트리밍 |
| 예시 도구 | RabbitMQ, Amazon SQS | Apache Kafka, Redpanda, Pulsar |
| 메시지 보존 기간 | 소비되면 삭제됨 (기본적으로) | 디스크에 장기간 저장 가능 (기본 전략) |
| 메시지 소비 방식 | 1회 소비(1 consumer만 처리) | 1개 이벤트를 여러 consumer가 읽을 수 있음 |
| 순서 보장 | 보통 큐 단위 또는 메시지 단위 | 파티션 내에서 순서 보장 |
| 내구성 / 저장 | 메모리 기반이 많음 / 저장 짧음 | 디스크 기반 로그 저장 / 장기 보관 가능 |
| 사용 사례 | 이메일 전송, 이미지 처리 | 실시간 로그 처리, 데이터 파이프라인 |
메시지 모델
일대일 모델 (Point-to-Point Model)
개념
- 메시지는 정확히 한 명의 소비자에게만 전달됨.
- 하나의 큐(queue)에 메시지가 쌓이고,
등록된 소비자 중 단 한 명만 그것을 꺼내 처리함.
구성 요소
- Producer (생산자): 메시지를 큐에 보냄.
- Queue (큐): 메시지를 보관.
- Consumer (소비자): 큐에서 메시지를 꺼내 처리. 보통 하나만 소비.
특징
- 하나의 메시지를 오직 한 소비자만 처리
- 작업 분산 또는 비동기 처리에 적합
예시
- 주문 시스템: 주문 요청을 큐에 넣고, 백엔드 워커가 하나씩 꺼내 처리
- 이메일 발송: 이메일 요청이 큐에 들어가고, 발송 서버 하나가 꺼내 처리
발행-구독 모델 (Publish-Subscribe Model)
개념
- 메시지를 특정 주제(Topic)로 발행하면,
그 주제를 구독하고 있는 모든 소비자가 동시에 메시지를 받음.
구성 요소
- Publisher (발행자): 메시지를 특정 토픽에 발행.
- Topic (토픽): 발행된 메시지의 채널.
- Subscriber (구독자): 토픽을 구독하고 메시지를 수신.
특징
- 여러 소비자가 동시에 동일한 메시지를 받을 수 있음
- 실시간 알림, 이벤트 브로드캐스팅에 적합
예시
- 실시간 알림 시스템: 새 댓글이 등록되면, 글 작성자, 관리자 모두에게 알림 발송
- 주식 가격 업데이트: 가격이 변하면 여러 차트/알림 시스템이 동시에 수신
토픽, 파티션, 브로커
Topic (토픽) – "카테고리"
개념:
- 메시지를 구분해서 저장하는 논리적 단위.
- "이벤트의 주제"라고 보면 됨.
- 예: order-created, user-registered, log-events 등
비유:
- 뉴스에서 "스포츠", "정치", "연예" 섹션처럼
- 구독자는 관심 있는 토픽만 구독함
Partition (파티션) – "토픽의 물리적 분할"
개념:
- 하나의 토픽은 여러 개의 파티션으로 쪼갤 수 있음.
- 각 파티션은 독립적인 메시지 저장소이며 순서 보장됨.
- 메시지는 특정 파티션에 들어가고, 파티션 내부에서는 순서가 유지됨
왜 중요해?
- 파티션을 늘리면 병렬 처리 가능 (→ 성능, 확장성 향상)
- Consumer Group이 각 파티션을 나눠서 동시에 읽을 수 있음
비유:
- 토픽이 책장이라면, 파티션은 책장의 칸. 칸마다 따로 책(메시지)이 들어감
Broker (브로커) – "Kafka 서버 인스턴스"
개념:
- Kafka 클러스터를 구성하는 개별 서버.
- 각 브로커는 여러 파티션을 저장하고 메시지를 관리함.
- 브로커 수를 늘리면 파티션을 나눠서 저장할 수 있음 (→ 분산 처리, 장애 대응)
역할
- 생산자에게서 메시지를 받고
- 해당 파티션을 담당하는 브로커가 저장
- 소비자에게 메시지를 전달
비유:
- 브로커는 도서관 직원. 파티션 칸을 담당하면서 책을 넣고 꺼내줌.
소비자 그룹이란?
하나의 그룹에 속한 여러 소비자가 메시지를 나눠서 소비(consume)할 수 있게 하는 논리적 단위
- 그룹 ID를 기준으로 메시지 소비를 조율함
- 같은 그룹에 속한 소비자끼리는 중복 소비하지 않음
- 다른 그룹이라면 동일한 메시지를 중복 소비 가능
구조 설명 (Kafka 기준)
예를 들어, 하나의 토픽 orders에 3개의 파티션이 있다고 가정하면:
소비자 그룹 A (Group ID: groupA)
| C1 | Partition 0 |
| C2 | Partition 1 |
| C3 | Partition 2 |
- groupA에 속한 C1, C2, C3는 서로 다른 메시지를 소비함 (파티션 단위로 분산)
- 각 메시지는 오직 한 번만 소비됨 (at-least-once, 상황에 따라 exactly-once)
소비자 그룹 B (Group ID: groupB)
- groupB도 동일한 orders 토픽을 구독하면, 그룹 A와는 별개로 같은 메시지를 다시 소비할 수 있음
- 즉, 다른 그룹은 같은 메시지를 "별도로" 소비 가능
핵심 특징 요약
| 병렬 처리 | 하나의 토픽에 여러 소비자가 병렬로 메시지를 처리할 수 있게 함 |
| 메시지 중복 소비 방지 | 같은 그룹 내에서는 중복 소비되지 않음 |
| 확장성 | 소비자 수를 늘려 처리량을 확장할 수 있음 (단, 파티션 수보다 많아도 유휴 소비자는 생김) |
| 오프셋 관리 | 그룹 단위로 오프셋(offset)을 관리해서 어디까지 처리했는지 추적함 |
| Failover | 한 소비자가 죽으면 같은 그룹의 다른 소비자가 그 파티션을 이어받음 (리밸런싱) |
예시 시나리오
Kafka 토픽 email-queue, 파티션 3개
groupId = email-sender
→ 이메일 전송 처리를 3개의 소비자 인스턴스(C1, C2, C3)가 나눠 처리
- C1이 죽으면 Kafka가 rebalance를 통해 C2, C3에 파티션을 재할당
- 이메일 중복 전송을 막기 위해 같은 그룹으로 묶는 것
주의할 점
- 소비자 수 > 파티션 수: 일부 소비자는 유휴 상태
- 소비자 수 < 파티션 수: 일부 소비자가 여러 파티션을 맡아야 하므로 부하 증가
- 같은 메시지를 여러 시스템에서 처리해야 한다면 → 다른 그룹 ID 사용
개략적 설계안
클라이언트
- 생산자 : 메시지를 특정 토픽으로 보낸다.
- 소비자 그룹 : 토픽을 구독하고 메시지를 소비한다.
핵심 서비스 및 저장소
- 브로커 : 파티션들을 유지한다. 하나의 파티션은 특정 토픽에 대한 메시지의 부분 집합을 유지한다.
- 저장소
- 데이터 저장소 : 메시지는 파티션 내 데이터 저장소에 보관된다. ( 브로커 내부에 존재 )
- 상태 저장소 : 소비자 상태(집계 상태 / 스트림에서의 오프셋 / 캐시 등)는 이 저장소에 유지된다.
- Kafka Streams에서 사용되는 데이터임. 일반적으로 메시지 발송, 구독만 사용시엔 필요 없음.
- 어플리케이션 내부에 RocksDB를 형성하여 저장되고, 백업 데이터는 topic에 있고 카프카 브로커 내부 저장소에 있음.
- 메타데이터 저장소 : 토픽 설정, 토픽 속성 등은 이 저장소에 유지된다.
- 2.8 이전은 주키퍼가 필수. 주키퍼는 별도로 클러스터 구성해서 띄워야함. (aws msk를 쓸 시, 자동으로 관리해줌)
- 2.8 이후는 Kafka KRaft 모드로 카프카 클러스터 단독으로 구성 가능.
- Kafka 내부에 KRaft (Kafka Raft metadata mode) 라는 메타데이터 관리자가 포함됨
- 앞으로는 KRaft 모드가 기본, ZooKeeper는 점차 폐기 예정
- 조정 서비스
- 서비스 탐색 : 어떤 브로커가 살아있는지 알려준다.
- 리더 선출 : 브로커 가운데 하나는 컨트롤러 역할을 담당. 한 클러스터에는 반드시 활성 상태 컨트롤러가 1개 있어야함. 해당 컨트롤러가 파티션 배치를 책임.
- 아파치 주키퍼나 etcd가 보통 컨트롤러 선출을 담당하는 컴포넌트로 이용됨.
✅ 조정 서비스란?
Kafka에서 "조정(coordination)"이란
- 브로커와 컨슈머, 파티션, 토픽 등 여러 구성요소들이
- 누가 뭘 담당하고 있는지,
- 서로 충돌 없이 어떻게 동작할지를 정합성 있게 관리하는 행위를 뜻합니다.
이를 위해 필요한 것이 바로 **조정 서비스(Coordinator Service)**입니다.
✅ Kafka에서의 조정 서비스 종류
| 클러스터 메타데이터 관리 | ✅ ZooKeeper | ✅ KRaft (Kafka Raft metadata mode) |
| 컨슈머 그룹 관리 | ✅ Kafka Group Coordinator | ✅ Kafka Group Coordinator |
| 트랜잭션 관리 | ✅ Kafka Transaction Coordinator | ✅ Kafka Transaction Coordinator |
🔹 1. ZooKeeper (전통적인 Coordination Service)
Kafka 2.8 이전까지는 다음과 같은 역할을 ZooKeeper가 맡았습니다:
| 클러스터 상태 관리 | 브로커 등록/제거 감시 |
| 컨트롤러 선출 | 누가 리더 브로커인지 관리 |
| 토픽/파티션 메타정보 저장 | 어떤 토픽이 있고, 파티션 수, 복제 정보 등 |
| 워치(watch) 기반 이벤트 감지 | 변화에 따른 이벤트 트리거 처리 |
📌 단점: 별도 시스템(ZooKeeper) 운영 부담, 느린 failover 등
🔹 2. Kafka KRaft 모드 (신규 구조, Kafka ≥ 2.8)
KRaft(Kafka Raft)는 ZooKeeper를 대체하기 위해 도입된 Kafka 자체 메타데이터 관리 방식입니다.
| 컨트롤러 역할 수행 | ✅ Kafka 내에서 수행 |
| 메타데이터 기록 | ✅ Raft 로그에 기록 |
| 고가용성 지원 | ✅ Quorum 기반 합의 구조 |
📌 장점: 관리 단순화, 빠른 리더 전환, 단일 시스템 운영
🔹 3. Group Coordinator (컨슈머 그룹 조정)
Kafka 브로커 내부에 존재하는 컴포넌트로, 컨슈머 그룹 내의 조정 역할을 수행합니다:
| 파티션 할당 | 어떤 컨슈머가 어떤 파티션을 가져갈지 결정 |
| 리밸런싱 | 컨슈머가 들어오거나 나가면 할당 재조정 |
| 오프셋 관리 | 컨슈머가 마지막으로 읽은 offset 저장 |
📌 __consumer_offsets topic을 통해 상태 저장
🔹 4. Transaction Coordinator
트랜잭션을 사용하는 경우, 해당 코디네이터가 다음을 관리:
| 트랜잭션 상태 추적 | prepare, commit, abort 등 |
| 상태 저장 | 내부 topic (__transaction_state)에 저장 |
| 트랜잭션 격리 보장 | Exactly-once semantics 지원 |
✅ 조정 서비스가 중요한 이유
Kafka는 분산 시스템이기 때문에, 다음과 같은 것들이 항상 일어납니다:
- 컨슈머가 들어오고 나가고 (리밸런스)
- 브로커가 죽고 살아나고 (리더 전환)
- 토픽이 생성되고 파티션이 늘어남
- 상태 일관성을 유지해야 함
이걸 자동으로 "조정"해주는 게 coordination service입니다.
✅ 요약
| ZooKeeper | Kafka 클러스터 메타데이터 관리 (기존) | 외부 서버 |
| KRaft Controller | Kafka 클러스터 메타데이터 관리 (신규) | Kafka 내부 |
| Group Coordinator | 컨슈머 그룹 파티션 할당 | Kafka 브로커 내부 |
| Transaction Coordinator | 트랜잭션 상태 추적 | Kafka 브로커 내부 |
상세 설계
데이터의 장기 보관 요구사항을 만족하면서 높은 대역폭을 제공하기위해 세가지 중요한 결정을 내림
- 회전 디스크의 높은 순차 탐색 성능과 현대적 운영체제가 제공하는 적극적 디스크 캐시 전략을 잘 이용하는 디스크 기반 자료 구조를 활용
- 메시지가 생산자로부터 소비자에게 전달되는 순간까지 아무 수정 없이도 전송이 가능하도록 하는 메시지 자료 구조를 설계하고 활용
- 일괄처리를 우선하는 시스템 설계. 소규모 i/o가 많으면 높은 대역폭 지원 어려움
데이터 저장소
- 읽기와 쓰기가 빈번
- 갱신/삭제 연산 x
- 순차적인 읽기/쓰기
쓰기 우선 로그 사용.
쓰기 우선 로그(Write-Ahead Log, WAL)란?
어떤 변경을 실제 적용하기 전에, 먼저 변경 내용을 로그(파일)에 기록하고, 그 다음에 적용하는 구조
- 즉, "쓰기 전에 기록해둔다"는 의미
- 시스템이 장애가 나더라도, 이 로그를 기반으로 재생(복구) 가능
- DB나 Kafka 같은 로그 기반 시스템에서 많이 사용
✅ Kafka의 메시지 저장 = Write-Ahead Log
Kafka는 메시지를 다음과 같은 방식으로 저장합니다:
🔹 핵심 특징
| Append-only | Kafka는 파티션 로그 파일에 메시지를 추가(append) 하기만 함. 수정/삭제 없음 |
| Sequential Write | 디스크의 **순차 쓰기(sequential write)**를 활용해 성능 극대화 |
| Segment 파일 | 각 파티션은 여러 개의 segment log 파일로 구성됨 (000000.log, 000001.log 등) |
| Offset 기반 | 각 메시지는 고유한 offset을 가지며, 이 순서대로 로그에 저장됨 |
✅ Kafka에서 쓰기 흐름 예시
- Producer가 메시지를 전송
- Kafka Broker는 해당 메시지를 파티션 로그(segment file)에 순차적으로 기록
- 디스크에 flush (혹은 OS cache에 머무르다 flush됨)
- 메시지가 저장되었다면 Consumer가 해당 offset부터 읽기 가능
✅ Write-Ahead Log의 장점
| 고성능 | 디스크 순차 쓰기로 매우 빠름 (SSD 없어도 빠름) |
| 내구성 보장 (Durability) | 메시지를 먼저 로그에 저장한 뒤 ack 처리 → 장애 시에도 복구 가능 |
| 재처리 용이 | 메시지가 로그에 남아 있으므로 같은 메시지를 여러 그룹이 재소비 가능 |
| 추가 연산 없이 유지 | 인메모리 구조 없이도, 로그만으로 상태 복원/조회 가능 |
✅ 로그 파일 예시
- .log: 실제 메시지 payload가 append되는 파일
- .index, .timeindex: 오프셋/시간 기반 빠른 조회를 위한 보조 파일
✅ Kafka의 메시지 저장과 WAL 비교
| 저장 구조 | Append-only 로그 |
| 구현 형태 | Segment log file (*.log) |
| WAL 역할 | 메시지를 durable하게 저장하고 장애 시 복구 기준이 됨 |
| 특징 | 수정/삭제 불가, TTL/retention에 따라 삭제만 발생 |
Kafka 메시지 구조 (ProducerRecord 기준)
Kafka의 **한 메시지(record)**는 아래와 같은 주요 필드들로 구성됩니다
| 필드명 | 설명 |
| topic | 메시지가 발행되는 토픽 이름 |
| partition | (선택) 명시적으로 메시지를 보낼 파티션 번호 |
| key | (선택) 메시지 키 – 파티션을 결정하는 데 사용 |
| value | 메시지 본문(실제 데이터) |
| timestamp | 메시지가 생성된 시간 (CreateTime or LogAppendTime) |
| headers | (선택) key-value 쌍의 메타데이터 – HTTP 헤더처럼 사용 가능 |
각 필드 자세히 설명
🔹 key (Optional)
- Kafka는 메시지의 키를 사용해서 어떤 파티션에 저장할지 결정함
- 기본 파티셔너: partition = hash(key) % partitionCount
- 동일한 key는 항상 같은 파티션으로 감 (순서 보장)
🔸 활용 예
- 주문 ID, 사용자 ID 등 → 같은 키의 메시지를 같은 파티션에 모아 순서 보장
🔹 value
- 실제 전송하고자 하는 데이터
- 문자열, JSON, Avro, Protobuf 등 다양하게 직렬화 가능
🔹 partition (Optional)
- 명시하지 않으면 Kafka가 key/hash 기반으로 자동 결정
- 명시하면 강제로 해당 파티션으로 전송됨
🔹 timestamp
- 메시지 생성 시간 (기본: Producer의 현재 시간)
- 브로커 설정에 따라 CreateTime 또는 LogAppendTime 사용
- 로그 조회, 보관 기간 계산 등에 활용
🔹 headers (Optional)
- Kafka 0.11 이후 도입된 기능
- 메시지에 추가적인 메타데이터를 key-value 형태로 전달
- 트래킹 ID, 인증 토큰, 포맷 타입 등 다양하게 활용 가능
그외 메시지의 크기와 crc 순환 중복 검사 등 다양한게 있음.
메시지의 크기는 브로커, 컨슈머, 프로듀서 설정을 맞춰야하니 잘 설정할 것.
(default size인 1mb를 넘어서 터진 경우도 있었음)
압축을 통한 크기도 줄이고 전송속도도 더 빠르게 가능.
일괄처리
메시지 일괄처리는 한번에 보내고 받을 양을 정해서 일괄로 처리하는 것이다.
해당 처리로 인해 높은 대역폭은 얻을 수 있으나,(값비싼 네트워크 왕복 비용 줄이기)
낮은 응답 지연은 못 얻을 수 있다. ( 일정 개수 쌓이기전까진 발송 또는 컨슘을 안함 )
잘 조정해서 사용할 것.
생산자측의 작업 흐름
생산자가 어떤 파티션에 메시지를 보내야할 때, 어떤 브로커에 연결할 것 인지 선택해야함.
해결하기 위한 것이, 라우팅 계층을 도입하는 것.
라우팅 계층은 '적절한' 브로커에 메시지를 보내는 역할을 담당. ( 리더 브로커 )
이렇게 되면 네트워크 노드가 하나 더 늘어나게 되므로 오버헤드가 발생하여 네트쿼으 전송 지연이 늘어남.
그래서 라우팅 계층을 생산자 내부로 편입시키고 버퍼를 도입하여 처리.
카프카는 별도의 라우팅 계층이 존재하지 않음.
| Kafka의 특성 | 설명 |
| 클라이언트 주도 라우팅 | 메시지를 어디로 보낼지는 Producer가 결정함 |
| 브로커는 수신만 | Kafka 브로커는 단지 해당 파티션의 메시지를 받고 로그에 저장함 |
| 중앙 라우터 없음 | 중간에서 메시지를 보고 라우팅하는 엔티티가 없음 |
| 경량 고성능 | 중간 계층이 없기 때문에 latency와 throughput에 유리함 |
Kafka는 “Producer가 알아서 대상 브로커를 찾아가는 구조”
= 라우팅 계층이 없지만 라우팅 로직은 Producer가 수행
소비자측의 작업 흐름
Kafka는 본질적으로 “풀(pull) 모델 기반”의 메시지 소비 시스템입니다.
하지만 다른 메시지 큐(RabbitMQ 등)는 “푸시(push) 모델” 기반이라 비교해서 이해하는 것이 매우 유용합니다.
✅ 용어 정리
| Pull 모델 | 소비자가 직접 브로커에 요청해서 메시지를 가져감 |
| Push 모델 | 브로커가 소비자에게 메시지를 자동으로 밀어넣음 |
🟦 Kafka (Pull 모델)의 소비자 작업 흐름
Kafka의 소비자(Consumer)는 풀 모델로 동작합니다.
즉, 소비자가 주기적으로 브로커에 메시지를 요청(fetch) 합니다.
✅ Kafka 소비 흐름 (Pull 기반)
🔹 흐름 단계별 설명
| 1. Poll() 요청 | 컨슈머가 주기적으로 poll() 호출하여 메시지 요청 |
| 2. Fetch 응답 | 브로커는 해당 파티션의 메시지를 최대 지정 크기까지 전달 |
| 3. 처리 | 컨슈머 애플리케이션에서 메시지 처리 로직 수행 |
| 4. 오프셋 커밋 | 자동 or 수동으로 오프셋 커밋 → 다음 메시지 위치 기억 |
🔸 Pull 모델의 장점 (Kafka 방식)
- 컨슈머가 속도 조절(back-pressure) 가능
- 재시도, 에러 처리, 병렬 처리 등 제어가 쉬움
- 메시지를 여러 그룹이 독립적으로 소비 가능 (복사 X)
🟩 Push 모델 (RabbitMQ 등)의 소비 흐름
Push 모델에서는 브로커가 메시지를 직접 소비자에게 전송합니다.
→ 소비자는 수동으로 요청하지 않음.
✅ Push 흐름 (RabbitMQ 등)
🔸 Push 모델의 특징
| 자동 전송 | 메시지를 자동으로 밀어 넣음 |
| 소비자 속도 조절 어려움 | 브로커가 너무 빠르게 보내면 병목 발생 가능 |
| 미처리 메시지 재전송 | 처리 실패 시 requeue 또는 DLQ |
🔄 비교: Pull vs Push 모델
| 동작 방식 | 컨슈머가 poll()로 요청 | 브로커가 자동으로 메시지 전송 |
| 속도 조절 | 컨슈머 주도 (backpressure) | 브로커 주도 (ACK 기반 조절) |
| 오프셋 관리 | 컨슈머가 명시적으로 커밋 | 브로커가 큐 상태 추적 |
| 신뢰성 제어 | 커밋/재시도 로직 명확히 구현 가능 | 재전송, DLQ로 제어 |
| 순서 보장 | 파티션 단위 순서 보장 | 큐 단위 순서 보장 (병렬 시 주의) |
| 복수 구독자 | 가능 (Consumer Group) | 기본은 1:1 (Fan-out 구성 필요) |
소비자 재조정
✅ 소비자 재조정(Consumer Rebalance)이란?
Kafka 컨슈머 그룹 내에서 파티션을 어떤 컨슈머가 담당할지를 다시 결정하는 작업
🔄 언제 재조정이 발생하나요?
| ✅ 컨슈머 추가 | 새 컨슈머가 그룹에 join |
| ✅ 컨슈머 종료/장애 | 컨슈머 crash 또는 종료로 leave |
| ✅ 구독 변경 | 컨슈머가 새로운 토픽을 구독하거나 해제 |
| ✅ 파티션 수 변경 | 토픽에 파티션을 추가/삭제 |
| ✅ 그룹 coordinator 변경 | 브로커 failover 등으로 coordinator 브로커 변경 |
✅ 재조정 처리 과정 요약
- Coordinator 선출
- 컨슈머 그룹마다 담당 브로커 1명이 Group Coordinator 역할 수행
- 컨슈머 그룹 가입 (join group)
- 모든 컨슈머가 자신의 상태/구독 토픽을 coordinator에게 전달
- 리더 컨슈머 선출
- coordinator는 컨슈머 중 하나를 리더로 지정
- 파티션 할당 결정
- 리더는 할당 전략(partition assignor) 에 따라 토픽의 파티션을 각 컨슈머에 할당
- 할당 통지
- coordinator가 각 컨슈머에게 결과 전송 → 컨슈머는 해당 파티션을 가져감
- 할당 후 처리
- onPartitionsAssigned() 콜백 호출
- 이전 파티션은 onPartitionsRevoked() 콜백으로 해제
📦 기본 제공 파티션 할당 전략 (Assignor)
| RangeAssignor (기본) | 파티션을 연속적으로 나눔 |
| RoundRobinAssignor | 모든 파티션을 균등하게 분산 |
| StickyAssignor | 가능한 기존 할당 유지하며 분산 |
| CooperativeStickyAssignor | 점진적 재조정 (onPartitionsRevoked 없이 일부만 바뀜) ✅ 추천 |
👉 설정 예:
✅ 재조정에 따른 콜백 처리
Kafka Consumer API는 재조정 발생 시 아래 콜백을 제공합니다:
consumer.subscribe(List.of("orders"), new ConsumerRebalanceListener() {
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
// 이전 파티션에서 커밋/정리 작업 수행
}
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
// 새 파티션에서 offset 초기화 또는 복원
}
});
⚠️ 재조정 시 주의할 점
| ❗ 중복 처리 | offset 커밋 전에 재조정 → 같은 메시지 다시 처리됨 |
| ❗ 처리 누락 | 커밋 없이 파티션 변경 → 아직 처리 안 된 메시지 누락 |
| ❗ 리밸런스 지연 | 처리 시간이 길면 리밸런스가 오래 걸림 → group 전반 지연 |
| ❗ session timeout | max.poll.interval.ms 초과 시 강제 제거 & 재조정 발생 |
🔧 안정적 리밸런스를 위한 팁
| enable.auto.commit | false | 수동 커밋 권장 (명확한 커밋 제어) |
| max.poll.interval.ms | 늘림 | 메시지 처리 시간이 길다면 증가 필요 |
| partition.assignment.strategy | CooperativeStickyAssignor | 점진적 재조정 지원 |
| onPartitionsRevoked() | 꼭 구현 | 오프셋 저장 or 작업 중단 처리 |
| commitSync() | 안전 커밋 | 재조정 직전에 커밋 보장 |
✅ 흐름 요약
[기존 상태]
Partition 0 → C1
Partition 1 → C2
[C2 장애 발생] ▶ 리밸런스 발생
[새 상태]
Partition 0 → C1
Partition 1 → C1
이때,
- onPartitionsRevoked() → Partition 1에 대한 작업 마무리
- onPartitionsAssigned() → Partition 1에 대해 다시 초기화 or 오프셋 복원
✅ 정리 요약
| 재조정이란? | 컨슈머 그룹 내 파티션 할당을 다시 분배하는 과정 |
| 언제 발생? | 컨슈머 join/leave, 구독 변경, 파티션 수 변경 등 |
| 핵심 위험 | 중복 처리, 누락, 지연 |
| 제어 방법 | 콜백 구현 + 수동 커밋 + 할당 전략 개선 |
| 추천 설정 | CooperativeStickyAssignor, enable.auto.commit=false, 적절한 poll 주기 |
리밸런싱 설정 없이 Kafka Consumer를 쓰면?
| 파티션 할당 전략 | RangeAssignor (균형 안 맞을 수 있음) |
| 리밸런스 콜백 | ❌ 없음 (ConsumerRebalanceListener 미등록 시) |
| 리밸런스 방식 | Full Rebalance (모든 파티션 반납 후 재할당) |
| 오프셋 커밋 | 자동 (enable.auto.commit=true) |
| 중복 처리 위험 | 있음 (커밋 전 리밸런스 시) |
| 처리 지연 가능성 | 있음 (전체 재조정 동안 멈춤) |
복제(Replication): “사본을 여러 브로커에 만들어 저장하는 것”
Kafka는 토픽의 각 파티션을 복제(replica)해서,
장애 발생 시에도 메시지를 잃지 않고 소비할 수 있도록 보장합니다.
🔸 핵심 용어
| Replica | 파티션의 복사본. 여러 브로커에 존재 |
| Leader Replica | 읽기/쓰기 요청을 처리하는 메인 파티션 |
| Follower Replica | 리더의 데이터를 복제만 하는 파티션 (쓰기 안 됨) |
| ISR (In-Sync Replica) | 리더와 동기화 상태인 복제본 목록 |
🔹 예시: replication.factor=3
Topic: orders, Partition 0
- Broker 1: Leader
- Broker 2: Follower (ISR)
- Broker 3: Follower (ISR)
✅ 사본 동기화(Synchronization): “리더와 팔로워의 데이터 일치 상태 유지”
복제는 복제고, 실제로 얼마나 잘 따라잡고 있느냐가 중요합니다.
→ 이게 바로 사본 동기화 상태 관리(In-Sync Replica, ISR)입니다.
🔹 ISR의 의미
- ISR은 리더가 메시지를 저장한 후, follower도 그 메시지를 저장해 ACK까지 완료한 브로커 집합
- acks=all 일 때는 ISR에 있는 모든 follower가 수신해야 메시지가 성공적으로 전송된 것으로 간주
🔹 동기화 실패 시
| 팔로워가 느리거나 끊김 | ISR에서 제거됨 |
| 리더가 장애 발생 | ISR 중 하나가 새로운 리더로 선출됨 |
| ISR이 1개뿐 (리더만 있음) | 장애 시 데이터 손실 위험 ↑ |
🔁 복제 + 동기화 전체 흐름
- Producer가 메시지를 전송
- Leader replica가 메시지를 저장
- Follower replicas가 leader로부터 메시지를 fetch & 저장
- ISR 내 모든 follower가 수신하면 → acks=all 조건 만족
- Consumer는 leader에서 메시지를 읽음 (follower는 읽기 안 됨)
✅ 설정 관련 주요 항목
| 설정 | 위치 | 기본값 | 설명 |
| replication.factor | 토픽 | 1 | 복제본 수 |
| min.insync.replicas | 토픽/브로커 | 1 | acks=all일 때 최소 동기화 replica 수 |
| acks | Producer | 1 | 복제 ACK 전략 (0, 1, all) |
| unclean.leader.election.enable | 브로커 | false | 비동기 follower도 리더 승격 허용할지 여부 |
🧠 복제 vs 동기화 비교
| 항목 | 복제 | 사본 동기화 |
| 무엇인가? | 여러 브로커에 사본을 유지 | 리더와 팔로워의 데이터 일치 상태 |
| 대상 | 파티션 | 리더 + 팔로워 (ISR) |
| 목적 | 장애 대비 | 데이터 일관성 보장 |
| 언제 사용됨? | 토픽 생성/운영 중 | 메시지 전송/브로커 failover 시 |
| 실패 시 | 일부 브로커 장애 → OK | 리더와 follower 불일치 시 acks=all 실패 가능 |
🧾 예시 상황: 리더 장애
- Leader broker 1 장애
- Kafka는 ISR에 있는 follower 중 하나를 새로운 leader로 승격
- 해당 리더로 계속 메시지 전송됨
- 장애 복구 시 broker 1은 follower로 다시 참여
✅ 정리 요약
| 복제 | 파티션 데이터를 여러 브로커에 복사하여 저장 |
| 사본 동기화(ISR) | 리더와 follower가 동일한 데이터를 갖고 있는지 추적 |
| 복제 ≠ 항상 동기화 | 느린 follower는 ISR에서 빠짐 |
| acks=all | ISR에 속한 모든 replica가 ack 해야 메시지 전송 성공 |
| 리더 장애 시 | ISR 내 follower가 leader로 승격됨 |
ack모드에 대한 설명
acks=0 # 응답 기다리지 않음 (최저 지연, 데이터 유실 가능)
acks=1 # 리더가 수신하면 OK (기본값)
acks=all # ISR의 모든 복제본이 수신해야 OK (최고 신뢰성)
메시지 전달 방식
✅ 1. 최대 한 번(At Most Once)
메시지는 최대 한 번만 전달된다.
→ 중복은 없음, 그러나 유실 가능 있음
🔹 동작 방식
- 메시지 처리 전에 오프셋을 커밋함
- 처리 도중 에러가 나더라도, 이미 커밋되었기 때문에 다시 전달되지 않음
🔹 특징
| 메시지 유실 가능성 | 있음 ❗ |
| 중복 처리 | 없음 ✅ |
| 속도 | 빠름 ✅ |
| 신뢰성 | 낮음 ❌ |
🔹 사용 예
- 로그, 모니터링 등 일부 유실을 감수할 수 있는 시스템
consumer.commitSync(); // 먼저 커밋
process(record); // 그 다음 처리
✅ 2. 최소 한 번(At Least Once)
메시지는 최소 한 번 이상 전달된다.
→ 유실 없음, 그러나 중복 가능성 있음
🔹 동작 방식
- 메시지 처리 후에 오프셋을 커밋함
- 만약 처리 도중 장애가 발생해 커밋이 안 됐다면 → 같은 메시지를 다시 소비
🔹 특징
| 메시지 유실 가능성 | 없음 ✅ |
| 중복 처리 | 가능성 있음 ❗ |
| 속도 | 중간 |
| 신뢰성 | 높음 ✅ |
🔹 사용 예
- 주문 처리, 사용자 알림, 금전 흐름 등 유실은 안 되지만 중복은 처리 가능한 시스템
process(record); // 먼저 처리
consumer.commitSync(); // 그 다음 커밋
✅ 3. 정확히 한 번(Exactly Once)
메시지는 정확히 한 번만 처리됨
→ 유실도 중복도 없음 ✅
Kafka 0.11부터 도입된 **Exactly Once Semantics (EOS)**를 통해 구현 가능
Producer + Consumer + Broker 간의 end-to-end 보장 필요
🔹 주요 설정 조합
# Producer
enable.idempotence=true
acks=all
retries > 0
# Consumer
isolation.level=read_committed
# Kafka Streams or Transaction 사용
🔹 핵심 메커니즘
| Idempotent Producer | 동일 메시지가 여러 번 전송돼도 1번만 저장됨 |
| Transactional Producer | 메시지 전송과 오프셋 커밋을 하나의 트랜잭션으로 묶음 |
| Broker | 트랜잭션 처리 지원 (__transaction_state topic) |
| Consumer | read_committed 설정 시, 커밋된 메시지만 소비함 |
🔹 특징
| 메시지 유실 가능성 | 없음 ✅ |
| 중복 처리 | 없음 ✅ |
| 속도 | 가장 느림 ❗ |
| 복잡도 | 가장 높음 ❗ |
| 신뢰성 | 최고 ✅✅✅ |
🧠 세 가지 전달 방식 요약
| 방식 | 메시지 유실 | 중복 처리 | 신뢰성 | 사용 사례 |
| At Most Once | ❗ 있음 | ❌ 없음 | 낮음 | 로그, 비중요 이벤트 |
| At Least Once | ❌ 없음 | ❗ 있음 | 중간 | 주문, 알림 |
| Exactly Once | ❌ 없음 | ❌ 없음 | 높음 | 결제, 이체, 재고 등 민감 시스템 |
✅ Kafka에서 Exactly Once 구현 예 (간단한 트랜잭션 방식)
producer.initTransactions();
producer.beginTransaction();
producer.send(...); // 메시지 전송
consumer.commitSync(); // 오프셋 커밋 (트랜잭션 포함)
producer.commitTransaction();
이 방식은 Producer가 트랜잭션 안에서 메시지 전송과 오프셋 커밋을 원자적으로 처리하게 만듭니다.
✅ 마무리 요약
| 기본 Kafka는 어떤 방식? | 최소 한 번 (at least once) 보장 |
| 최대 한 번으로 하려면? | 오프셋 먼저 커밋하고 처리 |
| 정확히 한 번은 쉬운가요? | ❌ 아니요, 설정 복잡 & 트랜잭션 필요 |
| 실무에서는? | 대부분 at-least-once + 중복 방지 로직 자체 구현 |
메시지 필터링 & 지연 전송 & 예약 전송
Kafka는 기본적으로 고성능·대용량 스트리밍 전송에 최적화된 플랫폼이기 때문에,
RabbitMQ나 SQS처럼 메시지 필터링, 지연 전송, 예약 전송을 기본 제공하진 않지만,
적절한 아키텍처 설계 또는 Kafka Streams, Kafka Connect, 3rd-party tool을 활용하면 구현할 수 있습니다.
✅ 1. 메시지 필터링 (Message Filtering)
🔹 Kafka 기본 동작
- Kafka 자체는 브로커에서 메시지를 필터링하지 않음
- 즉, 브로커는 모든 메시지를 저장하고, 소비자(Consumer)가 필요한 것만 골라서 처리해야 함
🔹 필터링 구현 방법
| Consumer 측 필터링 (가장 일반적) | 메시지를 모두 가져온 후, 특정 조건에 맞는 것만 처리 |
| Kafka Streams | 필터 연산 (.filter(), .filterNot())을 통해 스트림 단위로 필터링 |
| Kafka Connect + SMT | 메시지를 Sink로 보내기 전에 변형/필터 |
| Spring Kafka | @KafkaListener 안에서 조건 분기 처리 가능 |
| Header 기반 필터링 | Kafka 메시지 header에 tag를 넣고, 이를 기준으로 처리 여부 결정 |
📌 브로커 수준 필터링은 기본적으로 없음 (→ 최근 KIP-704: 서버측 필터링 제안 중이긴 하나, 아직 도입 전)
🔹 Kafka Streams 예시
KStream<String, String> filtered = inputStream
.filter((key, value) -> value.contains("urgent"));
✅ 2. 메시지 지연 전송 (Delayed Delivery)
Kafka는 기본적으로 메시지를 즉시 전송합니다.
지연 전송(delay) 기능은 기본 제공하지 않지만, 몇 가지 방식으로 구현할 수 있습니다.
🔹 방법 1: Delay Topic + Scheduler 기반
- Producer가 메시지를 delay-orders라는 토픽에 저장
- Scheduler 또는 Kafka Streams Processor가 일정 주기마다 메시지를 확인
- 특정 timestamp가 되면 → orders 토픽으로 재전송
[delay-topic] → [Stream Processor / Scheduler] → [실제 대상 topic]
🔹 방법 2: Kafka Streams + Timestamp 기반 delay
- 메시지의 header나 value에 scheduled_time을 포함
- Streams에서 현재 시간보다 이전인 것만 forward 처리
.filter((key, value) -> value.scheduledTime < System.currentTimeMillis())
📌 이 방법은 정확한 지연은 보장하지 않지만, 실시간 연산에 가까운 방식으로 유사 delay 구현 가능
✅ 3. 예약 전송 (Scheduled Delivery)
예약 전송은 지연 전송보다 더 정확한 특정 시각에 메시지를 전송하고자 하는 기능입니다.
Kafka 자체는 지원하지 않지만 다음과 같이 구성할 수 있습니다:
🔹 방법 1: 외부 스케줄러 (Quartz, Spring Scheduler 등) 사용
- 메시지와 예약 시간을 DB 등에 저장
- 예약 시간이 되면 → Kafka에 전송
🔹 방법 2: Kafka Streams + Timestamp 기반 처리 (위와 동일)
- scheduledTime을 기준으로 정렬 및 대기
- 정확한 시각이 되면 실제 topic으로 forward
📌 완벽하게 "시계 맞춘 듯한 전송"을 하려면 Kafka 외부 시스템과의 협업이 필요합니다.
댓글