포스트

스트리밍 시스템 1부 — 끝없는 이벤트를 시간으로 묶기

한 번의 클릭은 어느 시간의 비용이 되는가

광고 과금 이벤트는 두 번 처리되어서도 안 되고, 누락되어서도 안 됩니다. 매일 정산할 때 이 조건을 어떻게 보장할 수 있는지 이해하고 싶어 《Streaming Systems》를 읽었습니다.

그런데 정확히 한 번 처리하는 방법을 생각하기 전에 정해야 할 것이 있었습니다. 오늘의 과금 대상은 어떤 이벤트이며, 그 이벤트가 모두 도착했다고 언제 판단할 수 있을까요? 자정이 됐다고 전날의 클릭이 더는 들어오지 않는 것은 아닙니다.

책에 등장하는 윈도우, 워터마크, 트리거는 이 질문을 서로 다른 부분에서 다룹니다. 개념을 따로 읽을 때보다 하나의 과금 흐름에 적용해보니 각각의 역할이 더 선명해졌습니다.

끝없이 들어오는 클릭을 어느 시간에 묶고, 언제 그 결과를 사용할 수 있을까?

이번 글에서는 이 질문을 따라가려고 합니다. 아래 흐름은 책의 원리를 이해하기 위한 단순화한 예시입니다.

출발점 — 무엇을 계산할 것인가

광고주가 클릭 한 번당 비용을 지불하는 CPC(Cost Per Click) 광고를 가정하겠습니다. 모든 클릭의 단가는 100원이고, 캠페인별 클릭 수와 예상 비용을 계산합니다. 우선 각 클릭이 한 번씩 들어온다고 가정하고 시간 문제에 집중하겠습니다.

1
2
3
4
5
클릭 이벤트 → 캠페인별 클릭 수 집계 → 클릭 수 × 100원

event_id = click-42
campaign_id = A
event_time = 10:07

계산식만 보면 단순합니다. 하지만 서비스가 운영되는 동안 클릭은 계속 들어옵니다. 이처럼 끝을 미리 정할 수 없는 데이터를 무한 데이터(Unbounded Data)라고 합니다. 여기서 무한하다는 것은 지금 무한 개가 있다는 뜻이 아니라, 입력의 끝을 기다리는 방식으로 작업을 마칠 수 없다는 뜻입니다.

《Streaming Systems》는 이런 계산을 What, Where, When, How라는 네 가지 질문으로 나눠 설명합니다. 이번 예시에서는 다음과 같이 읽을 수 있습니다.

질문광고 비용 계산에서의 의미
What캠페인별 클릭 수와 비용을 계산한다
Where어떤 시간 범위의 클릭을 함께 계산할지 정한다
When현재까지 계산한 결과를 언제 내보낼지 정한다
How여러 번 내보낸 결과가 누적값인지 새로 추가된 값인지 정한다

What은 정했습니다. 이제 나머지 질문을 하나씩 해결해보겠습니다.

1. 첫 번째 문제 — 끝없는 클릭을 어디까지 묶을까

문제: 캠페인의 클릭 수에는 끝이 없다

광고주가 캠페인 A의 클릭 수를 보고 싶다고 해보겠습니다. 범위를 정하지 않으면 클릭 수는 서비스가 운영되는 내내 늘어납니다. 하루 전체를 볼지, 최근 10분을 볼지에 따라 같은 입력에서도 다른 결과가 필요합니다.

여기서는 대시보드에 10분 단위의 클릭 수와 예상 비용을 보여주기로 하겠습니다.

해결: 이벤트 시간으로 윈도우를 나눈다

윈도우(Window)는 어떤 이벤트를 같은 계산에 포함할지 정하는 기준입니다. 10분 고정 윈도우(Fixed Window)를 사용하면 다음과 같이 나눌 수 있습니다.

1
2
3
클릭 발생 시각                 소속 윈도우
10:01, 10:03, 10:07     →     [10:00, 10:10): 3건
10:12                  →     [10:10, 10:20): 1건

[10:00, 10:10)은 10:00을 포함하고 10:10은 포함하지 않는 구간입니다. 정확히 10:10에 발생한 클릭은 다음 윈도우에 속합니다.

이제 질문은 “캠페인 A의 클릭 수는 몇 개인가?”에서 “10:00부터 10:10까지 발생한 캠페인 A의 클릭은 몇 개인가?”로 구체화됩니다. 이것이 Where입니다.

