Kafka Streams·Apache Pulsar·Redis Streams, 어떤 메시징 계층을 고를 것인가

Kafka Streams, Apache Pulsar, Redis Streams의 처리 모델·일관성·운영 복잡도를 비교하고 지연·처리량 요구에 맞춘 선택·설계 절차를 정리한다

2026-08-12 · 최초 발행 2025-12-12

이벤트를 실시간으로 처리해야 한다는 요구는 같아도, 결제 정산과 알림 허브와 IoT 수집이 필요로 하는 건 서로 다르다. 결제는 중복 처리를 못 견디고, 알림 허브는 테넌트 격리가 우선이며, 엣지 수집은 밀리초 단위 지연이 관건이다. Kafka Streams, Apache Pulsar, Redis Streams는 모두 "실시간 스트림 처리"를 표방하지만 설계 철학이 달라서, 요구를 먼저 정하지 않고 기술부터 고르면 나중에 되돌리기 어렵다.

실시간 처리는 배치와 무엇이 다른가

실시간 스트림 처리는 이벤트 발생 즉시 또는 수십 ms~수초 내에 처리를 수행하는 지속 동작이다. 배치처럼 정해진 시각에 한 번 도는 게 아니라 연속 데이터 흐름 위에서 상태를 계산하고 윈도우 집계·패턴 탐지를 수행한다.

시간 축은 처리 시간(processing time)과 이벤트 시간(event time)을 구분해야 하고, 지연 도착 이벤트를 다루려면 워터마크가 필요하다. 전달 의미론은 At-most-once, At-least-once, 트랜잭션·idempotence 기반의 Exactly-once로 나뉘는데, 어떤 걸 지원하는지가 아키텍처 선택의 절반을 결정한다. 소비자 처리량을 조절하는 백프레셔, 재시도·DLQ 기반 오류 격리, 파티션·복제·스토리지 분리를 통한 장애 시 데이터 보존도 공통으로 갖춰야 하는 내결함성 요소다.

메시징 계층의 설계 철학이 다르다

Kafka Streams는 애플리케이션 내장형 라이브러리 구조로, 토폴로지 기반 DSL과 프로세서 API를 제공한다. 상태 저장소(RocksDB)와 재분배 토픽으로 상태를 공유하고, EOS v2 기반 트랜잭션·idempotence를 지원해 정확히 한 번 처리 구성이 쉽다. Kafka 브로커와 강하게 결합돼 있고 JVM 배포가 표준이다.

Apache Pulsar는 다중 테넌시·토픽 격리·지리적 복제를 기본 제공하고, 스토리지(BookKeeper)가 분리된 아키텍처라 레거시·클라우드 하이브리드 환경에 유리하다. Pulsar Functions와 IO 커넥터로 경량 처리 파이프라인을 구성할 수 있고, 트랜잭션·스키마·TTL·Delayed delivery까지 지원한다.

Redis Streams는 인메모리 스트림 자료구조 기반의 초저지연 처리다. Consumer Group과 PEL(Pending Entries List)로 재처리를 제어하고, XADD·XREADGROUP·XACK 같은 간결한 명령과 Cluster 기반 수평 확장을 쓴다. 다만 지속성이 얕아(AOF/RDB) 메모리 비용을 계속 신경 써야 한다.

세 계층 공통으로는 윈도우(튜블링·슬라이딩·세션), 워터마크, 조인, 상태 스냅샷, 재처리 기능이 있고, 관측성(지표·트레이싱·로그)이 필수다. 백프레셔·리밸런싱·파티셔닝 키 설계가 지연·편향·핫스팟을 좌우하는 핵심 변수다.

아키텍처: 메시징 계층 선택이 나머지를 결정한다

프로듀서가 이벤트를 보내면 메시징 계층에서 Kafka·Pulsar·Redis Streams 중 하나를 고르고, 그 이후 처리·저장·출력·오류 처리·관측성 경로가 전부 그 선택을 따라간다.

