Tech

실시간 CTR 집계 파이프라인 구축 기록

약 12분조회수를 불러오고 있어요
#Flink#Kafka#ClickHouse#Redis#Data Pipeline

데이터 엔지니어링 역량을 강화하는 과정에서 1BRC 같은 배치 중심 실험을 해봤지만, 기술이 돌아가는지만 확인하는 방식으로는 부족하다고 느꼈다.
핵심은 결과 자체보다 설계부터 운영 관측, 병목 분석, 복구 가능성까지 포함해 하나의 시스템을 끝까지 책임지며 현재 시스템에 맞는 기준을 스스로 세워보는 데 있다고 봤다.

그 과정에서 특히 끌린 주제가 스트리밍 처리였다.
정확한 집계, 지연 이벤트 처리, 상태 보존, 운영 안정성을 한 번에 검증할 수 있기 때문이다. 그래서 스트림 처리의 핵심 문제가 비교적 압축적으로 드러나는 CTR 집계를 개인 프로젝트 주제로 잡았다.

실시간 CTR 집계는 단순한 카운터 문제가 아니었다.
조회와 클릭 이벤트는 같은 시점에 도착하지 않았고, 파티션 분포도 고르지 않았으며, 집계 결과는 운영 화면과 분석 저장소가 동시에 신뢰할 수 있어야 했다.

이 글은 Flink 기반 CTR 파이프라인을 개인 프로젝트로 설계하면서 어떤 제약을 먼저 정의했고, 워터마크와 윈도우를 어떤 기준으로 결정했으며, 실제 병목을 어떻게 확인하고 수정했는지 정리한 기록이다.

CTR 파이프라인 전체 구조CTR 파이프라인 전체 구조

프로젝트 저장소: ctr-pipeline

왜 CTR 집계가 운영 문제인가

CTR은 보통 아래 두 이벤트를 합쳐 계산한다.

  • impression: 노출
  • click: 클릭

문제는 이 두 이벤트가 항상 같은 순서로, 같은 지연으로 들어오지 않는다는 점이다.

  • 네트워크 지연으로 클릭이 더 늦게 도착할 수 있다.
  • Kafka 파티션 분포가 한쪽으로 쏠릴 수 있다.
  • 집계는 짧은 윈도우 단위로 안정적으로 잘려야 한다.
  • 중복 또는 누락은 CTR 오차로 바로 이어진다.

즉, 이 시스템의 핵심은 "숫자를 빨리 계산하는 것"이 아니라 아래 조건을 동시에 만족시키는 것이었다.

  • 이벤트 순서가 어긋나도 결과를 설명할 수 있어야 한다.
  • 지연 이벤트를 받되, 레이턴시는 통제해야 한다.
  • 운영 API와 분석 저장소가 같은 결과를 기준으로 움직여야 한다.
  • 병목이 생겼을 때 Flink UI와 로그만으로 원인을 좁힐 수 있어야 한다.

잘못 설계하면 어떤 비용이 생기는가

개인 프로젝트였지만, 의미 있는 검증을 하려면 운영 문제를 함께 가정해야 했다.

  • 지연 클릭을 충분히 흡수하지 못하면 저성과 상품과 고성과 상품이 뒤바뀐 것처럼 보일 수 있다.
  • 집계 결과가 서빙 API와 분석 저장소에서 다르게 보이면, 숫자 자체보다 시스템 신뢰가 먼저 무너진다.
  • 파티션 분포와 병렬도가 맞지 않으면 처리량이 떨어지는 것보다 원인 추적 비용이 더 커진다.
  • 체크포인트가 불안정하면 장애 이후 어디까지 안전하게 처리됐는지 설명할 수 없게 된다.

결국 CTR 오차는 단순한 비율 계산 실수가 아니라, 잘못된 운영 판단과 디버깅 비용으로 이어질 수 있다.
그래서 이 프로젝트에서는 빠른 데모보다 설명 가능한 결과와 복구 가능한 상태를 우선순위에 뒀다.

