데이터 파이프라인 아키텍처, 무엇부터 봐야 할까?

데이터 파이프라인을 설계할 때 우리는 세 개의 근본적 긴장 관계와 맞닥뜨린다. 첫째, 배치 처리의 비용 효율성과 스트리밍의 신선도 사이의 선택. 둘째, 스키마 진화와 하위 호환성의 관리. 셋째, 재처리(reprocessing) 비용과 데이터 정확성의 균형. 이 세 축 위에서 적재·변환 경로의 형태가 결정된다.

배치와 스트리밍, 어디서 선을 그어야 할까?

배치와 스트리밍은 처리량과 지연시간의 트레이드오프로 자주 설명되지만, 더 정확하게는 재처리 가능성상태 관리 복잡도에서 갈린다.

배치 파이프라인은 일정 시간 윈도우 내 데이터를 수집했다 한 번에 처리한다. 이는 재처리가 명확하다—특정 날짜/기간의 입력을 다시 태우면 동일한 결과를 얻는다. 재계산 비용은 높지만 예측 가능하고, 이상 탐지 후 교정도 단순하다. 또한 복잡한 조인·집계 로직을 SQL이나 Spark SQL 같은 배치 엔진에서 선언적으로 표현하기 쉽다.

스트리밍 파이프라인은 데이터가 도착하는 즉시 처리한다. 이벤트 시간(event time)과 처리 시간(processing time)의 차이로 인해 늦게 도착하는 데이터(late arrival)와 순서 뒤바뀜에 대처해야 한다. 이를 위해 윈도우 상태(windowed state)를 메모리나 상태 저장소에 유지해야 하고, 상태 확대(state bloat)를 관리해야 한다. 재처리는 더 복잡하다—재처리를 위해 이벤트 타임스탬프를 기반으로 다시 읽어내야 하고, 그 사이에 들어온 신규 이벤트와의 순서를 어떻게 할지 결정해야 한다.

실무에서의 패턴: 높은 볼륨, 낮은 지연 요구(초~분 단위)는 스트리밍으로, 일일/시간 단위 집계는 배치로 구성하는 것이 표준적이다. 단 둘 다 필요한 경우가 많다—스트리밍으로 실시간 이벤트를 저장하고, 배치로 이를 집계·정규화하는 람다 아키텍처 형태. 2026년 기준 스트리밍 프레임워크의 상태 관리 기술(RocksDB 기반 상태 백엔드, 증분 체크포인트)은 충분히 성숙했으나, 여전히 배치보다 운영 복잡도가 높다.

스키마 진화를 어떻게 관리할 것인가?

데이터 소스가 변할 때—필드 추가, 타입 변경, 필드 제거—파이프라인을 깨지 않으려면 스키마 진화 전략이 필요하다.

열 추가(column addition): 가장 간단한 경우다. Parquet, ORC, Avro는 모두 스키마 진화를 지원하며, 새 필드는 기본값이나 null로 채워진다. 처리 코드가 존재하지 않는 열을 무시하도록 작성되면 하위 호환성이 유지된다.

타입 변경: 정수→문자열 같은 광범위 변경은 스키마 진화로 표준 지원되지 않는다. 대신 데이터 카탈로그(Hive metastore, Glue, Unity Catalog)의 스키마 추론 전략을 변경하거나, 호환 타입(예: 더 넓은 범위의 수형)으로 이동하는 방식을 쓴다. Apache Arrow의 확장 타입(extension types) 메커니즘으로도 우회 가능하지만 프레임워크 지원이 필요하다.

필드 제거: 이미 저장된 데이터에 영향을 주지 않으므로, 신규 데이터부터는 필드를 쓰지 않으면 된다. 하지만 기존 데이터를 읽을 때 호환성을 유지하려면, 제거할 필드를 일정 기간 유지하거나, 마이그레이션 배치를 별도로 실행해야 한다.

실무 기준: 메시지 포맷에 스키마 레지스트리(Confluent Schema Registry, AWS Glue Schema Registry)를 써서 버전을 명시적으로 관리하고, 각 버전에 대해 upstream과 downstream의 호환성 검증을 자동화하는 것이 표준이다. Avro의 호환성 규칙(backward, forward, full)을 선언해 두면, 스키마 변경 시 파이프라인이 깨질 가능성을 사전에 차단할 수 있다.

멱등성과 "정확히 한 번" 처리를 어디까지 보장할까?

"정확히 한 번(exactly-once)" 처리는 시스템 장애 후 복구해도 중복이나 손실 없이 데이터가 정확히 처리된다는 뜻이다. 이는 세 가지 계층에서 구현된다.