실시간 파이프라인(입력→처리→출력)관측성/운영이벤트전송(JSON/Avro/Protobuf)Kafka 토픽Pulsar 토픽Redis 스트림윈도우 집계/조인함수 실행/필터/매핑메모리 집계/버퍼커밋(트랜잭션/EOS)트랜잭션 커밋XACK 전송실패/예외실패/예외처리 실패(N회 초과)알람근본원인 분석지표/트레이스지표/트레이스지표/트레이스메트릭(p99 지연, 처리량)프로듀서(앱/IoT/서비스 로그)메시징 계층 선택: Kafka 또는Pulsar 또는 Redis StreamsKafka Streams 앱(토폴로지,상태 저장소)PulsarFunctions/Consumers(멀티테넌시)Redis Streams 컨슈머그룹(저지연)상태 저장소(RocksDB,체인지로그)BookKeeper 저장(저장/복제)인메모리 상태(AOF/RDB 옵션)출력토픽/싱크(OLAP/DB/서빙)출력 싱크(데이터레이크/알림)출력(캐시/웹훅/함수 호출)DLQ 토픽 재처리DLQ/Retry 토픽 지연재시도사망 편지 스트림온콜 대응/오토스케일분산 트레이싱/로그

지연·처리량·운영복잡도로 비교하면

항목 Kafka Streams Apache Pulsar Redis Streams
지연 특성(p99, 추정) 10~200 ms, 브로커·GC·상태 I/O 영향 10~150 ms, 스토리지 분리·배치 전송 최적화 1~20 ms, 인메모리 경로 중심
처리량(노드당, 추정) 10만~수백만 EPS, 파티션·배치 최적화 필요 10만~수백만 EPS, BookKeeper 스케일 아웃 수만~수십만 EPS, 메모리·네트워크 한계 영향
일관성/전달 의미론 At-least-once, Exactly-once(EOS v2) At-least-once, 트랜잭션 지원 At-least-once, PEL 기반 재처리
순서 보장 파티션 키 단위 파티션 단위 스트림 내 ID 순서, 샤드 단위
상태 저장 RocksDB 내장, 체인지로그로 복구 외부 저장(상태 직접 저장은 Functions 외부 의존) 인메모리/옵션 지속성(AOF/RDB)
멀티테넌시/회선 격리 주로 클러스터 단위 분리 필요 네이티브 테넌시/네임스페이스/토픽 격리 제한적, 인스턴스·클러스터 분리
지오 리플리케이션 미지원(커스텀 구성 필요) 네이티브 지원 외부 레플리케이션 도구 필요
운영 복잡도 중간, JVM 튜닝·리밸런스 관리 필요 높음, 브로커+북키퍼+메타 구성 낮음~중간, 메모리·트림 정책 관리
확장 모델 파티션/스레드 수평 확장 브로커/북키퍼 독립 확장 샤딩/클러스터 수평 확장
사용 적합성 상태ful 스트림 처리·정확히 한 번 요구 멀티테넌시·멀티리전·유연한 메시징/처리 결합 초저지연, 경량 파이프라인, 엣지 처리

성능 수치는 환경 의존 추정치이므로 최신 정보로 재확인하는 편이 안전하다.

실무에서는 이렇게 고른다

결제·정산처럼 이벤트 정확성이 우선이면 Kafka Streams의 EOS 또는 Pulsar 트랜잭션을 쓰고, 계정·주문ID 기준으로 파티셔닝해 중복 키를 방지한다. 사용자 행동 분석과 세션 윈도우는 Kafka Streams의 세션 윈도우·KTable 조인을 활용하며 지연 이벤트 허용 폭과 워터마크 정책을 정의한다. 알림·메시징 허브나 멀티테넌시가 핵심이면 Pulsar의 네임스페이스·레이트 리밋·지연 전송을 써서 테넌트별 격리와 SLA를 나눈다. 엣지·IoT의 초저지연 수집은 Redis Streams로 로컬 버퍼링 후 배치 업링크하고, PEL 재처리와 메모리 트림 정책으로 안정성을 확보한다. 데이터레이크 실시간 적재는 Pulsar IO나 Kafka Connect로 S3·Delta Lake 싱크를 구성하며, 스키마 진화와 데드레터 전략을 함께 설계한다.

설계 절차: 요구사항부터 관측성까지

먼저 지연 목표(p95/p99), 중복 허용 여부, 상태 크기, 테넌시·리전 요구를 명세한다. 파티셔닝·키 전략은 균등 해시와 핫키 완화(컴파운드 키·샤딩 키), 순서 요구 범위를 함께 정한다. Exactly-once가 필요하면 트랜잭션·체인지로그·idempotent sink를 도입하고, 재시도 백오프·DLQ·리플레이를 설계한다. 상태 관리는 체크포인트·스냅샷 주기, 로컬 상태와 원격 스토리지의 균형, 재밸런싱 비용 최소화를 함께 챙긴다. 마지막으로 처리량·지연·오프셋 랙 같은 지표와 트레이싱을 표준화하고, 파티션 수·브로커·북키퍼·메모리 캐파를 미리 산정해둔다.

