Kafka Streams와 Schema Registry로 완성하는 견고한 실시간 데이터 파이프라인 구축 가이드
백엔드 시스템이 복잡해지고 데이터의 양이 폭증하면서, 단순히 '메시지를 전달하는 통로' 역할만 하는 메시징 큐로는 더 이상 충분하지 않은 시점이 왔습니다. 우리는 이제 데이터의 **무결성(Integrity)**을 보장하고, 메시지 전송 과정에서 복잡한 비즈니스 로직을 실시간으로 변환하는 수준의 아키텍처를 요구받고 있습니다.
이 글에서는 대용량 실시간 스트리밍 데이터 파이프라인의 핵심 구성 요소인 Kafka Streams와 Schema Registry를 결합하여, 데이터 거버넌스 수준의 견고한 시스템을 구축하는 방법을 데이터 엔지니어의 관점에서 깊이 있게 다뤄보겠습니다.
1. 메시징 큐의 한계를 넘어서: 왜 스트리밍 처리가 필요한가?
기존의 메시징 큐(Message Queue)는 주로 '비동기 통신'과 '신뢰성 있는 메시지 전달'에 초점을 맞춥니다. 즉, A 서비스가 B 서비스에게 "이 작업 좀 해줘"라는 신호를 보내는 데 최적화되어 있죠.
하지만 실제 비즈니스 로직은 훨씬 복잡합니다. 예를 들어, '사용자가 장바구니에 상품을 추가(Event A)하고, 5분 이내에 결제(Event B)를 완료했을 때만, 할인 쿠폰을 적용하여 최종 주문(Event C)을 생성'해야 한다고 가정해 봅시다.
이 경우, 단순히 메시지를 큐에 넣는 것만으로는 부족합니다. 우리는 다음 세 가지를 반드시 해결해야 합니다.
- 데이터 구조의 일관성: 들어오는 모든 데이터가 정해진 규칙(스키마)을 따르는가?
- 상태 유지 및 집계: 특정 시간 범위(Window) 내의 여러 이벤트들을 모아서 계산할 수 있는가?
- 복잡한 변환 로직: A와 B 이벤트를 결합하여 C라는 새로운 비즈니스 엔티티를 생성할 수 있는가?
이러한 요구사항을 충족시키는 것이 바로 **스트리밍 데이터 처리(Stream Processing)**의 영역이며, Kafka Streams가 그 핵심 엔진 역할을 수행합니다.
2. 스트리밍 데이터의 생명줄: Schema Registry와 Avro의 이해
스트리밍 파이프라인에서 가장 치명적인 문제는 **스키마 불일치(Schema Mismatch)**입니다. 개발자가 실수로 필드를 추가하거나, 데이터 소스에서 필드 이름이 변경되면, 다운스트림 시스템은 예고 없이 다운되거나 잘못된 데이터를 처리하게 됩니다.
이 문제를 해결해주는 것이 바로 Schema Registry입니다.
Schema Registry는 데이터의 '청사진' 역할을 합니다. Kafka 토픽에 데이터가 기록되기 전에, 해당 데이터가 어떤 구조(스키마)를 가져야 하는지 중앙에서 관리하고 검증하는 시스템입니다.
💡 핵심 개념: 스키마 진화 (Schema Evolution)
데이터가 시간이 지남에 따라 변화하는 것을 '스키마 진화'라고 합니다. Schema Registry는 이 변화를 안전하게 관리하는 메커니즘을 제공합니다.
- Backward Compatibility (하위 호환성): 최신 버전의 스키마를 가진 Producer가, 이전 버전의 스키마를 기대하는 Consumer에게 데이터를 보낼 때 문제가 없는 경우입니다. (예: 필드를 추가하되, 기본값을 지정하는 경우)
- Forward Compatibility (상위 호환성): 이전 버전의 스키마를 가진 Consumer가, 최신 버전의 스키마를 가진 Producer로부터 데이터를 받을 때 오류 없이 처리할 수 있는 경우입니다. (예: 필드를 제거할 때, Consumer가 해당 필드를 무시할 수 있도록 설계하는 경우)
대부분의 경우, Avro 포맷과 함께 사용될 때 이 진화 관리가 가장 강력하게 작동합니다. Avro는 스키마와 데이터를 분리하여 관리하기 때문에, 데이터 자체에 스키마 정보가 포함되어 전송되더라도, Schema Registry가 이를 검증하고 관리할 수 있게 됩니다.
3. 실시간 데이터 변환 엔진: Kafka Streams 마스터하기
Schema Registry가 '규칙'을 정한다면, Kafka Streams는 그 규칙에 따라 '실제 계산'을 수행하는 엔진입니다. Kafka Streams는 Kafka 토픽을 마치 메모리상의 데이터셋처럼 취급하여, 복잡한 상태 기반(Stateful) 연산을 수행할 수 있게 해줍니다.
가장 실용적인 예시로, '특정 시간 동안의 사용자 활동 집계'를 구현해 보겠습니다.
📊 Windowing과 Grouping을 이용한 실습 예시 (Pseudo Code)
사용자 활동 로그(User Activity Log)가 들어온다고 가정하고, 5분 단위로 가장 많이 활동한 사용자 수를 계산하는 로직을 구현해 보겠습니다.
// 1. 소스 토픽 구독 및 키 기반 그룹화
KStream<String, ActivityEvent> inputStream = builder.stream("user_activity_topic");
// 2. 사용자 ID를 키로 지정하여 그룹화 (groupByKey)
KGroupedStream<String, ActivityEvent> groupedStream = inputStream.groupByKey();
// 3. 5분 윈도우(Window)를 설정하고 집계 수행 (windowedBy)
KTable<Windowed<String>, Long> userCount = groupedStream
.windowedBy(TimeWindows.of(Duration.ofMinutes(5))) // 5분 간격으로 윈도우 설정
.count(); // 해당 윈도우 내의 이벤트 개수 카운트
// 4. 결과 토픽으로 출력
userCount.toStream().to("user_activity_summary_topic");위 코드에서 핵심은 다음과 같습니다.
groupByKey(): 동일한 키(여기서는 사용자 ID)를 가진 모든 이벤트를 하나의 논리적 그룹으로 묶습니다.windowedBy(): 이 그룹화된 데이터를 시간적 범위(Window)로 제한합니다. 5분이라는 시간 창이 지나면 카운트가 리셋됩니다.count(): 이 윈도우 내에서 발생한 모든 이벤트를 집계합니다.
이러한 상태 기반 연산(Stateful Operation) 덕분에, 우리는 단순한 메시지 처리를 넘어 '사용자 세션 분석', '재고 수준 변화 감지' 등 복잡한 비즈니스 로직을 실시간으로 구현할 수 있습니다.
4. 견고한 아키텍처 설계 및 운영 Best Practice
성공적인 스트리밍 파이프라인은 단순히 기술을 나열하는 것이 아니라, 이들이 어떻게 연결되어 작동하는지에 대한 설계가 중요합니다.
🔗 전체 데이터 흐름 다이어그램 (개념적 흐름)
데이터는 다음과 같은 순서로 흐르며, 각 단계에서 무결성이 검증됩니다.
[데이터 소스] $\rightarrow$ [Kafka Producer] $\xrightarrow{\text{Avro/Schema Validation}}$ [Schema Registry] $\rightarrow$ [Kafka Topic] $\xrightarrow{\text{Consume}}$ [Kafka Streams App] $\xrightarrow{\text{Transform/Aggregate}}$ [Output Topic] $\rightarrow$ [다운스트림 시스템]
이 흐름에서 Kafka Connect는 외부 DB나 파일 시스템에서 데이터를 Kafka Topic으로 가져오는 역할을 담당하며, 이 과정에서도 Schema Registry의 검증을 거치게 됩니다.
🛡️ 운영 환경에서 발생하는 문제 해결 방안 3가지
실제 운영 환경에서는 예상치 못한 문제가 발생합니다. 다음은 데이터 유실 및 스키마 불일치에 대비하는 실무적인 방안입니다.
- Dead Letter Queue (DLQ) 도입: Kafka Streams나 Connect에서 파싱 오류가 발생한 메시지는 즉시 실패 처리하지 않고, 별도의 'DLQ 토픽'으로 전송합니다. 운영팀은 이 DLQ를 주기적으로 모니터링하여 원인 분석 및 재처리(Replay)를 수행합니다.
- 멱등성(Idempotency) 보장: 스트리밍 프로세스는 재처리가 잦습니다. 따라서, 최종 결과가 데이터베이스에 기록될 때, 동일한 이벤트가 두 번 처리되어 데이터가 중복 저장되는 것을 막기 위해 고유 트랜잭션 ID를 기반으로 멱등성을 보장하는 로직을 반드시 구현해야 합니다.
- 스키마 버전 관리 및 롤백 전략: 스키마 변경이 불가피할 경우, 새로운 스키마를 적용하기 전에 반드시 이전 버전과의 호환성을 테스트하는 'Staging 환경'을 거쳐야 하며, 문제가 발견되면 즉시 이전 버전의 스키마로 롤백할 수 있는 자동화된 메커니즘을 갖춰야 합니다.
✨ 실무자 관점의 경험 공유:
제가 가장 중요하다고 느낀 부분은 '오류 처리'입니다. 많은 개발자가 성공 케이스(Happy Path)에만 집중하지만, 실제 스트리밍 시스템은 '실패 케이스'가 더 많습니다. Kafka Streams의 try-catch 블록을 사용하여 예외를 잡고, 그 예외를 구조화하여 DLQ로 보내는 패턴을 습관화하는 것이 시스템 안정성의 80%를 좌우한다고 확신합니다.
5. 결론: 견고한 스트리밍 시스템 구축을 위한 로드맵
Kafka Streams와 Schema Registry의 조합은 단순한 메시지 전달을 넘어, 데이터의 구조적 무결성과 복잡한 비즈니스 로직을 결합한 '신뢰할 수 있는 데이터 파이프라인'을 완성합니다.
이번 에피소드 요약:
- 메시징 큐의 한계를 인지하고 스트림 처리의 필요성을 이해했습니다.
- Schema Registry를 통해 Avro 기반의 스키마 진화(호환성)를 관리하는 방법을 배웠습니다.
- Kafka Streams의
groupByKey와windowedBy를 활용하여 상태 기반 로직을 구현하는 방법을 익혔습니다.
다음 편에서는 이 파이프라인의 시작점인 '데이터 수집' 단계에 초점을 맞춥니다. Kafka Connect를 활용하여 관계형 데이터베이스(RDB)나 NoSQL 등 이기종 시스템의 데이터를 Kafka로 안정적으로 통합하는 심화 과정을 다뤄보겠습니다.
자주 묻는 질문 (FAQ)
Q1. Kafka Streams와 Kafka Connect 중 무엇을 먼저 배워야 하나요? A1. 두 기술은 상호 보완적입니다. Kafka Connect는 외부 데이터를 Kafka로 '넣는' 역할(Ingestion)에 특화되어 있고, Kafka Streams는 Kafka 내부 데이터를 '가공하고 변환'하는 로직 구현에 특화되어 있습니다. 데이터 흐름 전체를 이해하기 위해 순차적으로 학습하는 것을 추천합니다.
Q2. Avro를 사용하지 않고 JSON으로만 데이터를 처리해도 되나요? A2. 기술적으로는 가능하지만, 운영 환경에서는 강력히 비추천합니다. JSON은 스키마가 유연하다는 장점이 있지만, 그 유연함이 곧 '규칙 없음'으로 해석되어 데이터 무결성 검증이 어렵습니다. Schema Registry와 Avro를 사용해야 데이터 거버넌스가 확보됩니다.
Q3. Stateful Operation을 사용할 때 메모리 관리가 중요한가요? A3. 매우 중요합니다. Kafka Streams는 내부적으로 상태 저장소(State Store)를 사용하며, 이 상태가 메모리나 로컬 디스크에 저장됩니다. 대규모 데이터를 처리할 경우, 상태 크기(State Size)가 너무 커지면 JVM 메모리 부족(OOM)이나 성능 저하를 일으킬 수 있으므로, 주기적인 상태 검토와 최적화가 필수적입니다.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...