1. 소스 단계: 입력 데이터 중복을 피하려면, 소스가 멱등한 읽기를 지원해야 한다. 예를 들어 변위(offset, 파티션 번호)로 위치를 정확히 지정할 수 있어야 한다. Kafka는 이를 기본으로 제공하고, RDBMS의 증분 로드는 타임스탐프나 증분 키로 구현한다. S3의 객체는 ETag로 동일성을 검증할 수 있다.

2. 처리 단계: 변환 로직 자체가 멱등해야 한다. 동일 입력에 대해 항상 동일 결과를 낸다. SQL, Spark 코드는 대부분 멱등하지만, 외부 API 호출(예: HTTP 요청으로 데이터 보강), 난수 생성, 현재 시간 조회 등은 멱등하지 않다. 이런 작업을 피하거나, 결정론적 대안(정규화된 키에 기반한 조회 테이블)으로 바꿔야 한다.

3. 싱크 단계: 출력을 저장할 때, 중복을 감지하고 건너뛰거나, 멱등한 쓰기를 보장해야 한다. 파일 시스템(HDFS, S3)에 쓸 때는 파티션 기반 덮어쓰기(파티션 존재 시 삭제 후 재작성)로 멱등성을 보장한다. 데이터베이스에 쓸 때는 UPSERT(unique key 기반 업데이트-또는-삽입)를 쓴다. 트랜잭션을 지원하는 레이크하우스 포맷(Delta Lake, Apache Iceberg, Apache Hudi)은 원자적 쓰기를 통해 이를 단순화한다.

실무 수준: "정확히 한 번"을 모든 단계에서 완벽하게 구현하려면 비용이 크다(트랜잭션 오버헤드, 상태 저장소 유지). 대신 "최소 한 번(at-least-once)" 처리로 기본을 설정하고, 싱크 단계의 멱등성만 엄격히 보장하는 패턴이 일반적이다. 이렇게 하면 재시작 후 약간의 중복이 발생해도, 싱크에서 자동으로 제거된다.

백필과 재처리 비용, 언제 직면하는가?

백필(backfill)은 과거 기간의 데이터를 파이프라인에서 재처리하는 작업이다. 새로운 데이터 소스를 추가하거나, 처리 로직을 수정했을 때 발생한다.

비용 요소:

  • 저장소 I/O: 과거 데이터를 모두 읽어야 함. 배치 파이프라인은 날짜 범위로 쿼리하므로 선택적 읽기가 가능하지만, 스트리밍 파이프라인은 이벤트 로그를 처음부터 끝까지 재생해야 할 수 있다.
  • 컴퓨팅: 변환 작업을 다시 실행. 배치는 병렬 처리(Spark, Presto)로 비교적 빠르지만, 큰 백필은 여전히 수 시간~일 소요.
  • 상태 재구성: 스트리밍 파이프라인에서 윈도우 상태(예: 사용자별 세션)를 재구성해야 하면, 모든 이벤트를 시간순으로 메모리에 로드해야 할 수 있다.

재처리를 피하거나 줄이는 전략:

  1. 증분 처리: 새 데이터만 처리하도록 파이프라인을 구성. 마지막 성공 타임스탐프를 기록하고 그 이후만 읽음.
  2. 데이터 레이어 분리: 원본 데이터(raw layer)는 건드리지 않고, 변환 로직만 수정. 변환된 계층만 재계산. (medallion architecture)
  3. 재처리 가능한 포맷: 원본 이벤트를 Avro/Parquet로 보관해 두면, 언제든 재처리할 수 있다.

수치 기준: 1년치 일별 배치(365일)를 재처리할 때, Spark 클러스터(8 executor, 4 GB 메모리 각)에서 중간 규모 변환(조인 2개, 집계 3개)은 보통 24시간 소요된다. 1TB 규모 데이터라면 스토리지 I/O만 3060초 정도. 실제 병목은 보통 CPU 집약적 변환이다.

레이크하우스 포맷, 어느 것을 쓸까?

레이크하우스(Lakehouse)는 오브젝트 스토리지(S3, GCS, ADLS) 위에 트랜잭션 및 스키마 관리 기능을 얹은 구조다. Apache Parquet 같은 열 지향 포맷만으로는 부족한 원자성, 스키마 진화, 시간 여행(time travel) 기능을 제공한다.

