1606 words
8 minutes
[DE Design Pattern]03-02. Window Deduplication 패턴

2. Windowed Deduplicator 패턴#

핵심 문제#

분산 시스템에서 exactly-once delivery는 매우 어렵습니다. 대부분 at-least-once 환경에서 동작하므로 중복 레코드가 발생합니다. 비즈니스 로직이 각 레코드를 한 번만 처리해야 한다면 중복 제거가 필요합니다.

패턴의 핵심 아이디어#

“데이터를 제한된 범위(window) 안에서 본다”는 것입니다.

배치는 현재 처리 중인 데이터셋 전체가 암묵적 window입니다. 스트리밍은 무한 데이터이므로 시간 기반 window를 명시적으로 정의하여 그 안에서 중복을 판단합니다.

구현 방식 1: 배치 - dropDuplicates#

가장 단순한 방식입니다. 프레임워크가 제공하는 네이티브 함수를 활용합니다.

# Spark - dropDuplicates로 중복 제거
dataset = spark_session.read.schema('...').format('json').load(input_path)
# 지정 컬럼 조합으로 중복 판단
deduplicated = dataset.dropDuplicates(['type', 'full_name', 'version'])

핵심: 파라미터로 넘긴 컬럼 조합이 deduplication key. 파라미터 생략 시 전체 컬럼 사용.

구현 방식 2: 배치 - SQL WINDOW 함수#

네이티브 함수가 없을 때 SQL로 직접 구현하는 방식입니다.

-- ROW_NUMBER + PARTITION BY 기반 중복 제거
SELECT type, full_name, version FROM (
SELECT type, full_name, version,
ROW_NUMBER() OVER (
PARTITION BY type, full_name, version
ORDER BY type
) AS position
FROM duplicated_devices
) WHERE position = 1

핵심: PARTITION BY로 그룹핑 → ROW_NUMBER()로 순번 부여 → position = 1만 취하여 중복 제거.

구현 방식 3: 스트리밍 - state store 기반#

  • 스트리밍은 배치와 근본적으로 다릅니다. 데이터가 무한히 들어오므로 “이미 본 레코드”를 기억하는 state store가 필요
  1. 새 레코드 도착
  2. state store에서 이미 처리됐는지 확인
  3. 처음 본 레코드면 처리 태스크로 전달
  4. 처리된 키를 state store에 저장.
# Spark Structured Streaming - watermark + dropDuplicates
event_schema = StructType([
StructField("visit_id", StringType()),
StructField("visit_time", TimestampType())
])
deduplicated_visits = (
input_stream
.select(F.from_json("value", event_schema).alias("value_struct"))
.select("value_struct.visit_time", "value_struct.visit_id")
# watermark: 10분 이내 도착한 중복만 감지
.withWatermark("visit_time", "10 minutes")
# visit_id + visit_time 조합으로 중복 제거
.dropDuplicates(["visit_id", "visit_time"])
)

핵심: withWatermark가 두 가지 역할을 수행합니다. (1) 지연 데이터 경계 설정 — watermark보다 오래된 레코드는 무시. (2) state store 정리 — watermark보다 오래된 키를 state에서 자동 제거하여 무한 증가를 방지.

State Store 유형#

스트리밍 중복 제거에서 state store 선택은 성능과 안정성의 trade-off입니다.

Local (메모리만): 가장 빠르지만 장애 시 상태 유실. Local + fault-tolerance (메모리 + 원격 백업): 빠른 접근 + 주기적 원격 저장. 저장 빈도가 높으면 안전하지만 느려지고, 낮으면 빠르지만 일관성 위험. Remote (원격 저장소만): 네이티브 장애 복구 지원하지만 레이턴시와 비용 증가.

주요 Consequences#

Space vs Time trade-off (스트리밍): 짧은 deduplication window → 리소스 적게 쓰지만 일부 중복 놓침. 긴 window → 더 많은 키를 state store에 유지해야 하므로 리소스 부담 증가.

