윈도(Window)와 시간 의미론 실전
시간 의미론: 어느 시계를 기준으로 할 것인가
스트림 처리에서 "언제"는 하나가 아니다. Flink는 세 가지 시간 개념을 구분한다.
| 시간 개념 | 정의 | 특성 |
|---|---|---|
| 이벤트 시간(Event Time) | 이벤트가 발생한 실제 시각 (레코드에 내장) | 결정론적·재현 가능, 지연·순서 뒤바뀜 처리 가능 |
| 처리 시간(Processing Time) | Flink 오퍼레이터가 해당 레코드를 처리하는 시스템 시각 | 구현 단순, 네트워크 지연 따라 결과 달라짐 |
| 인제스천 시간(Ingestion Time) | 레코드가 Flink 소스에 진입한 시각 | 이벤트 시간이 없을 때 절충안 |
대부분의 프로덕션 파이프라인은 이벤트 시간을 사용한다. 모바일 앱 로그처럼 네트워크 상황에 따라 이벤트가 늦게 도착하거나 순서가 바뀌어도 올바른 집계를 보장해야 하기 때문이다.
워터마크: 이벤트 시간의 진행 신호
이벤트 시간을 사용하면 Flink는 "지금까지 처리된 이벤트 중 가장 늦은 시각"을 알지만, 앞으로 더 늦은 이벤트가 얼마나 더 올지를 알 수 없다. 이 불확실성을 해소하는 메커니즘이 워터마크(Watermark) 다.
워터마크 W(t)는 데이터 스트림과 함께 흐르는 특수 레코드로, "타임스탬프 t 이하의 이벤트는 이제 도착하지 않을 것"을 선언한다. Flink는 이 워터마크를 기준으로 윈도를 마감(trigger)한다.
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는 네 가지 내장 윈도 타입을 제공한다.
00:00~00:04 3분 갭 세션B
00:07~00:12
자동 만료 없음
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
윈도 처리는 네 컴포넌트의 협업이다.
레코드 → 윈도 배정
윈도별 레코드 누적
FIRE / FIRE_AND_PURGE
결정
함수 적용 전후
레코드 제거
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);- Allowed Lateness: 워터마크가 윈도 끝을 지난 후에도
allowedLateness기간 내에 도착한 이벤트는 해당 윈도를 재계산하고 결과를 다시 방출한다. 윈도 상태는 이 기간 동안 유지된다. - 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