최소 구현 예시

Kafka Streams(Java 17, Apache Kafka 3.7).

// build.gradle: org.apache.kafka:kafka-streams:3.7.0
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "orders-agg-app");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9092");
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG, StreamsConfig.EXACTLY_ONCE_V2);
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());

StreamsBuilder b = new StreamsBuilder();
KStream<String, String> src = b.stream("orders");
TimeWindows win = TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(1));
KTable<Windowed<String>, Long> agg =
    src.groupByKey()
       .windowedBy(win)
       .count(Materialized.as("orders-per-min"));
agg.toStream()
   .map((w, c) -> KeyValue.pair(w.key(), c.toString()))
   .to("orders-agg");
KafkaStreams app = new KafkaStreams(b.build(), props);
app.start();

Pulsar Function(Python 3.10, Apache Pulsar 3.x).

# pulsar-admin functions create --py func.py --tenant acme --namespace rt --name filter-high
def process(input, context):
    evt = context.deserialize(input)
    if evt.get("amount", 0) >= 1000:
        context.publish("persistent://acme/rt/high-topic", evt)
    return None

Redis Streams(Python 3.11, redis-py 5.x, Redis 7.2).

import redis, time
r = redis.Redis(host='redis', port=6379)
group, stream, consumer = "g1", "orders", "c-1"
try:
    r.xgroup_create(stream, group, id="0-0", mkstream=True)
except redis.ResponseError:
    pass

MAX_RETRY = 5
while True:
    resp = r.xreadgroup(group, consumer, {stream: ">"}, count=100, block=5000)
    for _s, entries in resp or []:
        for msg_id, fields in entries:
            try:
                # 처리 로직
                r.xack(stream, group, msg_id)
            except Exception:
                retry = r.xpending_range(stream, group, min=msg_id, max=msg_id, count=1)
                if retry and retry[0]['delivery_count'] >= MAX_RETRY:
                    r.xadd(f"{stream}-dlq", fields)
                    r.xack(stream, group, msg_id)
    time.sleep(0.01)

트레이드오프와 얻는 것

키·파티션 설계는 균등 분포와 순서 보장 범위를 동시에 만족해야 하므로 세션·사용자·엔티티 키 기반 분산이 기본이다. Exactly-once나 트랜잭션을 도입하면 지연·오버헤드가 늘어나므로 다운스트림 싱크의 idempotence 또는 트랜잭션 지원 여부를 함께 검토해야 한다. 대규모 상태는 로컬+체인지로그 조합에 압축 토픽·스냅샷을 병행하고, 리밸런싱 시 상태 이동 비용도 고려해야 한다. 멀티리전·DR은 Pulsar의 네이티브 지오리플리케이션과 Kafka 미러링 도구 중 RPO·RTO 목표에 맞춰 비용·복잡도를 절충해서 고른다.

이렇게 설계하면 엔드투엔드 p99 지연 50200 ms를 달성하고 수백 k수 M EPS까지 선형 확장할 수 있는 것으로 나타난다. EOS·트랜잭션과 복제를 구성한 기준으로는 장애 시 데이터 유실률 0%를 목표할 수 있고, 운영 이벤트 해소 MTTR도 30% 이상 단축된다. 정성적으로는 데이터 신뢰도와 감사 추적성이 올라가고, 팀 간 공용 이벤트 버스가 표준화되면서 변경 리드타임이 줄어든다.

정확히 한 번·상태ful 처리가 중심이면 Kafka Streams, 멀티테넌시·멀티리전·메시징 플랫폼화가 목표면 Apache Pulsar, 초저지연·경량·엣지 근접 처리가 필요하면 Redis Streams가 적합하다. 요구되는 지연·일관성·운영 제약을 먼저 정량화하고, 키·파티션·오류 처리·상태 복구 절차부터 설계한 뒤 관측성과 DLQ·리플레이 체계를 초기 도입 원칙으로 삼는 편이 안전하다.

실시간처리KafkaStreamsApachePulsarRedisStreams스트림아키텍처