시스템 요구사항과 제약

항목기준
입력 소스Kafka impression, click 토픽
집계 기준상품 단위 CTR
목표 처리초당 수천 건 이상 안정 처리
정확성중복/누락에 민감하므로 Exactly-once 우선
지연 허용늦게 도착한 이벤트를 일부 흡수해야 함
결과 소비처Redis, ClickHouse, DuckDB, FastAPI
실험 환경MacBook Air M1, 메모리 제약이 있는 로컬 환경

여기서 중요한 제약은 "운영 수준의 판단을 로컬 환경에서도 검증해야 한다"는 점이었다.
리소스가 넉넉한 환경에서만 성립하는 구조는 초기에 잘못된 자신감을 만들 수 있다고 봤다.

실험 환경 상세

초기 실험은 아래 조합으로 진행했다.

  • Kafka 3 broker
  • Flink JobManager 1, TaskManager 1
  • Redis + RedisInsight
  • ClickHouse
  • FastAPI serving layer
  • Superset

즉, "단순히 Flink job 하나만 돌린다"가 아니라, 실제로 결과를 읽고 확인하는 운영 구성까지 함께 올린 상태에서 검증했다.

설계 결정 요약

의사결정선택이유
시간 기준Event Time실제 발생 시각 기준으로 집계해야 정확도를 설명할 수 있기 때문이다.
윈도우10초 Tumbling Window5초는 변동성이 컸고, 30초는 응답성이 너무 늦었다.
워터마크Out-of-Order 5초정확도와 레이턴시를 동시에 맞춘 실험값이었다.
추가 지연 허용Allowed Lateness 5초늦게 도착한 이벤트를 일정 수준 흡수하기 위해 필요했다.
상태 보존Checkpoint + Exactly-onceCTR 오차를 작게 보지 않기 위해 기본 전제로 잡았다.
싱크 전략Redis + ClickHouse + DuckDB서빙, 분석, 로컬 검증 목적을 분리했다.
병렬도 기준Kafka 파티션 수와 정합 유지Backpressure를 줄이기 위해 처리 단계의 균형을 맞췄다.

이 프로젝트에서 중요한 점은 "Flink를 썼다"가 아니었다.
각 파라미터가 어떤 운영 문제를 해결하기 위해 존재하는지 설명할 수 있게 만드는 것이 더 중요했다.

시스템 구성

전체 흐름

Impressions / Clicks
  -> Kafka
  -> Flink
  -> Redis / ClickHouse / DuckDB
  -> FastAPI

운영 확인도 함께 하기 위해 Kafka UI와 RedisInsight를 붙였다.
스트림 시스템은 내부 상태를 보지 못하면 장애 재현이 어려워지기 때문에, 초기부터 관측 도구를 같이 두는 편이 낫다고 판단했다.

애플리케이션 구조

flink-app/src/main/kotlin/com/example/ctr/
├── domain/
│   ├── model/
│   │   ├── Event.kt
│   │   ├── EventCount.kt
│   │   └── CTRResult.kt
│   └── service/
│       ├── EventCountAggregator.kt
│       └── CTRResultWindowProcessFunction.kt
├── application/
│   └── CtrJobService.kt
├── infrastructure/
│   ├── flink/
│   │   ├── source/
│   │   └── sink/
│   └── config/
└── CtrApplication.kt

구조는 의도적으로 세 층으로 나눴다.

  • domain: 순수 집계 규칙
  • application: Flink job orchestration
  • infrastructure: Kafka, Redis, ClickHouse, DuckDB 연동

이렇게 나누면 집계 규칙 검증과 외부 시스템 검증을 분리할 수 있다.

데이터 흐름과 상태 관리

파이프라인의 기본 흐름은 아래와 같다.

  1. impressionclick 이벤트를 Kafka로 수집한다.
  2. Flink가 이벤트 시간을 기준으로 토픽을 소비한다.
  3. 상품 단위로 keyBy 한 뒤 10초 단위로 집계한다.
  4. 결과를 Redis, ClickHouse, DuckDB에 각각 쓴다.
  5. FastAPI는 Redis를 통해 최신 결과를 제공한다.

