컬리 SCM팀이 재고 정산을 배치에서 이벤트 기반으로 전환하며 Kafka Streams 텀블링 윈도우를 도입한 기록이다. "윈도우는 시간이 지나면 자동 발행"이라는 오해를 깨고 스트림 시간 기반 발행을 이해하기까지의 시행착오를 다룬다.
핵심 포인트- 스파이크성 트래픽(주문 마감·새벽 배송)을 상품코드 + 5분 간격으로 집계하되 정해진 시점마다 한 번만 저장해 DB 부하를 막는 게 목표다.
- 이벤트가 겹치지 않아 한 번만 발행되는 텀블링 윈도우를 선택하고 grace period 1분으로 지각 이벤트를 수용했다.
- 윈도우는 시간 경과가 아니라 스트림 시간(처리된 최신 이벤트 시간)이 종료 + 유예를 지나야 발행되며, 이후 이벤트가 안 들어오면 영원히 안 닫힌다.
- 데이터 생성 시간과 카프카 발행 시간이 달라 정산일이 어긋나던 문제를 TimestampExtractor로 생성 시간 기준 이벤트 시간을 재정의해 해결했다.
- 이벤트가 없는 시간대의 윈도우는 외부 더미 이벤트를 파티션별로 주입해 스트림 시간을 강제 전진시킨다.
상세 정리- 배경: 배치는 데이터 누적 대기로 실시간성이 부족하고 특정 시간대 부하가 집중돼, 카프카 전환으로 준실시간 정산·확장성을 확보했다.
- 윈도우 선택: 텀블링(고정·비겹침) vs 호핑·세션·슬라이딩 중, 하나의 이벤트가 정확히 하나의 윈도우에 들어가 중복 집계를 막는 텀블링을 채택했다.
- 구현: groupByKey 후 TimeWindows.ofSizeAndGrace(5분, 1분) + reduce 집계 + suppress(untilWindowCloses)로 중간 집계를 억제하고 최종만 발행한다.
- 발행 시점 개념: 이벤트 시간(레코드 timestamp)과 스트림 시간(처리된 최신 이벤트 시간)을 구분하며, 윈도우 시간 내 이벤트만으론 발행이 불가하고 이후 이벤트가 스트림 시간을 갱신해야 발행된다.
- 문제 1: 23:59 생성 데이터가 00:00에 발행되면 다음 날 00:00~00:05 윈도우에 들어가 잘못된 날 정산에 포함된다.
- 해결 1: TimestampExtractor를 구현해 record.value의 createdAt으로 이벤트 시간을 재정의하면 23:59 데이터가 23:50~23:59 윈도우에 정확히 포함된다.
- 문제 2: 윈도우 종료 후 새 이벤트가 없으면 스트림 시간이 멈춰 윈도우가 안 닫힌다.
- 실패 시도 1: Processor 내부 forward는 Topology 내부 전파일 뿐 스트림 시간을 안 올린다(소스 토픽 소비 레코드 timestamp로만 계산).
- 실패 시도 2: WindowStore를 직접 스캔해 강제 발행하면 내부 상태가 안 닫혀 다음 이벤트 때 같은 윈도우가 중복 발행된다.
- 해결 2: PunctuationType.WALL_CLOCK_TIME으로 3분마다 각 파티션에 더미 이벤트(dummy-key-{partition}, 파티션 번호 직접 지정)를 발행해 스트림 시간을 강제 전진시키며, 스트림 시간이 파티션별 독립이라 키를 파티션마다 다르게 한다.
- 교훈: 윈도우 설계는 시간을 자르는 게 아니라 시간과 이벤트의 관계를 정의하는 일이고, 경계 시점(자정) 정책·재계산 로직이 필요하며 시간 단위 테스트가 몇 분씩 걸려 팀 스터디·리뷰가 핵심이었다.
왜 읽나Kafka Streams로 시간 기반 집계·정산을 구현하는 백엔드/데이터 엔지니어에게 스트림 시간·grace period·윈도우 강제 발행의 함정 레퍼런스.