Notice
Recent Posts
Recent Comments
Link
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

Log4KJS

Streaming Systems - What / Where / When / How 본문

카테고리 없음

Streaming Systems - What / Where / When / How

IceMelon404 2026. 2. 22. 23:43

이번 글에서는 Streaming Systems의 핵심 이론을 Flink DataStream API 코드로 어떻게 구현하는지, 이론과 실무를 연결하는 다리를 놓아보겠습니다.

1. What: 무엇을 계산하는가? (Transformation)

스트리밍 파이프라인의 가장 기초는 들어오는 데이터에 어떤 연산을 수행할지 정의하는 것입니다. 이는 무한한 데이터 흐름 속에서 개별 레코드를 변환하거나 필터링하는 요소별(Element-wise) 변환과, 여러 레코드를 묶어 의미 있는 값을 도출하는 집계(Aggregation)로 나뉩니다.

 

스트림 데이터 변환 파이프라인

 

Flink 구현 예제 (Java):

Flink의 DataStream API는 map, filter와 같은 익숙한 함수형 연산자를 제공하여 이러한 변환을 직관적으로 구현할 수 있게 합니다.

DataStream<SensorReading> sensorData = env.addSource(...);

DataStream<SensorReading> highTempReadings = sensorData
    // [Map] 섭씨 온도를 화씨로 변환
    .map(r -> new SensorReading(r.id, r.timestamp, r.temperature * 1.8 + 32))
    // [Filter] 화씨 100도 이상의 데이터만 필터링
    .filter(r -> r.temperature > 100.0);

 


집계(aggregation) 에 대해서는 후술할 Window 연산에서 다루겠습니다 


2. Where: 어디서 계산되는가? (Window)

무한히 흐르는 스트림 데이터를 집계하려면, 데이터를 '유한한 조각(Chunk)'으로 잘라야 합니다. 이때 자르는 기준이 되는 것이 윈도우(Window)입니다. 중요한 점은 데이터를 처리하는 현재 서버 시간이 아니라, 이벤트가 실제로 발생한 시간(Event Time)을 기준으로 데이터를 그룹화한다는 것입니다.

 

텀블링 윈도우

슬라이딩 윈도우 

세션 윈도우

 

Flink 구현 예제 (Java):

Flink에서는 .keyBy(...)를 통해 데이터를 특정 키(예: 사용자 ID)별로 파티셔닝한 후 윈도우를 적용하는 Keyed Window가 일반적입니다. 이는 데이터가 여러 서버로 분산되어 병렬 처리가 가능하게 합니다. 반면, .windowAll(...)을 사용하는 Non-Keyed Window는 모든 데이터가 단일 노드로 몰려 심각한 병목을 초래할 수 있습니다.

stream
    .keyBy(data -> data.getUserId())
    // 10분 크기의 슬라이딩 윈도우를 5분 간격으로 이동 (Event Time 기준)
    .window(SlidingEventTimeWindows.of(Time.minutes(10), Time.minutes(5)))
    .reduce(new MyReducer());

 

3. When:  언제 결과를 내보내는가? (Watermark & Trigger)

이벤트 시간 기준으로 윈도우를 정의했더라도, 실제 결과는 물리적인 처리 시간(Processing Time) 에 출력되어야 합니다. 스트리밍 시스템은 필연적으로 발생하는 지연 시간과 결과의 완전성 사이에서 트레이드오프를 경험합니다. 이 문제를 해결하는 핵심 도구가 워터마크(Watermark)트리거(Trigger)입니다.

3.1. Watermark: 이벤트 시간의 진행 지표

네트워크 지연 등으로 인해 이벤트가 생성된 순서와 다르게(Out-of-order) 도착할 수 있습니다. 워터마크는 "이 시간 이전의 데이터는 다 들어왔다"라고 시스템에 알려주는 지표로, 윈도우의 종료 시점을 판단하는 기준이 됩니다.

 

이벤트 시간 vs 처리 시간의 괴리(Skew)와 워터마크

 

Flink 구현 예제 (Java):

Flink에서는 WatermarkStrategy를 통해 데이터 소스에 워터마크 생성 로직을 주입합니다. forBoundedOutOfOrderness를 사용하면 데이터의 최대 지연 시간을 고려하여 워터마크를 생성할 수 있습니다.

