Kafka Streams와 ksqlDB
왜 스트림 처리가 필요한가
Kafka 토픽에 쌓인 이벤트는 그 자체로는 정적인 로그다. 실시간으로 집계·변환·조인하려면 스트림 처리(stream processing) 레이어가 필요하다. Kafka 생태계는 두 가지 수준의 도구를 제공한다.
- Kafka Streams: Java 라이브러리. 별도 클러스터 없이 애플리케이션 프로세스 안에 삽입한다.
- ksqlDB: SQL 인터페이스가 있는 스트림 처리 데이터베이스. Kafka Streams 위에 구축됐다.
Kafka Streams
아키텍처 개요
Kafka Streams는 Kafka 클라이언트 라이브러리다. 처리 로직을 JAR에 포함하여 일반 Java 애플리케이션처럼 배포한다. 별도 스트림 처리 클러스터(Flink, Spark 등)가 필요 없다.
처리 그래프는 토폴로지(Topology) 로 표현한다. 토폴로지는 세 종류의 노드로 구성된 DAG(Directed Acyclic Graph) 다.
| 노드 종류 | 역할 |
|---|---|
| Source Processor | Kafka 토픽에서 레코드를 읽어 그래프에 주입 |
| Stream Processor | 변환·집계·조인 로직 수행 |
| Sink Processor | 결과를 Kafka 토픽 또는 외부 시스템에 출력 |
KStream, KTable, GlobalKTable
세 가지 핵심 추상화가 스트림-테이블 이중성(stream-table duality) 을 구현한다.
KStream — 이벤트 스트림. 같은 키로 새 레코드가 올 때마다 추가(append)된다. 집계 없이 이벤트 자체를 다룰 때 사용한다.
KTable — 키별 최신 상태를 나타내는 테이블. 같은 키로 새 레코드가 오면 이전 값을 덮어쓴다(upsert). 내부적으로 변경 로그 토픽(changelog topic)과 로컬 RocksDB 상태 저장소를 유지한다.
GlobalKTable — KTable과 같지만 모든 파티션을 인스턴스마다 완전히 복제한다. 파티션 수를 맞출 필요(co-partitioning) 없이 KStream-GlobalKTable 조인이 가능하다. 소규모 룩업 테이블(국가 코드, 설정 값 등)에 적합하다.
| KStream | KTable | GlobalKTable | |
|---|---|---|---|
| 의미론 | append | upsert | upsert (전체 복제) |
| 메모리 | — | 로컬 파티션 | 전체 복제 |
| 조인 시 co-partitioning | 필요 | 필요 | 불필요 |
상태 저장소와 내결함성
집계·조인처럼 이전 레코드를 참조하는 상태형 연산(stateful operation) 은 상태 저장소가 필요하다. 기본 구현은 RocksDB(임베디드 로컬 디스크 스토어)다. RocksDB 저장소는 로컬이므로 네트워크 I/O 없이 빠르다.
장애 복구를 위해 모든 상태 변경이 내부 changelog 토픽에 Kafka 레코드로 기록된다. 인스턴스가 재시작되면 changelog 토픽을 replay해 상태를 재구성한다.
윈도우 처리
무한 스트림을 유한한 시간 범위로 잘라 집계할 때 윈도우를 사용한다.
| 윈도우 종류 | 설명 |
|---|---|
| Tumbling | 고정 크기, 겹침 없음. 예: 1분 집계 |
| Hopping | 고정 크기지만 슬라이드 간격이 더 짧아 겹침 발생. 예: 1분 크기, 30초마다 슬라이드 |
| Session | 이벤트 간 비활성 간격으로 동적으로 구분. 사용자 세션 분석에 적합 |
ksqlDB
무엇인가
ksqlDB는 Kafka Streams 위에 만들어진 스트림 처리 데이터베이스다. SQL 문법으로 스트림 처리 로직을 정의한다. 개발자가 Java 코드를 작성하지 않아도 된다.
ksqlDB 서버는 독립 프로세스로 실행되며, 클라이언트는 REST API 또는 CLI(ksql)로 접근한다.
STREAM vs TABLE
ksqlDB에서 Kafka 토픽을 두 가지 뷰로 선언한다.
-- 이벤트 스트림 (KStream과 대응)
CREATE STREAM page_views (
user_id VARCHAR,
url VARCHAR,
ts BIGINT
) WITH (kafka_topic='page-views', value_format='JSON');
-- 키별 최신 상태 테이블 (KTable과 대응)
CREATE TABLE users (
user_id VARCHAR PRIMARY KEY,
country VARCHAR
) WITH (kafka_topic='users', value_format='JSON');Push 쿼리와 Pull 쿼리
ksqlDB는 두 가지 쿼리 모드를 구분한다.
Push 쿼리 — 스트림을 구독해 결과가 생길 때마다 클라이언트로 계속 전송한다. EMIT CHANGES 절로 표현한다. 연결이 끊어질 때까지 결과를 흘려준다.
-- 실시간 이상 주문 모니터링 (push)
SELECT user_id, SUM(amount) AS total
FROM orders
WINDOW TUMBLING (SIZE 1 MINUTE)
GROUP BY user_id
HAVING SUM(amount) > 10000
EMIT CHANGES;Pull 쿼리 — 현재 시점의 구체화 뷰(materialized view) 상태를 조회하고 즉시 반환한다. 일반 SQL SELECT와 유사하다. 낮은 지연으로 최신 집계값을 읽을 때 사용한다.
-- 특정 사용자의 현재 총 주문 금액 조회 (pull)
SELECT user_id, total_amount
FROM order_totals
WHERE user_id = 'u-1234';영구 쿼리(Persistent Query)
CREATE TABLE AS SELECT / CREATE STREAM AS SELECT로 만든 쿼리는 서버에서 계속 실행된다. 새 이벤트가 입력 토픽에 도착할 때마다 자동으로 처리돼 출력 토픽을 갱신한다.
-- 영구적으로 실행되는 분당 집계 테이블
CREATE TABLE order_totals AS
SELECT user_id, SUM(amount) AS total_amount
FROM orders
WINDOW TUMBLING (SIZE 1 MINUTE)
GROUP BY user_id
EMIT CHANGES;Kafka Streams vs ksqlDB 선택 기준
| 기준 | Kafka Streams | ksqlDB |
|---|---|---|
| 언어 | Java/Kotlin | SQL |
| 배포 | 애플리케이션 JAR에 내장 | 독립 서버 클러스터 |
| 유연성 | Processor API로 임의 로직 가능 | SQL 표현력 범위 내 |
| 운영 복잡도 | 낮음 (별도 클러스터 없음) | 중간 (ksqlDB 서버 관리 필요) |
| 학습 곡선 | Java 숙련 필요 | SQL 문법만으로 시작 가능 |
| 적합한 사용 | 복잡한 비즈니스 로직, 마이크로서비스 | 탐색적 분석, 프로토타입, 실시간 대시보드 |
두 도구는 경쟁이 아니라 보완 관계다. ksqlDB는 내부적으로 Kafka Streams 위에 동작하며, 복잡한 처리가 필요할 경우 Kafka Streams로 내려가 구현하면 된다.
Open Questions
- ksqlDB의 라이선스(Confluent Community License)는 자사 서비스 제공을 제한한다. 2026년 현재 Apache Flink SQL이 클라우드 중립적 대안으로 자주 언급되나, ksqlDB-Flink 전환 비용과 SQL 호환성은 별도 검토 필요.
- Kafka Streams 인스턴스를 여러 대로 확장할 때 상태 저장소 standby replica 설정(
num.standby.replicas)이 재시작 속도에 미치는 영향은 실제 측정값으로 검증 필요.
References
- https://kafka.apache.org/documentation/streams/
- https://kafka.apache.org/42/streams/core-concepts/
- https://docs.confluent.io/platform/current/streams/concepts.html
- https://docs.confluent.io/platform/current/ksqldb/overview.html
- https://developer.confluent.io/courses/ksqldb/push-queries-and-pull-queries/
- https://www.confluent.io/blog/how-real-time-materialized-views-work-with-ksqldb/
- https://risingwave.com/blog/kafka-stream-processing-sql-beyond-ksqldb/
- https://medium.com/@zdb.dashti/how-kafka-streams-uses-rocksdb-for-state-management-and-fault-tolerance-b8bb8fd14439