그런데 시간을 기준으로 묶으려면 어떤 시간을 사용할지도 정해야 합니다.

시간의미click-42의 예
이벤트 시간(Event Time)이벤트에 기록된 발생 시각사용자가 클릭한 10:07
처리 시간(Processing Time)처리기가 이벤트를 관찰하거나 처리하는 시각처리기에 도착한 10:12

사용자가 10:07에 클릭했지만 네트워크 지연으로 10:12에 도착했다면, 처리 시간 기준으로는 다음 구간에 들어갑니다. 이번 예시는 클릭이 발생한 시각에 비용을 귀속하기로 했으므로 이벤트 시간을 사용합니다.

날짜 경계에서는 이 선택이 정산에 직접 영향을 줍니다. 23:59:58에 발생해 다음 날 00:00:03에 도착한 클릭은 전날의 과금 대상입니다. 일일 정산이라면 날짜를 나누는 시간대도 함께 정해야 합니다. 이 글에서는 서울 시간을 기준으로 생각하겠습니다.

제약: 윈도우가 끝나도 입력이 끝난 것은 아니다

윈도우로 계산 범위를 정했지만, 그 범위의 이벤트가 모두 도착했는지는 아직 알 수 없습니다.

1
2
3
4
5
현재 처리 시각: 10:10
집계할 윈도우: [10:00, 10:10)

아직 도착하지 않은 클릭:
  발생 시각 10:08 → 도착 시각 10:13

10:10이 되는 순간 결과를 확정하면 이 클릭이 빠집니다. 이벤트 시간을 사용하면 늦게 도착해도 올바른 윈도우를 찾을 수 있지만, 도착하지 않은 이벤트를 미리 계산할 수는 없습니다.

결국 다음 질문은 어느 시각까지의 입력이 도착했다고 판단할 것인가입니다.

2. 두 번째 문제 — 얼마나 기다린 뒤 결과를 내보낼까

문제: 모두 도착할 때까지 기다릴 수 없다

모든 클릭이 도착한 뒤 계산하면 좋겠지만, 마지막 클릭이 언제 들어올지 알기 어렵습니다. 그렇다고 대시보드에 아무것도 보여주지 않을 수도 없습니다. 광고주는 현재 비용을 보고 싶고, 시스템은 예산 소진 상황을 파악해야 합니다.

이 문제에는 두 가지 판단이 필요합니다. 입력이 얼마나 진행됐는지 판단하는 것과, 지금 가진 데이터로 결과를 보여줄지 결정하는 것입니다.

해결: 워터마크로 진행을 판단하고 트리거로 출력한다

워터마크(Watermark)는 이벤트 시간 기준으로 입력이 어디까지 진행됐다고 보는지 나타냅니다. 일반적인 소스에서는 관찰한 데이터와 지연 특성을 바탕으로 입력 완전성을 추정합니다. 예를 들어 워터마크가 10:10을 지났다면, 그 이전 시각의 이벤트는 도착했을 것으로 보고 계산을 진행할 수 있습니다. 추정의 강도는 소스가 제공하는 보장에 달려 있습니다. Beam의 워터마크 설명

워터마크는 서버의 현재 시각과 다릅니다. 처리 시각이 10:15여도 입력이 밀려 있다면 워터마크는 그보다 뒤처질 수 있습니다.

트리거(Trigger)는 현재까지의 계산 결과를 언제 내보낼지 정합니다. 다음 예시에서는 워터마크가 윈도우 끝을 지나기 전에도 중간 결과를 내보내고, 이후 지연 클릭이 들어오면 다시 출력하도록 구성했다고 하겠습니다.

출력 계기누적 클릭 수예상 비용결과의 성격
처리 시간 기준 중간 출력30건3,000원Early
다음 중간 출력60건6,000원Early
워터마크가 윈도우 끝을 통과100건10,000원On-Time
이후 지연 클릭 5건 반영105건10,500원Late

Early는 빠르게 상황을 보여주는 결과이고, On-Time은 워터마크가 윈도우 끝을 통과할 때의 결과입니다. Late는 그 이후 지연 데이터를 반영한 결과입니다. 이런 출력을 실제로 낼지는 트리거와 지연 데이터 정책에 달려 있습니다.

여기서 On-Time을 최종 정산 금액으로 받아들이면 안 됩니다. 추정에 기반한 워터마크를 사용한다면 이후에도 해당 윈도우에 속하는 이벤트가 들어올 수 있습니다.