WatermarkStrategy<PostEvent> watermarkStrategy = WatermarkStrategy
      // 데이터가 최대 5초 늦게 도착할 수 있음을 허용
      .<PostEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
      .withTimestampAssigner((event, timestamp) -> event.getTimestamp());

 

3.2. Trigger: 결과 방출 시점의 결정

트리거는 워터마크가 도달하기 전(조기), 도달했을 때(정시), 도달한 후(지연) 특정 조건을 만족하면 윈도우의 결과를 계산하여 내보내도록 신호를 보냅니다. 특히 워터마크 이후에 도착하는 **지연 이벤트(Late Event)**를 어떻게 처리할지가 중요합니다.

 
 

 

Flink 구현 예제 (Java):

Flink는 다양한 빌트인 트리거를 제공하며, allowedLateness와 sideOutputLateData를 통해 지연 데이터를 유연하게 처리할 수 있습니다.

OutputTag<PostEvent> lateDataTag = new OutputTag<PostEvent>("late-data"){};

SingleOutputStreamOperator<Result> mainStream = stream
    .keyBy(data -> data.getUserId())
    .window(TumblingEventTimeWindows.of(Time.hours(1)))
    // 15분마다 조기 결과를 방출하는 트리거 설정
    .trigger(ContinuousEventTimeTrigger.of(Time.minutes(15)))
    // 워터마크 이후 1시간까지 지연 데이터 허용 (이 기간 동안 윈도우 상태 유지)
    .allowedLateness(Time.hours(1))
    // 1시간마저 초과한 데이터는 별도 채널로 빼냄
    .sideOutputLateData(lateDataTag)
    .process(new MyWindowFunction());

// 사이드 출력으로 빠진 데이터 별도 처리
DataStream<PostEvent> lateStream = mainStream.getSideOutput(lateDataTag);

 

4. How: 결과는 어떻게 수정/축적되는가? (Accumulation)

하나의 윈도우에서 여러 번 트리거가 발생할 때(예: 조기 방출 후 정시 방출), 동일한 윈도우의 결과를 어떻게 정제(Refinement)할 것인지 결정하는 전략이 필요합니다. 특히 allowedLateness 기간 동안 지연 데이터가 도착하면 윈도우가 다시 트리거되는데, 이때 이전 결과와 어떻게 결합될지가 중요합니다.

 

4.1. 누적 모드 (Accumulating Mode)

  • 개념: 트리거가 발생할 때마다 윈도우 상태를 유지한 채, 지금까지 집계된 전체 값을 내보냅니다. 지연 데이터가 도착하면 기존 값에 더해져서 다시 방출됩니다.
  • Flink 구현: Flink DataStream API의 기본 동작입니다.
stream
    .keyBy(data -> data.getUserId())
    .window(TumblingEventTimeWindows.of(Time.hours(1)))
    // 15분마다 조기 방출. 결과는 누적됨 (예: 10 -> 25 -> 40...)
    .trigger(ContinuousEventTimeTrigger.of(Time.minutes(15)))
    .sum("amount");

 

4.2. 무시 모드 (Discarding Mode)

  • 개념: 트리거가 발생할 때마다 윈도우 상태를 초기화하고, 지난번 방출 이후 새로 들어온 델타(Delta) 값만 내보냅니다.
  • Flink 구현: 기존 트리거를 PurgingTrigger로 감싸서 사용합니다.
stream
    .keyBy(data -> data.getUserId())
    .window(TumblingEventTimeWindows.of(Time.hours(1)))
    // 15분마다 조기 방출 후 상태 초기화. 결과는 독립적임 (예: 10 -> 15 -> 15...)
    .trigger(PurgingTrigger.of(ContinuousEventTimeTrigger.of(Time.minutes(15))))
    .sum("amount");

 

4.3. 누적 및 철회 모드 (Accumulating & Retracting Mode)

  • 개념: 새로운 누적 값을 보낼 때, 이전에 보냈던 값을 취소(Retract)하는 메시지를 함께 보냅니다. 이는 하위 시스템이 멱등성을 보장하지 않아도 정확한 최종 결과를 얻을 수 있게 합니다.
  • Flink 구현: DataStream API로 직접 구현하기는 매우 복잡하며, 주로 Flink SQL (Table API)에서 내부적으로 사용하는 강력한 기능입니다.