데이터 엔지니어 · 데이터 사이언티스트를 위한 핵심 요약
Kafka Streams는 Apache Kafka에 내장된 오픈소스 클라이언트 라이브러리로, Kafka 토픽을 입력/출력으로 사용하는 실시간 스트림 처리 애플리케이션을 Java/Scala/Kotlin으로 쉽게 빌드할 수 있게 해줍니다. 별도의 클러스터 관리 없이 Kafka 브로커에 의존해 가볍고 확장 가능하며, exactly-once semantics를 지원합니다.
데이터 엔지니어와 데이터 사이언티스트에게 적합한 이유는 실시간 데이터 파이프라인, ETL, ML 피처 엔지니어링 등에서 Kafka 생태계 내에서 일관되게 처리할 수 있기 때문입니다.
밀리세컨드 단위 저지연으로 무한 스트림을 처리. map, filter, join 등 DSL로 간단 구현.
KTable과 State Store로 집계, 윈도잉, 세션화 지원. changelog 토픽으로 fault-tolerance.
트랜잭션과 idempotent producer로 데이터 중복 없이 처리. 생산성 극대화.
Kafka Streams는 기존 배치/스트림 도구 대비 가볍고 Kafka-native라 운영 부담이 적습니다. 아래 표에서 주요 대안과 비교:
| 기능 | Kafka Streams | Apache Flink | Spark Streaming | Storm |
|---|---|---|---|---|
| 지연(Latency) | Sub-second | Sub-second | 1-10초 | Sub-second |
| 운영 복잡도 | 낮음 (Kafka만) | 높음 (별도 클러스터) | 중간 (YARN) | 높음 |
| Exactly-Once | 예 | 예 | 예 (Structured Streaming) | 부분 |
| 상태 관리 | RockDB/Changelog | State Backend | Checkpoint | 외부 |
| 언어 | Java/Kotlin/Scala | Java/Scala/Python | Scala/Python/Java | Java |
| 비용 | 최저 (라이브러리) | 높음 | 중간 | 중간 |
실무에서 Kafka Streams는 마이크로서비스 내 스트림 처리에 최적화되어 있습니다.
Kafka Streams의 핵심은 Topology로, 소스(Source) → 프로세서(Processor) → 싱크(Sink)로 구성됩니다. KStream(무상태 스트림), KTable(업데이트 스트림), GlobalKTable(전역 테이블) 등을 DSL로 조합합니다.
위 아키텍처에서 Streams 앱은 Kafka 파티션만큼 인스턴스 스케일아웃됩니다.
Python 우선으로 kafka-python 라이브러리를 사용한 기본 상호작용 예제와 함께, Kafka Streams Java 예제를 병행합니다. Kafka 브로커는 Docker로 빠르게 시작.
# Docker로 Kafka + Zookeeper 실행 (포트 9092)
docker run -d --name kafka -p 9092:9092 \
-e KAFKA_ZOOKEEPER_CONNECT=localhost:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 \
confluentinc/cp-kafka:latest
# 토픽 생성
docker exec kafka kafka-topics --create --topic input-topic --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1
docker exec kafka kafka-topics --create --topic output-topic --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1
# pip install kafka-python
from kafka import KafkaProducer, KafkaConsumer
import json
# Producer
producer = KafkaProducer(bootstrap_servers='localhost:9092', value_serializer=lambda v: json.dumps(v).encode('utf-8'))
for i in range(10):
producer.send('input-topic', {'word': f'test-{i}'})
producer.flush()
# Consumer
consumer = KafkaConsumer('input-topic', bootstrap_servers='localhost:9092', value_deserializer=lambda m: json.loads(m.value.decode('utf-8')))
for msg in consumer:
print(msg.value)
<!-- pom.xml -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
<version>3.6.0</version>
</dependency>
import org.apache.kafka.streams.*;
import org.apache.kafka.streams.kstream.*;
import java.util.Arrays;
import java.time.Duration;
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-app");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> textLines = builder.stream("input-topic");
KTable<String, Long> wordCounts = textLines
.flatMapValues(line -> Arrays.asList(line.toLowerCase().split(" ")))
.groupBy((key, word) -> word)
.count();
wordCounts.toStream().to("output-topic", Produced.with(Serdes.String(), Serdes.Long()));
KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();
streams.cleanUp(); // Graceful shutdown
실행: mvn compile exec:java -Dexec.mainClass="WordCountApp". Python으로 input-topic에 데이터 넣으면 output-topic에서 집계 확인.
| 설정 | 기본값 | 설명 | 권장 실무값 |
|---|---|---|---|
| application.id | - | 앱 식별자 (State Store 그룹화) | unique-per-app |
| cache.max.bytes.buffering | 10MB | 버퍼 크기 (메모리 사용 ↑) | 50MB ~ 100MB |
| commit.interval.ms | 30s (AT_LEAST_ONCE), 1s (EXACTLY_ONCE) | 오프셋 커밋 주기 | 100ms (저지연) |
| num.stream.threads | 1 | 프로세서 스레드 수 | CPU 코어 수 |
| max.in.flight.requests.per.connection | 5 | 비동기 요청 (Throughput ↑) | 8 (Exactly-Once 시 1) |
| rocksdb.config.setter | - | State Store 튜닝 | block_cache=4GB |
| processing.guarantee | at_least_once | Exactly-once 활성 | exactly_once |
JMX Metrics 활성화 (kafka-streams-metrics.properties). Prometheus + Grafana로 changelog lag, restore 시간 추적. Standby tasks=1로 HA 강화.
num.stream.threads 증가, buffer 증가. Python consumer에서 max_poll_records=500으로 조절.
원인: 디스크 풀/네트워크 파티션. 해결: application.server 설정 후 kafka-streams-application-reset.sh --application-id wordcount-app --input-topics input-topic 실행 후 재시작.
원인: 파티션 리밸런싱. 해결: session.timeout.ms=30s, max.poll.interval.ms=5m. Topology optimization으로 join 최소화.
Python으로 피처 생성 후 Kafka 토픽으로 넣고 Streams로 집계. confluent-kafka로 고성능 consumer 사용.