이벤트 시간과 윈도우 상태

CTR 계산은 윈도우마다 아래 상태를 유지하는 방식으로 구현했다.

data class EventCount(
    val productId: String,
    val impressions: Long,
    val clicks: Long,
    val windowStart: Long,
    val windowEnd: Long
)

이 모델은 단순하지만 중요한 장점이 있었다.

  • 집계 단위를 명확하게 표현할 수 있다.
  • 결과 저장소가 달라도 동일한 계약으로 전달할 수 있다.
  • 테스트에서 순수 로직 검증이 쉬워진다.

Exactly-once 경계

CTR은 "한두 건 정도 차이"를 허용하기 어려운 지표였다.
그래서 초기부터 아래 조합을 기본값으로 잡았다.

env.enableCheckpointing(10_000)
env.checkpointConfig.checkpointingMode = CheckpointingMode.EXACTLY_ONCE
env.checkpointConfig.externalizedCheckpointCleanup =
    CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION

Exactly-once 보장 구성Exactly-once 보장 구성

이 선택은 처리량보다 복구 가능성과 재현 가능성을 우선한 결정이었다.
체크포인트를 보존해두면 장애 후에도 "어디까지 안전하게 처리됐는가"를 판단할 수 있다.

파라미터를 이렇게 결정했다

10초 윈도우

실험 초기에 5초, 10초, 30초를 비교했다.

  • 5초: CTR 값이 지나치게 흔들려 운영 지표로 보기 어려웠다.
  • 10초: 응답성과 안정성의 균형이 가장 좋았다.
  • 30초: 지표 안정성은 좋았지만 실시간 모니터링 용도로는 너무 느렸다.

여기서 배운 점은 분명했다.
윈도우 길이는 기술 파라미터가 아니라 "누가 어떤 속도로 결과를 소비하는가"에 맞춰 정해야 한다.

워터마크 5초

워터마크를 너무 짧게 두면 늦게 도착한 클릭을 버리게 되고, 너무 길게 두면 결과가 너무 늦어진다.
이번 실험에서는 5초가 가장 현실적인 균형점이었다.

  • 5초 미만: 지연 이벤트 손실이 눈에 띄게 늘었다.
  • 5초: 정확도 저하를 줄이면서 결과 반영 속도도 유지했다.
  • 5초 초과: 레이턴시 증가 대비 체감 이득이 크지 않았다.

Allowed Lateness 5초

워터마크만으로는 늦게 온 이벤트를 충분히 흡수하기 어려웠다.
그래서 추가로 5초를 더 허용해 늦은 클릭 일부를 집계에 반영했다.

핵심은 "무한히 기다리는 것"이 아니라 "운영상 설명 가능한 선에서만 기다리는 것"이었다.

트레이드오프를 비교하며 정리한 선택 기준

이번 글에서 적은 최종 구성은 처음부터 정답으로 정해져 있던 것이 아니었다.
현재 남아 있는 기록 기준으로, 당시에는 여러 선택지를 비교하며 어떤 트레이드오프를 감수할지 먼저 정리해야 했다.

선택지고민한 트레이드오프최종 판단
5초 윈도우CTR 값 변동이 지나치게 커서 운영 지표로 보기 어려웠다.10초로 늘려 응답성과 안정성의 균형을 맞췄다.
30초 윈도우지표는 더 안정적이지만 실시간 모니터링 용도로는 반응이 너무 늦었다.10초 윈도우를 유지했다.
더 짧은 워터마크늦게 도착한 클릭 손실이 늘어 정확도 설명이 어려워졌다.Out-of-Order 5초를 택했다.
Processing Time 중심 집계네트워크 지연이나 시스템 부하에 따라 늦은 이벤트가 쉽게 제외될 수 있었다.Event Time 기준으로 고정했다.
단일 결과 저장소빠른 조회는 가능해도 분석, 교차 검증, 로컬 디버깅을 한 번에 만족시키기 어려웠다.Redis, ClickHouse, DuckDB로 역할을 분리했다.
파티션-병렬도 불일치Backpressure가 생겨도 어느 단계에서 막히는지 읽기가 어려웠다.Kafka 파티션과 Flink, 싱크 병렬도를 정합시켰다.

