Kafka + AI 실시간 파이프라인 - 스트리밍 데이터에 LLM 적용하기
Apache Kafka 스트리밍 데이터에 LLM 추론을 적용하는 실시간 AI 파이프라인 구축 방법을 정리합니다. 실시간 분류, 감성 분석, 이상 탐지, 자동 요약 파이프라인을 다룹니다.
고객 리뷰가 쏟아지는 쇼핑몰을 상상해보세요. 하루에 수만 건의 리뷰가 올라오는데, "지금 이 순간 불만이 폭발하고 있다"는 사실을 다음 날 배치 분석 리포트로 알면 이미 늦습니다. 실시간으로 감지해서 즉시 대응해야 하죠.
Kafka + LLM 파이프라인이 바로 그 역할입니다. 데이터가 생성되는 그 순간, LLM이 분류하고 분석해서 알림을 쏘는 실시간 AI 파이프라인을 이 글에서 단계별로 구축해봅니다.
이 글에서 배우는 것
- 배치 처리와 실시간 처리의 차이, 그리고 실시간이 필요한 상황
- Kafka Consumer + LLM 추론 서비스를 연결하는 아키텍처 설계
- 감성 분류·이상 탐지 파이프라인 Python 구현
- 마이크로 배치로 처리량을 5~10배 높이는 최적화 기법
- 파티션 분산으로 처리량을 수평 확장하는 방법
1. 실시간 AI 파이프라인의 필요성
배치 vs 실시간
"배치로 하루에 한 번 돌리면 안 되나요?" — 물론 됩니다. 하지만 실시간 대응이 필요한 상황에서는 수분~수시간의 지연이 치명적입니다. 어떤 상황에서 실시간이 필요한지 먼저 파악하세요.
| 구분 | 배치 처리 (Spark + LLM) | 실시간 처리 (Kafka + LLM) |
|---|---|---|
| 지연시간 | 수분~수시간 | 수초~수십초 |
| 처리 방식 | 일괄 수집 후 처리 | 이벤트 발생 즉시 처리 |
| 적합 사례 | 일 단위 보고서, 배치 분류 | 실시간 알림, 즉시 대응 |
| LLM 호출 | 대량 배치 요청 | 건별 또는 마이크로 배치 |
활용 시나리오
| 시나리오 | Kafka 토픽 | LLM 처리 | 출력 |
|---|---|---|---|
| 실시간 감성 분석 | customer_reviews | 감성 분류 | 대시보드 + 알림 |
| 실시간 이상 탐지 | server_logs | 로그 분석 | PagerDuty 알림 |
| 실시간 콘텐츠 분류 | user_posts | 카테고리 분류, 유해성 검사 | 콘텐츠 필터링 |
| 실시간 번역 | chat_messages | 다국어 번역 | 번역된 메시지 전달 |
| 실시간 요약 | news_articles | 뉴스 요약 | 알림 서비스 |
2. 아키텍처
전체 구조
전체 흐름을 머릿속에 그려봅시다. 웹 이벤트·로그·IoT 데이터가 Kafka 입력 토픽으로 모이고, Consumer가 꺼내서 LLM에 넘기고, 결과를 출력 토픽으로 내보내는 구조입니다.
한 문장으로: Kafka는 고속 데이터 버퍼, LLM은 실시간 두뇌, Consumer는 이 둘을 잇는 연결고리입니다.
처리 패턴
3. 구현
Python Kafka Consumer + LLM
이제 직접 코드로 보겠습니다. 고객 리뷰를 실시간으로 읽어 감성 분류하고, 긴급 리뷰는 별도 알림 토픽으로 보내는 파이프라인입니다.
from confluent_kafka import Consumer, Producer
import json
import requests
# Kafka Consumer 설정
consumer = Consumer({
'bootstrap.servers': 'kafka:9092',
'group.id': 'llm-processor',
'auto.offset.reset': 'latest',
'enable.auto.commit': False,
'max.poll.interval.ms': 300000,
})
producer = Producer({'bootstrap.servers': 'kafka:9092'})
consumer.subscribe(['customer_reviews'])
def classify_with_llm(text: str) -> dict:
"""LLM으로 리뷰 분류"""
response = requests.post(
"http://vllm-server:8000/v1/chat/completions",
json={
"model": "meta-llama/Llama-3.1-8B-Instruct",
"messages": [{"role": "user", "content": f'리뷰를 분석하세요. JSON 반환: {{"sentiment": "긍정/부정/중립", "category": "카테고리", "urgent": true/false}}\n\n리뷰: {text}'}],
"temperature": 0.0,
"max_tokens": 100
},
timeout=10
)
return json.loads(response.json()["choices"][0]["message"]["content"])
# 메시지 처리 루프
while True:
msg = consumer.poll(1.0)
if msg is None:
continue
if msg.error():
continue
review = json.loads(msg.value().decode('utf-8'))
try:
result = classify_with_llm(review['text'])
# 결과를 출력 토픽에 전송
output = {**review, **result, "processed_at": datetime.now().isoformat()}
producer.produce(
'review_analysis',
key=msg.key(),
value=json.dumps(output).encode('utf-8')
)
# 긴급 리뷰는 알림
if result.get("urgent"):
producer.produce('urgent_alerts', value=json.dumps(output).encode('utf-8'))
consumer.commit(msg)
except Exception as e:
# 실패 시 DLQ(Dead Letter Queue)로 전송
producer.produce('review_dlq', value=msg.value())
consumer.commit(msg)
producer.flush()마이크로 배치 처리
건별로 LLM을 호출하면 처리량이 너무 낮습니다. 5초 또는 10건 중 먼저 도달하는 조건으로 메시지를 모아서 한 번에 보내면 처리량이 5~10배 높아집니다.
import asyncio
import aiohttp
from collections import deque
class MicroBatchProcessor:
def __init__(self, batch_size=10, batch_timeout=5.0):
self.batch_size = batch_size
self.batch_timeout = batch_timeout
self.buffer = deque()
async def process_stream(self, consumer, producer):
"""마이크로 배치 기반 스트림 처리"""
while True:
msg = consumer.poll(0.1)
if msg and not msg.error():
self.buffer.append(msg)
# 배치 크기 또는 타임아웃 도달 시 처리
if len(self.buffer) >= self.batch_size or \
(self.buffer and self._timeout_reached()):
batch = [self.buffer.popleft() for _ in range(min(len(self.buffer), self.batch_size))]
# 배치 LLM 호출
results = await self.batch_classify(batch)
for msg, result in zip(batch, results):
producer.produce('output_topic', value=json.dumps(result).encode())
consumer.commit(msg)
producer.flush()
async def batch_classify(self, messages):
"""배치 LLM 호출"""
texts = [json.loads(m.value())['text'] for m in messages]
# vLLM 배치 API 호출
async with aiohttp.ClientSession() as session:
tasks = [self.call_llm(session, t) for t in texts]
return await asyncio.gather(*tasks)4. 실시간 이상 탐지 파이프라인
서버 로그를 실시간으로 분석해 이상 징후를 즉시 잡아내는 파이프라인입니다. 규칙 기반 알람은 미리 정의한 패턴만 잡지만, LLM은 맥락을 이해해 새로운 유형의 이상도 찾아냅니다.
# 서버 로그 실시간 분석
def analyze_log(log_entry: str) -> dict:
"""로그 엔트리를 LLM으로 분석"""
response = requests.post(
"http://ollama:11434/api/generate",
json={
"model": "llama3.1:8b",
"prompt": f"""다음 서버 로그를 분석하세요.
로그: {log_entry}
JSON 반환:
{{"severity": "critical/warning/info", "category": "memory/disk/network/application/security", "requires_action": true/false, "summary": "한줄 요약"}}""",
"stream": False,
"options": {"temperature": 0.0}
}
)
return json.loads(response.json()["response"])5. 성능 최적화
실시간 파이프라인의 가장 큰 병목은 LLM 추론 속도입니다. 다음 전략들을 조합하면 같은 하드웨어에서 처리량을 크게 높일 수 있습니다.
| 전략 | 설명 | 지연시간 | 처리량 |
|---|---|---|---|
| 마이크로 배치 | N건 모아서 1회 호출 | +수초 | 5~10x |
| 비동기 호출 | asyncio로 병렬 LLM 호출 | 동일 | 3~5x |
| 경량 모델 | 7B Q4 모델 사용 | -50% | 2x |
| 결과 캐싱 | 유사 입력 캐시 | -90% (캐시 히트) | - |
| 컨슈머 스케일링 | 파티션 수만큼 컨슈머 확장 | 동일 | N배 |
| 우선순위 큐 | 긴급/일반 분리 처리 | 긴급: -80% | - |
파티션과 컨슈머 스케일링
처리량이 부족하면 단순히 Consumer와 LLM 서버를 추가하면 됩니다. Kafka 파티션 수만큼 Consumer를 늘리면 처리량이 선형으로 확장됩니다.
참고: 실시간 LLM 파이프라인에서 가장 큰 병목은 LLM 추론 속도입니다. 경량 모델(7B Q4)과 마이크로 배치를 조합하면 대부분의 실시간 요구사항을 충족할 수 있습니다.
마치며 — 핵심 요약
- 실시간 AI 파이프라인은 Kafka(버퍼) + Consumer(연결) + LLM(분석) + 출력 토픽(전달) 네 가지 조각으로 구성됩니다.
- 마이크로 배치(5초 또는 10건)로 LLM 호출을 묶으면 처리량이 5~10배 오르고 API 비용도 절감됩니다.
- **경량 모델(7B Q4)**을 로컬에서 vLLM으로 서빙하면 지연시간이 절반으로 줄어들고 외부 의존도가 사라집니다.
- 처리 실패 시 **DLQ(Dead Letter Queue)**로 보내서 재처리 경로를 확보하는 것이 운영 안정성의 핵심입니다.
- 파티션 수만큼 Consumer를 늘리면 처리량이 선형으로 확장됩니다. 먼저 충분한 파티션 수(최소 12개 권장)로 토픽을 설계하세요.
- 처음엔 건별 처리 → 마이크로 배치 → 비동기 병렬 순서로 단계적으로 최적화해나가면 됩니다.
References
- Apache Kafka Documentation — https://kafka.apache.org/documentation/
- Confluent Kafka Python — https://docs.confluent.io/kafka-clients/python/
- vLLM Documentation — https://docs.vllm.ai/
— Data Dynamics 엔지니어링 팀