제약: 지연 데이터를 기다리는 만큼 상태를 유지해야 한다

위 예시에서 10,000원이 10,500원으로 바뀌려면 이전 윈도우의 계산 상태가 남아 있어야 합니다. 하지만 모든 과거 윈도우를 계속 유지하면 필요한 저장 공간도 늘어납니다.

그래서 지연 허용 범위(Allowed Lateness)를 정합니다. 예를 들어 Beam에서 윈도우 끝 이후 30분의 지연을 허용하면, 워터마크가 그 만료 경계를 지나갈 때까지 윈도우 상태를 유지해 지연 데이터를 반영할 수 있습니다. 이는 실제 시계로 30분을 기다린다는 의미와 다릅니다. Beam의 지연 데이터 처리 설명

1
2
3
4
5
6
윈도우: [10:00, 10:10)
지연 허용 범위: 30분

워터마크가 윈도우 끝을 통과 → On-Time 출력 가능
그 뒤 허용 범위 안의 지연 클릭 → 결과 갱신 가능
워터마크가 만료 경계를 통과 → 윈도우 상태 정리 가능

허용 범위를 넘은 이벤트는 이 계산에서 제외될 수 있습니다. 광고 과금이라면 이를 조용히 버려도 되는지, 별도 보관해 정산 조정으로 반영할지 정책이 필요합니다.

이렇게 출력 시점과 기다릴 범위를 정하면 빠른 결과를 제공할 수 있습니다. 대신 같은 윈도우에서 여러 번 나온 값을 소비자가 어떻게 해석할지라는 문제가 남습니다.

3. 세 번째 문제 — 결과가 여러 번 나오면 무엇을 더해야 할까

문제: 30 다음에 나온 60의 의미가 모호하다

첫 번째 출력이 30이고 두 번째 출력이 60이라고 해보겠습니다. 새 클릭이 60건 추가됐다는 뜻일까요, 지금까지의 합계가 60건이라는 뜻일까요?

소비자가 이를 구분하지 못하면 생산자의 집계가 맞아도 최종 비용이 틀립니다. 이 문제를 다루는 것이 How입니다.

해결: 출력 사이의 관계를 명시한다

클릭이 차례로 30건, 30건, 40건 들어왔다고 하겠습니다. 이 집계의 출력 방식은 다음처럼 나눌 수 있습니다.

방식각 출력의 의미출력 예소비자의 처리
Discarding이전 출력 이후 새로 모인 클릭의 집계30, 30, 40각 값을 합산해 100으로 계산
Accumulating해당 윈도우에서 지금까지 모인 클릭의 집계30, 60, 100최신 누적값 100으로 갱신

누적 모드에서는 이미 출력한 데이터도 다음 계산에 포함합니다. 반대로 Discarding 모드에서는 출력한 구간의 데이터를 다음 집계에 누적하지 않습니다. Beam의 누적 모드 설명

이번 대시보드에서는 누적 모드를 사용하겠습니다. 캠페인과 윈도우를 같은 결과의 키로 보고 현재 값을 갱신하면, 광고주는 그 구간에서 지금까지 발생한 클릭과 비용을 볼 수 있습니다.

1
2
3
4
키: campaign=A, window=[10:00, 10:10)

클릭 수: 30 → 60 → 100 → 105
예상 비용: 3,000원 → 6,000원 → 10,000원 → 10,500원

제약: 출력 의미를 정해도 저장의 정확성은 별개다

누적 비용 10,000원과 10,500원을 각각 새 과금으로 더하면 20,500원이 됩니다. 누적 출력이라는 계약을 지켜야 올바른 결과가 남습니다.

순서도 고려해야 합니다. 10,500원을 반영한 뒤 예전의 10,000원이 늦게 도착한다면, 단순 덮어쓰기로는 값이 되돌아갈 수 있습니다. 저장 단계에서는 같은 윈도우의 어떤 결과가 더 최신인지 판별할 방법이 필요합니다.

Discarding도 마찬가지입니다. 새로 추가된 비용을 출력하더라도 그 출력이 재전달되면 같은 금액을 두 번 더할 수 있습니다. 누적 모드는 출력의 의미를 정하지만, 장애와 재시도에 따른 중복 반영까지 해결하지는 않습니다.

이 지점에서 시간과 윈도우의 문제가 과금 결과를 신뢰하는 문제로 이어집니다.

실무에 적용해본다면 — 대시보드와 일일 정산