운영 중 실제로 부딪힌 병목

1. Backpressure

가장 먼저 드러난 문제는 Backpressure였다.
Kafka 파티션, Flink 병렬도, 싱크 병렬도의 균형이 깨지면 특정 구간에서 처리량이 급격히 떨어졌다.

Flink Backpressure 확인 화면Flink Backpressure 확인 화면

당시 로컬에서 확인한 대표 컨테이너 상태는 아래와 같았다.

이름CPU메모리
kafka177.39%345.4MiB
kafka2127.08%330.2MiB
kafka341.49%293.9MiB
flink-taskmanager55.83%756.4MiB
flink-jobmanager1.89%767.4MiB
clickhouse8.72%310.9MiB
ctr-api0.60%23.31MiB

로컬 환경에서도 이미 TaskManager와 JobManager가 각각 700MiB 이상을 쓰고 있었고, Kafka broker 3개까지 같이 떠 있는 상태였다.
즉, 단순히 "맥북이라 느리다"가 아니라, 실제로 전체 파이프라인 구성이 자원 압박을 만들고 있다는 점이 먼저 확인됐다.

Flink UI 기준으로는 초당 약 812건 처리, 25분 실행 시 약 120만 건 처리가 가능했다.
문제는 평균 처리량보다도 특정 구간에서 병목이 몰릴 때 전체 파이프라인이 급격히 흔들린다는 점이었다.

이 시점의 대응은 두 단계였다.

  1. 파티션 수를 3에서 6, 다시 12까지 늘렸다.
  2. Flink와 싱크 병렬도를 파티션 수와 맞췄다.

이 변경 이후, 병목은 "Flink가 느리다"가 아니라 "정합이 깨진 단계가 있다"는 식으로 더 정확히 읽히기 시작했다.

2. Serving API 병목

로컬 부하 테스트에서는 집계보다 서빙 계층이 먼저 흔들렸다.

부하 테스트 조건은 아래와 같았다.

  • 대상: FastAPI 기반 CTR 조회 API
  • 가상 사용자 수: 최대 20 VUs
  • 목표: p(95) < 1000ms
  • 실행 시간: 35초

요약 수치만 보면 아래와 같다.

p(95) = 6.41s
avg = 1.89s
dropped_iterations = 127
http_req_failed = 0.00%

즉, 오류는 없었지만 느려서 목표 SLO를 전혀 맞추지 못한 상태였다.

K6 raw output
█ THRESHOLDS
 
    http_req_duration
    ✗ 'p(95)<1000' p(95)=6.41s
 
    http_req_failed
    ✓ 'rate<0.01' rate=0.00%
 
    response_time
    ✗ 'p(95)<1000' p(95)=6.41s
 
  █ TOTAL RESULTS
 
    checks_total.......: 450     12.054939/s
    checks_succeeded...: 100.00% 450 out of 450
    checks_failed......: 0.00%   0 out of 450
 
    CUSTOM
    response_time..................: avg=1.9s  min=6ms    med=1.12s max=8.42s p(90)=5.37s p(95)=6.41s
 
    HTTP
    http_req_duration..............: avg=1.89s min=6.09ms med=1.12s max=8.42s p(90)=5.37s p(95)=6.41s
    http_req_failed................: 0.00%  0 out of 225
    http_reqs......................: 225    6.02747/s
 
    EXECUTION
    dropped_iterations.............: 127    3.402172/s
    iteration_duration.............: avg=1.9s  min=6.78ms med=1.13s max=8.43s p(90)=5.38s p(95)=6.41s
    iterations.....................: 225    6.02747/s
    vus............................: 20     min=0        max=20
 
