foreachBatch — 마이크로배치 출력을 평범한 배치 DataFrame처럼 다루기
Spark foreachBatch 및 Vectorized Parquet Reader 공식 문서 종합 (웹 리서치, 2026) — Databricks 공식 문서(structured-streaming/foreach) + The Internals of Spark SQL(Vectorized Decoding) Databricks 공식 문서 Use foreachBatch to write to arbitrary data sinks
이 모듈을 다 읽으면
- foreachBatch가 스트리밍 출력을 배치 DataFrame으로 노출한다는 것을 설명할 수 있다
- 여러 싱크에 쓸 때의 트레이드오프와 idempotent 싱크가 필요한 이유를 설명할 수 있다
spark-learn-37에서 다룬 마이크로배치 실행 모델을 전제로, 각 마이크로배치의 출력을 스트리밍 전용 API가 아니라 평범한 배치 모드 DataFrame으로 다뤄, 배치용 로직이나 여러 싱크에 자유롭게 쓸 수 있게 해주는 foreachBatch를 정리한다.
핵심 아이디어 — 마이크로배치 하나하나를 배치 DataFrame으로 받는다
streamingDF.writeStream.foreachBatch(함수)로 등록한 함수는, 각 마이크로배치가 끝날 때마다 그 배치의 출력 데이터(DataFrame)와 마이크로배치의 고유 ID, 이 두 파라미터를 받아 호출된다. 이 DataFrame은 스트리밍 전용 제약이 걸린 게 아니라 완전히 평범한 배치 모드 DataFrame이라, 배치 전용 데이터소스 커넥터(스트리밍을 네이티브로 지원하지 않는 것들)로 쓰거나, 배치에서만 되는 연산을 적용하는 등 자유도가 훨씬 크다.
핵심 포인트
- foreachBatch 함수는 (마이크로배치 출력 DataFrame, 배치 고유 ID) 두 파라미터를 받아 매 마이크로배치마다 호출됨
- 이 DataFrame은 완전히 평범한 배치 모드 DataFrame — 스트리밍 네이티브 지원이 없는 배치 전용 데이터소스나 배치 전용 연산도 자유롭게 적용 가능
여러 싱크에 쓸 때의 트레이드오프 — 단순함 vs 지연시간
foreachBatch 함수 하나 안에서 같은 배치 DataFrame을 여러 싱크에 순서대로 쓰는 방식은 구현이 단순하지만, 쓰기 작업들이 직렬로(순차적으로) 실행돼 마이크로배치 하나당 지연시간이 늘어난다. 반대로 싱크마다 별도의 스트리밍 쿼리를 각각 띄우는 방식은 병렬로 쓸 수 있어 지연시간은 개선되지만, 소스에서 같은 데이터를 여러 번 읽어야 해 구현이 복잡해진다 — 프로덕션에서는 이 단순함과 성능 사이의 트레이드오프를 상황에 맞게 골라야 한다.
핵심 포인트
- foreachBatch 하나로 여러 싱크에 순차적으로 쓰면 구현은 단순하지만 쓰기가 직렬화돼 마이크로배치당 지연시간 증가
- 싱크마다 별도 스트리밍 쿼리를 띄우면 병렬 처리로 지연시간은 개선되지만 소스를 여러 번 읽어야 해 복잡도 증가
idempotent 싱크가 필요한 이유 — at-least-once 재처리
Structured Streaming의 기본적인 스트리밍 보장은 at-least-once다 — 즉 장애 이후 같은 마이크로배치가 다시 처리될 수 있다. foreachBatch로 쓰는 대상 싱크가 이런 재처리에 대비돼 있지 않으면(예: 단순 INSERT만 하는 경우) 같은 데이터가 중복 기록될 수 있다. 그래서 foreachBatch를 쓸 때의 일반 원칙은 대상 싱크 자체가 멱등적(idempotent)이거나, 그게 아니라면 같은 배치 ID로 중복 호출되는 상황을 애플리케이션이 직접 추적해 대응해야 한다는 것이다.
핵심 포인트
- Structured Streaming의 기본 보장은 at-least-once — 장애 후 같은 마이크로배치가 재처리될 수 있음
- foreachBatch 대상 싱크는 멱등적이어야 하거나, 아니면 같은 배치 ID의 중복 호출을 애플리케이션이 직접 추적·대응해야 함