주요 포맷:

  • Delta Lake: Parquet 파일 위에 트랜잭션 로그(JSON 기반)를 둬서 ACID 보장. 스키마 진화, 시간 여행 지원. Spark 생태계와 긴밀하고, Databricks가 주도. 읽기 성능은 표준적.
  • Apache Iceberg: 더 명시적인 메타데이터 구조(manifest files). 스냅숏(snapshot) 기반 버전 관리. 쓰기 성능과 동시성 제어가 상대적으로 강함. Netflix가 주도.
  • Apache Hudi: 증분 처리(incremental processing)에 초점. CoW(Copy-on-Write)와 MoR(Merge-on-Read) 옵션으로 비용-성능 조절 가능. 지연시간 낮은 스트리밍 쓰기가 강점. 메타데이터 오버헤드가 높을 수 있음.

선택 기준:

  • Spark 기반 배치 + 간단한 스키마 진화: Delta Lake. 구현이 가장 간단하고 Spark 통합이 강함.
  • 다중 엔진(Spark, Presto, Flink) 지원 필요: Iceberg. 엔진 중립적인 메타데이터 포맷.
  • 실시간 스트리밍 쓰기 + 증분 읽기: Hudi. 변경 로그 추적 기능(change data capture) 내장.

메타데이터 오버헤드는 모두 존재한다—매 쓰기마다 메타데이터 파일을 생성하므로, 초당 수백 건 이상 이벤트를 쓸 때는 작은 파일 문제(small file problem)가 발생할 수 있다. 보통은 배치 병합(compaction)으로 관리한다.

어떤 상황에서 어떤 구조를 택할까?

높은 기록 빈도, 낮은 지연 요구 (실시간 분석)

  • 구조: 스트리밍 → 레이크하우스 + 배치 병합
  • 구체: Kafka/Kinesis → Flink/Spark Streaming → Delta/Iceberg (매시간 병합) → 쿼리 엔진
  • 이유: 이벤트를 즉시 저장하되, 메타데이터 오버헤드를 배치 병합으로 흡수

높은 볼륨, 일일 리포팅

  • 구조: 배치 적재 → 레이크하우스 → SQL 쿼리
  • 구체: CDC 또는 daily dump → Spark SQL/dbt → Iceberg/Delta → BI 도구
  • 이유: 예측 가능한 처리, 재처리 비용 관리 용이

마이크로배치 + 이상 탐지

  • 구조: 스트리밍(마이크로배치) → 실시간 집계 → 임계값 기반 경고
  • 구체: Kafka → Spark Structured Streaming (10초 마이크로배치) → Redis/메모리 상태 → 경고 시스템
  • 이유: 높은 처리량과 낮은 지연 사이 균형. 상태 관리 복잡도 최소화.

간과하기 쉬운 것: 데이터 신선도와 일관성의 차이

"신선도(freshness)"와 "일관성(consistency)"은 자주 혼동된다. 신선도는 데이터가 소스 변화를 얼마나 빨리 반영하는가이고, 일관성은 서로 다른 테이블/뷰가 같은 시점의 세계상을 보는가이다.

예를 들어, 스트리밍으로 사용자 이벤트는 초 단위로 업데이트되지만(높은 신선도), 사용자 프로필은 매일 한 번 배치로 업데이트된다면, 이 둘은 일관성이 떨어진다. 사용자 세션 분석에서 같은 시점에 두 데이터를 조인하면, 이벤트는 최신이지만 프로필은 어제 데이터일 수 있다.

배치 파이프라인은 신선도는 낮지만(1시간, 1일 지연), 모든 입력이 동일 배치 윈도우 내에서 처리되므로 강한 일관성을 보장한다. 스트리밍은 높은 신선도를 주지만, 느린 소스(배치로만 들어오는 차원 데이터)와 섞으면 일관성이 약화된다.

실무 대책: 신선도가 다른 데이터를 섞을 때는 시간 기준을 명시적으로 정하자. 예를 들어 "이 보고서는 매일 UTC 자정 기준 스냅숏"이라고 정의하고, 모든 테이블을 그 시점으로 정렬해 읽는 방식. 이를 위해 모든 레이크하우스 테이블에 처리 타임스탐프를 기록해 두면 편하다.