running (0m37.3s), 00/20 VUs, 225 complete and 0 interrupted iterations
ctr_load ✓ [======================================] 00/20 VUs  35s  10.00 iters/s

원인은 단순했다.

  • Redis 조회 자체보다 API 계층과 직렬화 비용이 컸다.
  • 로컬 환경에서 불필요한 계층이 전체 실험 속도를 잡아먹고 있었다.

이 문제는 이후 성능 개선 단계에서 Redis와 Serving API 구조를 다시 점검하는 계기가 됐다.
즉, 초기 아키텍처는 "기능적으로 돌아가는가"를 증명했지만, 운영 경로를 최적화하려면 서빙 계층 자체를 다시 설계해야 했다.

3. 체크포인트 장애와 수정 과정

기존 기록에는 배포 직후 체크포인트 타임아웃이 연속으로 발생했다는 내용이 남아 있다.
정확한 timeout 수치까지 보존되지는 않았지만, 당시 핵심 문제는 로컬 자원 제약 환경에서 체크포인트 주기, 병렬도, 윈도우 상태 크기가 함께 맞물리며 복구 경계가 불안정해졌다는 점이었다.

당시에는 아래 세 가지 방향으로 조정했다.

  1. 체크포인트 주기와 timeout을 완화했다.
  2. 병렬도를 다시 맞춰 특정 단계에 부하가 몰리지 않게 조정했다.
  3. 윈도우 상태를 줄여 체크포인트가 잡아야 할 상태 크기를 줄였다.

이 사례가 중요한 이유는 성능 문제가 아니라 신뢰성 문제였기 때문이다.
Exactly-once를 선언하는 것과 실제로 복구 가능한 상태를 유지하는 것은 별개의 문제였고, 체크포인트가 흔들리면 그 차이가 바로 드러났다.

4. 파티션 Skew

productId 분포가 고르지 않으면 특정 파티션에 lag가 몰렸다.
이런 Skew 문제는 스트림 처리에서 자주 보이지만, 실제로 겪고 나서야 "키 설계도 운영 설계"라는 점이 선명해졌다.

이번 단계에서는 파티션 수 확장과 병렬도 정합으로 대응했지만, 장기적으로는 아래 두 가지가 필요하다고 판단했다.

  • 키 분포 분석 자동화
  • 특정 상품군 쏠림을 고려한 샤딩 전략

변경 전후 비교표

현재 소스에 남아 있는 기록만 기준으로, 주요 조정 전후를 정리하면 아래와 같다.
일부 항목은 정확한 수치 대신 당시 관찰 결과와 조정 방향을 기록했다.

항목변경 전변경 후관찰 결과
윈도우 길이5초10초5초에서 보이던 변동성이 줄고 운영 지표로 읽기 쉬워졌다.
윈도우 길이30초10초응답이 늦어지던 구성을 버리고 실시간 모니터링에 맞췄다.
이벤트 시간 처리Processing Time에 가까운 단순 처리 가정Event Time + Watermark + Allowed Lateness지연 이벤트를 설명 가능한 범위 안에서 반영할 수 있게 됐다.
파티션/병렬도Parallelism 1과 정합이 깨진 상태파티션 3 -> 6 -> 12, 병렬도 정합 유지Backpressure 원인을 더 정확히 읽을 수 있게 됐다.
체크포인트배포 직후 timeout 연속 발생주기/timeout 완화, 병렬도 조정, 상태 축소실험이 체크포인트 실패에 계속 막히지 않도록 안정화했다.
서빙 APIp(95) < 1000ms 목표 대비 p95 = 6.41s병목 원인을 API 계층과 직렬화 비용으로 식별이후 성능 개선 단계에서 서빙 구조를 재검토하는 기준이 생겼다.

검증 방법

단위 테스트

