pile·
백엔드·마켓컬리마켓컬리 Hello World·

Kafka Streams 윈도우 도입기

컬리 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·윈도우 강제 발행의 함정 레퍼런스.
마켓컬리
마켓컬리 Hello World 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·github-engGitHub Engineering·

    조기 종료를 없애야 벡터화된다 — 메모리 속도 소스 코드 케이스 폴딩

    GitHub의 코드 검색 엔진 Blackbird는 480TB 이상의 소스 코드를 인덱싱하기 전 모든 바이트에 case folding을 적용한다. 이 글은 Rust로 구현한 case folding을 메모리 대역폭 한계(45+ GiB/s)까지 끌어올린 두 가지 반직관적 최적화를 상세히 다룬다. 핵심은 루프 조기 종료(break) 제거로 LLVM 벡터화를 유도하고, UTF-8을 디코딩하지 않고 바이트 공간 산술만으로 fold를 수행하는 것이다.

    #rust#unicode#simd+2
  2. 백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 — 전시 아키텍처 개선기 (2/3)

    여기어때 전시개발팀이 숙소 상세(PDP) API를 해부한 결과, 코드상으로는 DB 호출 3번처럼 보이던 요청이 실제로는 MongoDB $lookup 체인으로 컬렉션을 19회 접근하는 구조였다. 이 트랜잭션 스크립트 방식의 핵심 문제는 "aggregation이 I/O를 가린다"는 점으로, 독립적인 쿼리 10개가 단일 파이프라인에 직렬화되어 병렬화 기회를 잃고, 가격 때문에 거의 안 바뀌는 이미지까지 매 요청마다 읽어야 하는 읽기 증폭이 발생했다. V3에서는 "조회 시점 조립"을 "쓰기 시점 사전 조립"으로 전환하고, 화면별로 복제되던 가격 계산 로직을 goodsprice 단일 모듈로 수렴했다. 4개 API(PLP/PDP/RDP/ILP)의 반복 마이그레이션은 Claude Code skill로 절차를 고정하고 쉐도잉 + 동일성 검증으로 안전망을 마련하는 방식으로 진행됐다.

    #architecture#migration#caching+2