1498 words
7 minutes
[de design pattern]03. Clickhouse에서 지연데이터 handling하기

01. 지연 데이터(Late Data)#

  • 모바일 환경, 네트워크 단절 등으로 인해 이벤트 데이터는 종종 정해진 파티션(시간)이 닫힌 후 지연 도착
  • 지연 데이터를 무시하면 DAU, 매출 등의 핵심 지표가 영구적으로 어긋남.
  • 이를 해결하기 위해 매일 고정된 과거 기간(Lookback Window)의 파티션을 다시 열어 지연 데이터를 병합하는 패턴을 Static Late Data Integrator라고 함.

02. Static Late Data Integrator#

  • 고정된 회고 기간(Static Lookback): 매일 파이프라인이 실행될 때, ‘오늘’ 데이터뿐만 아니라 ‘최근 N일(예: 14일)‘의 파티션을 무조건 다시 스캔
  • Idempotent(멱등성) 보장: 중복 데이터가 들어와도 최종 결과가 동일하도록 기존 데이터를 읽고, 새 데이터를 합친 후 파티션을 덮어씀.

Static Late Data Integrator 사용목적#

  • 지표의 정확성 확보.
  • 이 패턴이 없다면, 과거 파티션에 누락된 데이터를 채워 넣기 위해 매번 수동으로 백필(Backfill) 스크립트를 실행해야 함.

02. 구현: Spark/Airflow 기반 vs ClickHouse#

Case 1: Spark/Airflow#

파이프라인 레이어(컴퓨팅)에서 무거운 병합 연산을 직접 수행하는 전통적인 방식.

  • 동작 방식: 1. Airflow가 최근 14일 치 날짜 목록을 생성.
  1. Spark가 각 날짜별로 기존 파티션 데이터와 지연 도착 데이터를 모두 메모리에 올림.
  2. uniondropDuplicates 수행.
  3. 해당 날짜 파티션을 통째로 덮어쓰기(Overwrite).
# Spark 로직: 기존 데이터 + 지연 데이터 병합 후 덮어쓰기
def integrate_late_data(partition_date):
existing_df = spark.read.load(f"/data/events/date={partition_date}")
late_df = spark.read.load(f"/data/late_events/date={partition_date}")
# 중복 제거 및 병합
merged_df = existing_df.union(late_df).dropDuplicates(['event_id'])
# 파티션 전체 덮어쓰기 (비용이 매우 높음)
merged_df.write.mode("overwrite").save(f"/data/events/date={partition_date}")
  • Trade-off: 데이터 무결성은 파이프라인 종료 즉시 100% 보장되지만, 매일 변경점도 없는 14일 치 데이터를 읽고 쓰는 막대한 I/O 및 연산 비용(Snowball Effect) 발생.

Case 2: ClickHouse 네이티브 (ReplacingMergeTree)#

연산의 책임을 파이프라인에서 데이터베이스 스토리지 엔진으로 위임(Push-down)하는 방식.

  • 동작 방식:
  1. 파이프라인은 ‘과거 데이터를 읽거나 병합’할 필요 없음.
  2. 그냥 지연 데이터가 큐에 들어오는 대로 ClickHouse에 INSERT만 수행.
  3. ClickHouse의 ReplacingMergeTree 엔진이 백그라운드에서 비동기적으로 동일한 event_id를 찾아 중복을 제거
-- 1. 테이블 생성 시 엔진 설정으로 중복 제거 기준(event_id) 명시
CREATE TABLE events (
event_date Date,
event_id String,
user_id String,
action String
) ENGINE = ReplacingMergeTree()
PARTITION BY event_date
ORDER BY (event_id);
-- 2. 파이프라인 로직 (Python/Airflow 불필요, 단순 INSERT)
INSERT INTO events (event_date, event_id, user_id, action)
VALUES ('2026-02-14', 'evt_late_01', 'user_A', 'click'); -- 14일 전 지연 데이터 그냥 삽입
  • Trade-off: 파이프라인 구성이 극도로 단순해지고 쓰기 비용이 거의 ‘0’에 수렴하지만, 백그라운드 병합이 완료되기 전까지는 조회 시 중복 데이터가 노출될 수 있음 (Eventual Consistency).

ClickHouse FINAL 키워드#

ClickHouse 방식(Case 2)의 ‘조회 시점 중복 노출’ 문제를 해결하기 위해 FINAL 키워드를 사용

특징 및 사용 상황#

  • 쿼리 시점에 SELECT ... FROM table FINAL 형태로 사용.
  • 백그라운드 병합을 기다리지 않고, 쿼리 실행 순간 메모리에서 강제로 중복을 제거하여 100% 정확한 결과를 반환.

Trade-off#

  • 장점: 파이프라인 덮어쓰기 없이도 완벽한 데이터 정합성 보장. 재무 데이터나 Ad-hoc 분석에 적합.
  • 단점 (Anti-pattern): 매 쿼리마다 무거운 병합 연산을 수행하므로, 수십 명이 동시에 접속하는 실시간 대시보드(High QPS)에서 사용 시 CPU 자원 고갈 및 시스템 장애 유발.

시스템 디자인 Tradeoff#

  • 쓰기 최적화 (파이프라인 단순화) 우선: 대시보드 조회가 적고 정합성 지연을 일부 허용한다면 ClickHouse ReplacingMergeTree + 단순 INSERT 채택.
  • 읽기 최적화 (빠른 조회) 우선: 높은 QPS의 실시간 대시보드가 필수라면, Spark 방식의 REPLACE PARTITION 로직을 채택하여 저장 시점에 완벽히 중복을 제거해 두어야 함.

Concept

  • Static Late Data Integrator : 고정된 과거 기간(Lookback window)의 파티션을 정기적으로 재처리하여 지연 도착 이벤트를 병합하는 데이터 엔지니어링 패턴.
  • ReplacingMergeTree : ClickHouse의 스토리지 엔진 중 하나로, 정렬 키(ORDER BY)가 동일한 데이터가 INSERT 되면 백그라운드에서 구형 데이터를 삭제하고 최신 데이터만 남기는 비동기 병합 엔진.
  • FINAL 키워드 : ClickHouse에서 쿼리 시점에 강제로 백그라운드 병합 로직을 즉시 수행하여, 완전히 중복 제거된 최신 상태의 데이터를 반환하게 만드는 수식어.

Key_Takeaways

  • 지연 데이터 처리를 위해 기존 Spark/Airflow 패턴은 매일 과거 데이터를 읽고 덮어쓰는 막대한 I/O 비용을 발생시킴.
  • ClickHouse의 ReplacingMergeTree를 활용하면 중복 제거 연산을 데이터베이스 스토리지 엔진으로 위임하여 파이프라인을 단순화할 수 있음.
  • 단, ClickHouse의 비동기 병합 특성상 즉각적인 데이터 정합성이 깨질 수 있음.
  • 이를 해결하기 위해 FINAL 키워드를 사용할 수 있으나, 쿼리 시점의 연산 부하가 극심해지므로 높은 QPS의 대시보드 환경에서는 지양해야 함.
  • 비즈니스 요구사항(데이터 정합성의 민감도 vs 쿼리 응답 속도)에 따라 읽기 최적화와 쓰기 최적화 중 적절한 트레이드오프를 선택해야 함.
[de design pattern]03. Clickhouse에서 지연데이터 handling하기
https://yjinheon.netlify.app/posts/02de/de-design-pattern/03-dead-letter/03-dl-late_date_clickhouse/
Author
Datamind
Published at
2025-02-24
License
CC BY-NC-SA 4.0