핵심 계산은 순수 도메인 로직으로 유지했다.
그래서 0 나눗셈, 클릭 수 반영, 윈도우 경계 같은 기본 규칙을 빠르게 검증할 수 있었다.

class CTRResultTest {
    @Test
    fun `CTR 계산 - 정상 케이스`() {
        val result = CTRResult.calculate(
            productId = "product1",
            impressions = 100,
            clicks = 10,
            windowStart = 0L,
            windowEnd = 10000L
        )
 
        assertThat(result.ctr).isEqualTo(0.1)
    }
}

통합 테스트

실제 검증 포인트는 fromCollection -> keyBy -> window -> aggregate 전체 흐름이었다.
윈도우와 이벤트 시간 처리는 코드 한 줄이 아니라 흐름 단위로 깨지는 경우가 많기 때문이다.

@Test
fun `EventCountAggregator 통합 테스트`() {
    val env = StreamExecutionEnvironment.getExecutionEnvironment()
    env.setParallelism(1)
 
    val events = listOf(
        Event(eventType = "impression", productId = "p1", eventTime = 1_000L),
        Event(eventType = "click", productId = "p1", eventTime = 2_000L)
    )
 
    val result = env.fromCollection(events)
        .keyBy { it.productId }
        .window(TumblingEventTimeWindows.of(Time.seconds(10)))
        .aggregate(EventCountAggregator())
        .executeAndCollect()
 
    assertThat(result.first().impressions).isEqualTo(1)
    assertThat(result.first().clicks).isEqualTo(1)
}

수동 검증

테스트만으로는 부족했다. 아래 경로는 눈으로도 검증했다.

  • Flink UI에서 Backpressure와 처리량 확인
  • Kafka UI로 토픽 적재 상태 확인
  • Redis 결과와 ClickHouse 결과를 비교
  • FastAPI 응답과 원시 이벤트 수를 샘플 대조

운영 화면에서 보는 최신 CTR 값과 ClickHouse 집계 결과가 어긋나지 않는지 확인하는 과정이 특히 중요했다.
결과 저장소가 둘 이상일 때는 "둘 다 동작한다"보다 "둘이 같은 계약을 보고 있는가"를 먼저 확인해야 한다.

서빙 API 응답 예시

실제 API는 아래 형태로 최신 CTR 상태를 제공했다.

{
  "product1": {
    "productId": "product1",
    "impressions": 523,
    "clicks": 42,
    "ctr": 0.0803,
    "windowStart": 1701360000000,
    "windowEnd": 1701360010000
  }
}

이 응답 형태를 유지한 이유는 두 가지였다.

  • 운영 화면에서 바로 읽기 쉬워야 한다.
  • 이전 윈도우와 현재 윈도우를 비교하는 확장이 가능해야 한다.

스트림 시스템은 "테스트가 있으니 안전하다"보다 "관측 가능한 상태를 만들었는가"가 더 중요하다는 점을 이 단계에서 분명히 배웠다.

기술 선택 이유

Flink를 택한 이유

CTR 집계에서는 아래 세 가지가 중요했다.

  • 낮은 지연
  • 이벤트 시간 처리
  • 상태 기반 집계

Spark Streaming의 마이크로배치 모델보다 Flink의 이벤트 시간 처리와 상태 관리가 이 문제에 더 맞는다고 판단했다.

ClickHouse와 DuckDB를 같이 둔 이유

  • ClickHouse: 집계 결과를 장기적으로 분석하기 위한 OLAP 저장소
  • DuckDB: 로컬 디버깅과 가벼운 검증

실험 단계에서 운영용 분석 저장소와 개발용 확인 저장소를 분리해두면, 쿼리 검증 속도가 훨씬 빨라진다.

이 판단 기준이 다른 플랫폼 문제에도 적용되는 이유