핵심 정리

  • 배치와 스트리밍의 선택은 지연시간이 아니라 재처리 가능성과 상태 관리 비용으로 판단한다. 배치는 재처리가 명확하고 로직 표현이 선언적이지만, 스트리밍은 신선도를 얻는 대신 상태 관리 복잡도를 감수한다.

  • 스키마 진화는 메시지 포맷 레벨(Avro)에서 호환성 규칙을 선언하고, 카탈로그 레벨(Schema Registry)에서 버전 검증을 자동화하는 이중 방어가 표준이다.

  • 멱등성은 "모든 단계에서"보다 "싱크 단계에서만"엄격히 보장하는 것이 실무 기준이다. 레이크하우스 포맷(Delta, Iceberg, Hudi)의 UPSERT 지원이 이를 단순화한다.

  • 백필과 재처리 비용은 데이터 레이어 분리(medallion architecture)와 증분 처리로 줄인다. 1년치 배치 재처리는 중규모 변환 기준 2~4시간 정도.

  • 레이크하우스 포맷 선택은 주요 처리 엔진과 쓰기 패턴으로 결정된다: Spark 배치 → Delta, 다중 엔진 → Iceberg, 스트리밍 → Hudi.

  • 신선도(소스 반영 속도)와 일관성(테이블 간 동기화)을 구분하고, 혼합된 신선도의 데이터는 명시적 타임스탐프로 정렬해 읽는다.

  • 스트리밍과 배치를 섞는 람다 아키텍처는 복잡도가 높으므로, 가능하면 배치로 통일하거나 스트리밍 + 배치 병합(컴팩션)으로 단순화한다.

자주 묻는 질문

배치 처리를 선택하면 지연시간이 몇 시간이나 됩니까?
배치 윈도우 크기(1시간, 1일, 1주)에 따라 다르다. 일일 배치라면 최대 24시간 지연 가능. 지연시간이 중요하면 윈도우를 줄이거나(시간 단위 배치), 처리 시간을 최적화해야 한다. 마이크로배치(10초~1분)도 배치 방식이지만, 스트리밍보다 상태 관리가 단순하다.

스키마 변경 후 기존 데이터를 다시 읽으면 어떻게 됩니까?
파일 포맷(Parquet, Avro)이 스키마 진화를 지원하면, 새 스키마로 읽을 때 없는 필드는 기본값(null 등)으로 채워진다. 하지만 기존 데이터 자체가 변경되지는 않는다. 필드 의미가 바뀌었다면(예: 정수 필드가 시간 단위에서 밀리초 단위로 변함), 데이터는 변경되지 않으므로 재처리가 필요하다.

"정확히 한 번" 처리를 위해 트랜잭션이 반드시 필요한가?
소스, 처리, 싱크 세 단계가 모두 멱등해야 한다. 트랜잭션은 이를 쉽게 구현하는 수단일 뿐이다. 예를 들어 싱크에서 UPSERT를 쓰면, 같은 입력을 여러 번 실행해도 결과가 같다. 트랜잭션 없이도 멱등성을 얻을 수 있으므로, 비용과 요구사항을 맞춰 선택한다.

재처리 비용은 몇 시간 단위로 계획해야 합니까?
데이터 크기, 변환 복잡도, 클러스터 크기에 따라 크게 달라진다. 1년치(365일) 일별 배치, 100GB 규모, Spark 소형 클러스터라면 2~6시간. 매우 복잡한 조인·머신러닝 변환이면 배로 증가할 수 있다. 실무에서는 한 달치 백필을 먼저 실행해 수행 시간을 예측한다.

Delta Lake와 Iceberg 중 어느 것을 쓸까요?
Spark 배치 주도 환경이면 Delta Lake(구현 단순, Spark 최적화). 여러 엔진(Presto, Flink, Spark)을 섞거나, 복잡한 시간 여행·스냅숏 관리가 필요하면 Iceberg. 선택 후 변경은 상당한 마이그레이션 비용이므로 초기 평가가 중요하다.

신생 데이터 소스를 추가하려면 얼마나 많은 과거 데이터를 백필해야 합니까?
비즈니스 분석 기준을 결정해야 한다. 최소 1년은 권장된다(계절성, 전년대비 비교). 비용이 관건이면 최근 90일부터 시작해 점차 확대하는 방식도 있다. 다만 백필 부분과 신규 데이터 부분의 일관성 기준을 명확히 해야 한다.

마이크로배치와 진정한 스트리밍의 성능 차이는 얼마나 됩니까?
지연시간: 마이크로배치는 배치 간격(보통 1초~1분)만큼 고정. 진정한 스트리밍은 이벤트 도착 직후 처리(밀리초 단위)하므로 10배 이상 빠를 수 있다. 단 처리량과 안정성에서 마이크로배치는 배치 최적화를 그대로 사용해 효율적이다. 선택은 신선도 요구사항에 따라 결정.