LLM WikiAccess-protected knowledge portal
← 스터디 홈
3편 · 약 20분

윈도(Window)와 시간 의미론 실전

시간 의미론: 어느 시계를 기준으로 할 것인가

스트림 처리에서 "언제"는 하나가 아니다. Flink는 세 가지 시간 개념을 구분한다.

시간 개념정의특성
이벤트 시간(Event Time)이벤트가 발생한 실제 시각 (레코드에 내장)결정론적·재현 가능, 지연·순서 뒤바뀜 처리 가능
처리 시간(Processing Time)Flink 오퍼레이터가 해당 레코드를 처리하는 시스템 시각구현 단순, 네트워크 지연 따라 결과 달라짐
인제스천 시간(Ingestion Time)레코드가 Flink 소스에 진입한 시각이벤트 시간이 없을 때 절충안

대부분의 프로덕션 파이프라인은 이벤트 시간을 사용한다. 모바일 앱 로그처럼 네트워크 상황에 따라 이벤트가 늦게 도착하거나 순서가 바뀌어도 올바른 집계를 보장해야 하기 때문이다.

워터마크: 이벤트 시간의 진행 신호

이벤트 시간을 사용하면 Flink는 "지금까지 처리된 이벤트 중 가장 늦은 시각"을 알지만, 앞으로 더 늦은 이벤트가 얼마나 더 올지를 알 수 없다. 이 불확실성을 해소하는 메커니즘이 워터마크(Watermark) 다.

워터마크 W(t)는 데이터 스트림과 함께 흐르는 특수 레코드로, "타임스탬프 t 이하의 이벤트는 이제 도착하지 않을 것"을 선언한다. Flink는 이 워터마크를 기준으로 윈도를 마감(trigger)한다.

이벤트 스트림 (이벤트 시간 기준, 5분 Tumbling Window)
t=00:00 t=00:03 t=00:01 WM(00:02) t=00:04 t=00:07 WM(00:05) → 윈도 [00:00,00:05) 트리거
WatermarkStrategy BoundedOutOfOrderness
maxOutOfOrderness = 2분
W = max(이벤트 시각) − 2분
즉 t=00:07 수신 시
W = 00:05 생성
워터마크와 윈도 트리거 흐름

WatermarkStrategy 설정

Flink 1.11부터 WatermarkStrategy 인터페이스로 워터마크 생성 전략을 명시한다.

DataStream<Event> stream = env
    .fromSource(
        kafkaSource,
        WatermarkStrategy
            .<Event>forBoundedOutOfOrderness(Duration.ofMinutes(2))
            .withTimestampAssigner(
                (event, recordTimestamp) -> event.getEventTime()),
        "kafka-source");

forBoundedOutOfOrderness(maxDelay): 이벤트가 최대 maxDelay만큼 늦게 도착할 수 있다고 선언한다. Flink는 관찰된 최대 이벤트 시각에서 maxDelay를 뺀 값을 워터마크로 주기적으로 생성한다.

forMonotonousTimestamps(): 이벤트 시각이 단조 증가한다고 가정한다. 최대 이벤트 시각 그대로 워터마크를 생성한다. 지연이 전혀 없는 이상적인 상황에만 적합하다.

윈도의 4가지 형태

윈도는 무한 스트림을 집계할 수 있는 유한 구간으로 나누는 단위다. Flink는 네 가지 내장 윈도 타입을 제공한다.

Tumbling Window (크기=5분)
0~5분 5~10분 10~15분
겹침 없음, 고정 크기
Sliding Window (크기=10분, 슬라이드=5분)
0~10분
5~15분
윈도 겹침, 하나의 이벤트가 여러 윈도에 속함
Session Window (갭=3분)
세션A
00:00~00:04
3분 갭 세션B
00:07~00:12
활동 기반, 크기 고정 없음
Global Window 모든 이벤트를 하나의 윈도로 직접 Trigger 구현 필요
자동 만료 없음
Flink 윈도 타입 비교

Tumbling Window

stream
    .keyBy(Event::getUserId)
    .window(TumblingEventTimeWindows.of(Duration.ofMinutes(5)))
    .reduce((a, b) -> new Event(a.getUserId(), a.getCount() + b.getCount()));

고정 크기, 겹침 없음. "매 5분마다 사용자별 이벤트 수 집계" 같은 요구에 적합하다.

Sliding Window

stream
    .keyBy(Event::getUserId)
    .window(SlidingEventTimeWindows.of(
        Duration.ofMinutes(10),   // 윈도 크기
        Duration.ofMinutes(5)))   // 슬라이드
    .aggregate(new CountAggregator());

이벤트 하나가 크기/슬라이드 개수의 윈도에 속한다. 이동평균, 롤링 합계에 적합하지만 메모리를 더 사용한다.

Session Window

stream
    .keyBy(Event::getUserId)
    .window(EventTimeSessionWindows.withGap(Duration.ofMinutes(3)))
    .process(new SessionSummarizer());

사용자 행동 분석처럼 "활동 후 일정 시간 비활성이면 세션 종료"를 표현한다. 윈도 경계가 데이터에 따라 동적으로 결정된다.