Exactly-once processing ≠ Exactly-once delivery: 중복 제거가 완벽해도 출력 쪽에서 transient error로 재시도하면 downstream에 중복 전달 가능. 이를 해결하려면 별도의 idempotency 패턴이 필요합니다.

Automatic retries의 한계: 런타임 에러로 잡이 재시작되면 이미 처리된 레코드를 다시 처리할 수 있습니다. 이는 deduplication 로직 이전 단계에서 발생하는 문제이므로 중복 제거만으로는 해결 불가합니다.


Concept

  • Windowed Deduplicator : 제한된 범위(window) 내에서 중복 레코드를 식별·제거하는 패턴
  • Deduplication Key : 레코드 유일성을 판단하는 컬럼 조합 (예: visit_id + visit_time)
  • Implicit Global Window : 배치에서 현재 처리 중인 데이터셋 전체를 하나의 window로 간주하는 개념
  • State Store : 스트리밍에서 이미 처리된 키를 기억하는 저장소. Local / Local+fault-tolerant / Remote 세 유형
  • Watermark (중복 제거 맥락) : state store에서 오래된 키를 정리하는 시간 경계. window 크기를 제한하여 state 무한 증가 방지
  • dropDuplicates : Spark의 네이티브 중복 제거 함수. 배치와 스트리밍 모두 지원
  • ROW_NUMBER() + PARTITION BY : SQL에서 그룹별 순번을 매겨 첫 번째 행만 취하는 중복 제거 기법
  • Exactly-once Processing : 각 레코드를 정확히 한 번만 처리하는 것. delivery와는 다른 개념
  • At-least-once Delivery : 레코드가 최소 한 번 이상 전달되는 모드. 중복 가능성 있음

  • 추가적으로 공부하면 좋은 컨셉, 주제들:
    • Idempotent Writer 패턴 : 같은 데이터를 여러 번 써도 결과가 같도록 보장하는 패턴. exactly-once delivery의 핵심
    • Kafka Consumer Group Offset Management : Kafka에서 중복 소비를 방지하는 오프셋 커밋 전략
    • Bloom Filter : 대용량 데이터에서 메모리 효율적으로 “이미 본 적 있는가”를 판단하는 확률적 자료구조
    • Delta Lake MERGE INTO : upsert 기반으로 중복을 방지하며 데이터를 적재하는 기법

퀴즈

Q1. 스트리밍 중복 제거에서 watermark를 “10 minutes”로 설정했을 때, 15분 전에 발생한 동일 visit_id의 중복 레코드가 도착하면 어떻게 처리되나요?

A1. watermark보다 오래된 레코드이므로 late data로 간주되어 파이프라인에 진입하지 못하고 무시됩니다. 동시에 해당 visit_id의 키도 이미 state store에서 제거되었을 가능성이 높습니다. 결과적으로 중복 제거 대상이 아니라 아예 처리 자체가 안 됩니다.

Q2. 배치에서 dropDuplicatesROW_NUMBER() OVER (PARTITION BY ...) 방식의 실질적 차이는?

A2. dropDuplicates는 어떤 행이 남을지 비결정적(프레임워크가 임의 선택)입니다. 반면 ROW_NUMBER() + ORDER BY는 정렬 기준을 명시하므로 “가장 최신” 또는 “가장 먼저 들어온” 등 특정 행을 의도적으로 선택할 수 있습니다. 비즈니스 로직상 어떤 레코드를 남겨야 하는지가 중요하면 WINDOW 함수 방식이 적합합니다.


다음 챕터(Late Data Detector 패턴)로 진행할까요?

[DE Design Pattern]03-02. Window Deduplication 패턴
https://yjinheon.netlify.app/posts/02de/de-design-pattern/03-dead-letter/03-dl-02-window_dedup/
Author
Datamind
Published at
2025-02-24
License
CC BY-NC-SA 4.0