시간 기반 스트림 처리
시간 기반 스트림 처리 (Timely Stream Processing)
무한히 흐르는 이벤트를 다루다 보면 '언제' 일어난 일인지가 중요해지는 순간이 와요. 시계열 분석을 하거나 일정 시간 단위로 집계(보통 윈도우라고 부르죠)를 하거나, 이벤트가 발생한 시각이 결과에 영향을 주는 처리를 할 때가 바로 그런 경우예요. 시간이 계산의 한 축으로 들어간 처리를 **시간 기반 스트림 처리(timely stream processing)**라고 불러요. 상태 기반 스트림 처리의 확장으로 이해하면 좋아요.
이벤트 시간과 처리 시간: 두 가지 시간 개념
스트리밍 프로그램에서 시간을 말할 때는 두 가지 서로 다른 기준을 가리킬 수 있어요. 하나는 처리 시간(processing time), 다른 하나는 **이벤트 시간(event time)**이에요.
처리 시간은 그 연산을 실행하고 있는 머신의 시스템 시계를 그대로 사용하는 개념이에요. 처리 시간으로 도는 프로그램에서 시간 기반 연산(예: 시간 윈도우)은 연산자를 실행하는 머신의 시스템 클록을 따르죠. 예를 들어 1시간 단위 처리 시간 윈도우는 시스템 시계가 정각을 가리킨 사이에 특정 연산자에 도착한 레코드를 모두 담아요. 애플리케이션이 오전 9시 15분에 시작했다면, 첫 번째 시간 윈도우는 9:1510:00 사이에 처리된 이벤트를, 다음 윈도우는 10:0011:00 사이 이벤트를 담는 식이에요.
처리 시간은 가장 단순한 시간 개념이라 스트림과 머신 사이의 조정이 필요 없어요. 그래서 성능이 가장 좋고 지연이 가장 낮죠. 다만 분산·비동기 환경에서는 결정적이지 않다는 단점이 있어요. 메시지 큐에서 레코드가 도착하는 속도, 시스템 내부에서 레코드가 연산자 사이를 흐르는 속도, 예정되거나 그렇지 않은 장애까지 — 이런 것들에 좌우되기 때문이에요.
반면 이벤트 시간은 각 이벤트가 원래 발생한 기기에서 찍힌 시각을 가리켜요. 보통 그 타임스탬프는 레코드가 Flink에 들어오기 전에 레코드 안에 이미 담겨 있고, 우리가 각 레코드에서 그 이벤트 타임스탬프를 꺼내 쓰는 구조예요. 이벤트 시간에서는 시간의 진행이 벽시계가 아니라 데이터 자체에 의존해요. 그래서 이벤트 시간 프로그램은 이벤트 시간 워터마크를 어떻게 생성할지 반드시 지정해야 해요. 워터마크가 이벤트 시간의 진행 상황을 알려주는 장치니까요(자세한 내용은 아래에서 다뤄요).
완벽한 상황이라면 이벤트 시간 처리는 이벤트가 언제 도착하든, 어떤 순서로 오든 완전히 일관되고 결정적인 결과를 만들어 주는데요. 단, 이벤트가 타임스탬프 순서대로 온다고 보장되지 않는다면, 순서가 뒤섞인(아웃 오브 오더) 이벤트를 기다리는 동안 어느 정도의 지연이 생겨요. 기다릴 수 있는 시간은 유한할 수밖에 없으니, 이 지점에서 이벤트 시간 애플리케이션이 얼마나 결정적일 수 있는지가 제한되는 거예요.
이벤트 시간과 워터마크
이벤트 시간을 지원하는 스트림 프로세서는 이벤트 시간이 얼마나 진행됐는지를 측정할 방법이 필요해요. 예를 들어 1시간 단위 윈도우를 만드는 연산자는, 이벤트 시간이 한 시간의 끝을 지나갔다는 알림을 받아야 그 진행 중이던 윈도우를 닫을 수 있거든요. 이벤트 시간은 벽시계(처리 시간)와 독립적으로 진행될 수 있어요. 어떤 프로그램에서는 연산자의 현재 이벤트 시간이 처리 시간보다 약간 뒤처져 가면서도 같은 속도로 함께 가기도 하고, 어떤 프로그램은 Kafka 토픽에 이미 쌓여 있는 과거 데이터를 빠르게 삼키면서 몇 초의 처리 시간 동안 몇 주 분량의 이벤트 시간을 훑어가기도 해요.
Flink에서 이벤트 시간의 진행을 측정하는 장치가 바로 **워터마크(watermark)**예요. 워터마크는 데이터 스트림의 일부처럼 흐르면서 타임스탬프 t를 함께 운반하는데, Watermark(t)는 그 스트림에서 이벤트 시간이 t에 도달했다는 선언이에요. 즉 타임스탬프가 t보다 같거나 오래된(t' <= t) 요소는 이제 더 이상 나타나지 않아야 한다는 뜻이죠. 순서가 뒤섞인 스트림에선 특히 워터마크가 중요해요. 워터마크가 연산자에 도착하면, 연산자는 자신의 내부 이벤트 시간 시계를 그 워터마크 값까지 앞당길 수 있어요.
병렬 스트림에서의 워터마크
워터마크는 소스 함수에서, 또는 소스 함수 직후에 생성돼요. 소스 함수의 각 병렬 서브태스크는 보통 독립적으로 자기만의 워터마크를 만들어 내요. 이 워터마크들이 스트리밍 프로그램을 따라 흐르면서 도착하는 연산자들의 이벤트 시간을 앞당기고, 연산자가 이벤트 시간을 앞당길 때마다 후속 연산자에게 새 워터마크를 아래로 내려보내요.
여러 입력 스트림을 소비하는 연산자(예: union, 또는 keyBy(...)·partition(...) 뒤의 연산자)의 현재 이벤트 시간은 입력 스트림들의 이벤트 시간 중 최솟값이에요. 입력 스트림들이 이벤트 시간을 갱신하면 그 연산자의 이벤트 시간도 함께 갱신되죠.
늦은 데이터 (Lateness)
워터마크 조건을 어기는 요소가 나올 수도 있어요. 즉 Watermark(t)가 흐른 뒤에도 타임스탬프가 t' <= t인 요소가 더 나타나는 경우죠. 실제 환경에서는 특정 요소가 얼마든지 늦게 도착할 수 있어서, 어떤 타임스탬프의 요소가 모두 도착했을 시점을 정확히 정하기 어려울 때가 많아요. 게다가 지연이 어느 정도 제한된다 해도, 워터마크를 너무 늦게 흘려보내면 이벤트 시간 윈도우의 평가가 지나치게 지연돼서 바람직하지 않아요.
그래서 스트리밍 프로그램은 늦은(late) 요소가 있을 거라는 사실을 명시적으로 감안하는 경우가 많아요. 늦은 요소란 워터마크가 신호한 시스템의 이벤트 시간 시계가, 그 요소의 타임스탬프 시각을 이미 지나간 뒤에 도착한 요소를 말해요. 이벤트 시간 윈도우에서 늦은 요소를 다루는 방법은 Allowed Lateness 문서에서 자세히 볼 수 있어요.
윈도우
스트림에서 이벤트를 집계(개수, 합 등)하는 방식은 배치 처리와 달라요. 스트림은 기본적으로 무한(unbounded)이라 모든 요소를 다 세는 것은 불가능하거든요. 그래서 스트림의 집계는 **윈도우(window)**로 범위를 한정해요. '지난 5분 동안의 개수', '마지막 100개 요소의 합' 같은 집계가 대표적이에요.
윈도우는 시간 기반(예: 30초마다)일 수도, 데이터 기반(예: 100개 요소마다)일 수도 있어요. 보통은 겹침이 없는 텀블링 윈도우(tumbling window), 겹침이 허용되는 슬라이딩 윈도우(sliding window), 활동이 끊긴 간격으로 구분되는 세션 윈도우(session window) 같은 유형으로 나누죠. 각 윈도우 유형의 구체적인 사용법은 DataStream API의 윈도우 문서에서 확인할 수 있어요.
더 알아보기 (Learn more)
- 사용자 정의 함수 (Working with State) — 키 기반 상태를 실제로 다루는 API
- 상태 기반 스트림 처리 (Stateful Stream Processing) — 상태가 무엇인지, 어떻게 체크포인트와 연결되는지
- 윈도우 (Windows) — 텀블링·슬라이딩·세션 윈도우 사용법
- 체크포인트 (Checkpointing) — 장애 복구를 위한 체크포인트 설정과 방법