윈도 라이프사이클: Assigner → Buffer → Trigger → Evictor → Function

윈도 처리는 네 컴포넌트의 협업이다.

WindowAssigner
레코드 → 윈도 배정
상태 버퍼
윈도별 레코드 누적
Trigger
FIRE / FIRE_AND_PURGE
결정
Evictor (선택)
함수 적용 전후
레코드 제거
Window Function
ReduceFunction
AggregateFunction
ProcessWindowFunction
출력 스트림
윈도 처리 파이프라인
  • WindowAssigner: 들어온 레코드를 어느 윈도(들)에 넣을지 결정.
  • Trigger: 워터마크가 윈도 끝을 지나면 기본적으로 FIRE. 커스텀 트리거로 중간 결과를 먼저 방출(FIRE)하고 나중에 최종 결과도 방출하는 조기 트리거(early trigger) 패턴도 가능하다.
  • Evictor: 트리거 직후 집계 함수에 넘기기 전에 일부 레코드를 제거. 드물게 사용된다.
  • Window Function: ReduceFunction / AggregateFunction은 증분(incremental) 집계로 메모리 효율이 좋다. ProcessWindowFunction은 윈도 내 전체 레코드를 한꺼번에 받아 처리하지만 모든 레코드를 버퍼링해야 한다.

늦은 데이터 처리: Allowed Lateness와 Side Output

워터마크가 윈도를 마감한 후에도 늦은 이벤트가 도착할 수 있다. Flink는 이를 두 단계로 처리한다.

OutputTag<Event> lateTag = new OutputTag<Event>("late-events"){};

SingleOutputStreamOperator<Result> result = stream
    .keyBy(Event::getUserId)
    .window(TumblingEventTimeWindows.of(Duration.ofMinutes(5)))
    .allowedLateness(Duration.ofMinutes(1))   // 최대 1분 늦은 이벤트 재집계
    .sideOutputLateData(lateTag)              // 1분 넘으면 사이드 출력으로 라우팅
    .reduce(new SumReducer());

// 1분 초과 늦은 이벤트 별도 처리
DataStream<Event> lateStream = result.getSideOutput(lateTag);
  1. Allowed Lateness: 워터마크가 윈도 끝을 지난 후에도 allowedLateness 기간 내에 도착한 이벤트는 해당 윈도를 재계산하고 결과를 다시 방출한다. 윈도 상태는 이 기간 동안 유지된다.
  2. Side Output: allowedLateness도 초과한 이벤트는 주 스트림 대신 사이드 출력으로 분리한다. DLQ(Dead Letter Queue)처럼 별도 처리하거나 모니터링에 활용한다.

이벤트 시간 vs 처리 시간 — 언제 무엇을 선택하는가

상황권장
모바일·IoT 기기 로그, 네트워크 지연 있음이벤트 시간
결과 재현성·정확성이 중요이벤트 시간
실시간 모니터링, 약간의 부정확 허용처리 시간
이벤트에 신뢰 가능한 타임스탬프 없음처리 시간 또는 인제스천 시간

이벤트 시간을 쓰면 Kafka 파티션 재처리 시 동일한 결과가 나온다. 처리 시간은 재처리해도 결과가 달라진다.

워터마크 생성 전략 실전 팁

  • maxDelay를 너무 크게 잡으면 윈도가 늦게 마감돼 결과 지연이 커진다. 너무 작으면 늦은 이벤트가 버려진다. 데이터 소스별 지연 분포를 먼저 분석한다.
  • 소스 병렬도가 여러 개면 각 파티션에서 워터마크가 독립 생성된다. Flink는 모든 병렬 소스 인스턴스의 워터마크 중 최솟값을 다운스트림으로 전달한다(워터마크 정렬).
  • Kafka 토픽의 특정 파티션에 이벤트가 없어 워터마크가 진행되지 않는 경우, WatermarkStrategy.withIdleness(Duration.ofSeconds(30))로 유휴 파티션을 감지하고 해당 파티션을 워터마크 계산에서 제외한다.
WatermarkStrategy
    .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(10))
    .withTimestampAssigner((e, ts) -> e.getEventTime())
    .withIdleness(Duration.ofSeconds(30));  // 30초 이상 이벤트 없으면 idle 처리

References

  • https://nightlies.apache.org/flink/flink-docs-stable/docs/concepts/time/
  • https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/operators/windows/
  • https://nightlies.apache.org/flink/flink-docs-stable/docs/learn-flink/streaming_analytics/
  • https://developer.confluent.io/courses/flink-java/windowing-and-watermarks-in-flink/
  • https://datorios.com/blog/apache-flink-watermarks-event-time-processing-windowing/
  • https://mohankumarsagadevan.medium.com/exploring-windowing-in-apache-flink-tumbling-sliding-session-and-global-windows-f9b4d51d4076
  • https://apxml.com/courses/real-time-data-pipelines-kafka-flink/chapter-4-advanced-windowing-time-attributes/handling-late-data-side-outputs