이 프로젝트는 CTR 집계를 다뤘지만, 여기서 고정한 기준은 특정 도메인에만 묶이지 않는다.

  • 이벤트 순서와 지연을 어떤 기준으로 흡수할지 정하는 문제
  • 여러 소비처가 같은 결과를 보도록 계약을 맞추는 문제
  • 장애 이후 어디까지 안전하게 처리됐는지 설명할 수 있어야 하는 문제
  • 병목이 생겼을 때 코드가 아니라 처리 단계 단위로 원인을 좁히는 문제

이 네 가지는 스트림 집계뿐 아니라 CDC 파이프라인, IoT 이벤트 처리, 정산 집계처럼 비즈니스가 커질수록 내부가 복잡해지는 시스템에서 반복해서 등장한다.
그래서 이번 프로젝트의 가치는 CTR 수치를 계산했다는 사실보다, 복잡한 내부를 어떤 기준으로 정리하고 설명 가능한 상태로 만들지 실험했다는 데 있다.

한계와 다음 단계

이번 구현은 파이프라인의 핵심 문제를 드러내는 데는 충분했지만, 아직 운영 수준이라고 보기는 어렵다.

  • 단일 노트북 기반 검증이라 다중 TaskManager 환경 성능이 부족하다.
  • productId 외 다차원 집계는 아직 설계하지 않았다.
  • 멀티 리전 수준의 지연 분포를 반영한 워터마크 재조정은 하지 못했다.
  • 장기적으로는 Lakehouse 계층과의 결합도 검토해야 한다.

다음 단계는 아래 순서로 보는 것이 맞다고 판단했다.

  1. 클라우드 환경에서 병렬도와 체크포인트 안정성 재검증
  2. 다차원 집계 모델링
  3. 서빙 계층 단순화 또는 재설계
  4. 저장소 계층을 Lakehouse까지 확장할지 판단

남긴 판단 기준

이 프로젝트를 지나며 남은 기준은 세 가지다.

  • 스트림 처리 파라미터는 교과서값이 아니라 운영 소비 방식으로 정해야 한다.
  • Exactly-once는 비용이 들더라도, 설명할 수 없는 오차보다 싸다.
  • 병목은 코드 한 줄보다 처리 단계의 균형이 먼저 깨질 때 더 자주 발생한다.

학습 과정에서 바뀐 기준

이 프로젝트는 Flink API를 익히는 데서 끝나지 않았다.
오히려 "기술이 돌아간다"와 "운영 가능한 기준을 설명할 수 있다" 사이의 차이를 확인하는 과정에 가까웠다.

1BRC를 하면서도 느꼈지만, 결과가 나온다는 사실만으로는 충분하지 않았다.
왜 10초 윈도우를 택했는지, 왜 워터마크를 5초로 잡았는지, 그 판단이 어떤 부작용을 만들 수 있는지를 스스로 설명할 수 있어야 했다.

이 과정에서 특히 크게 배운 점은 공식 문서와 이론의 무게였다.
워터마크, Allowed Lateness, 체크포인트 같은 설정은 겉으로는 단순해 보여도 실제 동작 방식과 장애 양상을 이해하지 못하면 적절한 값을 잡기 어렵다. 설정 하나가 결과 정확도와 레이턴시에 직접 영향을 준다는 점도 더 분명해졌다.

또 하나 남은 기준은 도구와 책임의 경계였다.
Code Agent 같은 도구는 구현과 탐색 속도를 높여주지만, 어떤 기준으로 설계할지 결정하고 결과를 검증하고 최종 책임을 지는 주체는 결국 사람이다. 이 프로젝트를 개인 포트폴리오로 남기는 이유도, 단순히 Flink를 사용했다는 사실보다 그런 판단과 검증의 과정을 증명하고 싶었기 때문이다.

결국 이 프로젝트의 목적은 Flink 기능 시연이 아니었다.
실시간 지표를 운영 가능한 시스템으로 바꾸려면 어떤 판단을 먼저 고정해야 하는지, 그리고 그 판단을 어디까지 검증해야 하는지 확인하는 과정이었다.

댓글