같은 클릭 스트림을 사용해도 대시보드와 정산은 필요한 조건이 다릅니다. 앞의 예시를 확장하면 다음과 같은 정책을 생각할 수 있습니다.

항목실시간 대시보드일일 정산
시간 범위이벤트 시간 기준 10분정산 시간대 기준 하루
결과를 사용하는 시점중간 결과부터 표시입력 마감과 처리 완료를 확인한 뒤 확정
지연 이벤트허용 범위 안에서 화면 갱신마감 전 반영하거나 마감 후 조정 내역으로 처리
결과의 의미현재까지의 예상 비용해당 정산 범위에 확정한 비용

대시보드에서 값이 수정되는 것은 자연스럽습니다. 하지만 정산은 “충분히 기다렸다”는 이유만으로 확정하기 어렵습니다. 어느 범위의 이벤트를 대상으로 삼았는지, 그 범위가 모두 수집됐는지, 처리 결과가 저장됐는지를 확인해야 합니다.

이때 두 질문을 구분하면 도움이 됩니다.

1
2
완전성: 이번 정산에 필요한 이벤트가 모두 들어왔는가?
처리의 정확성: 받아들인 이벤트를 누락·중복 없이 반영했는가?

실제 클릭이 105건인데 100건만 도착했다면, 그 100건을 올바르게 계산해도 전체 결과는 아직 불완전합니다. 반대로 105건을 모두 받았어도 한 클릭을 두 번 과금하면 비용은 틀립니다.

워터마크는 입력의 진행을 판단하는 근거를 제공합니다. 전체 수집을 보장하려면 소스의 완료 조건을 함께 봐야 하고, 추정만 가능한 환경에서는 마감과 조정 정책을 명시해야 합니다. 외부 원장에 빠짐없이 한 번 반영됐는지는 별도의 처리 보장이 필요합니다.

시간 정책을 하나의 흐름으로 표현하면

지금까지의 선택을 의사 코드로 묶어보겠습니다. 특정 프레임워크의 실행 가능한 API가 아니라, 대시보드 집계에 필요한 정책을 표현한 예시입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
clickEvents
    .filter(event.type == CLICK)
    .assignEventTime(event.eventTime)
    .keyBy(event.campaignId)
    .window(FixedWindow(10 minutes))
    .trigger(
        early = periodically in processing time,
        onTime = when watermark passes window end,
        late = when accepted late data arrives
    )
    .allowedLateness(30 minutes in event-time progress)
    .accumulationMode(ACCUMULATING)
    .aggregate(
        clickCount = count(),
        estimatedCost = clickCount * 100
    )
    .emitWindowSnapshotToDashboard()

이 코드는 윈도우별 예상 비용을 언제 어떤 형태로 출력할지 표현합니다. 출력의 버전 관리나 장애 복구, 외부 저장소의 중복 방지는 아직 포함하지 않았습니다. 이 결과를 그대로 청구 API에 연결하면 정산 시스템이 완성되는 것은 아닙니다.

시간의 경계에서 처리의 보장으로

광고 과금 예시를 따라가며 각 개념이 해결하는 문제와 남기는 조건을 정리할 수 있었습니다.

단계해결한 문제남는 조건
이벤트 시간과 윈도우끝없는 클릭을 어느 구간에 귀속할지 정함그 구간의 입력이 모두 도착했는지 판단해야 함
워터마크와 트리거, 지연 정책언제 출력하고 얼마나 상태를 유지할지 정함결과가 여러 번 나오고 이후에도 바뀔 수 있음
누적 방식여러 출력 사이의 관계를 정함저장 순서와 재시도에 따른 중복을 다뤄야 함

1부에서 다룬 것은 끝없이 들어오는 이벤트에 계산 범위와 출력 기준을 부여하는 방법입니다. 그 기준이 생기면 다음 질문을 할 수 있습니다.

정산 대상 범위를 정했다면, 장애와 재시도를 거쳐도 그 범위의 과금 결과를 어떻게 한 번씩 남길 수 있을까?

2부에서는 이 질문을 이어가겠습니다. 최소 한 번과 최대 한 번 처리에서 출발해, 입력 위치와 계산 상태를 함께 복구하는 체크포인트, 외부 원장에 결과를 한 번만 반영하는 트랜잭션과 멱등성을 살펴보겠습니다. 그리고 이 보장들을 일일 정산의 확정 절차에 연결해보려고 합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.