Chapter 6

광고 클릭 이벤트 집계

  • 6.1 문제 이해 및 설계 범위 확정
  • 6.2 개략적 설계안 제시
  • 6.3 상세 설계
  • 6.4 마무리

광고 클릭 이벤트 집계 시스템은 구글이나 페이스북 같은 광고 플랫폼에서 "이 광고가 몇 번 클릭됐나"를 실시간으로 세는 거야. 단순해 보이지만, 초당 수백만 건의 클릭을 정확하게 세면서 중복도 처리해야 하니 설계가 만만치 않지.

핵심 기능 두 가지야.

  • 지난 M분 동안 광고 ad_id별 클릭 수를 집계해서 반환
  • 지난 M분 동안 가장 많이 클릭된 광고 top N을 반환

비기능 요구사항이 까다로워.

  • 정확히 한 번 처리(exactly-once) — 같은 클릭을 두 번 세거나, 클릭을 빠뜨리면 안 돼. 광고비 정산에 쓰이니까 돈 문제거든.
  • 실시간에 가까운 집계 — 광고주가 몇 분 이내에 집계 결과를 볼 수 있어야 해
  • 높은 처리량 — 초당 수백만 건의 클릭 이벤트

규모를 잡아보면 — DAU 10억 명, 하루 클릭 수 약 10억 건이면 평균 초당 약 1만 건, 피크 시 5배로 초당 5만 건. 어마어마한 수준은 아니지만, 정확성 때문에 난이도가 올라가지.

원시 데이터는 (ad_id, click_timestamp, user_id, ip, country) 같은 형태야. 집계 결과는 (ad_id, window_start, window_end, count).

방법 1: 원시 데이터를 DB에 넣고 질의 시점에 집계 — SQL로 GROUP BY하면 돼. 간단하지만, 데이터가 쌓이면 질의가 느려지고 DB에 부하가 심해. 대규모에서는 비현실적이지.

방법 2: 스트림 처리로 실시간 집계 — 클릭 이벤트가 들어오는 즉시 메모리에서 집계하고, 결과만 DB에 저장. 이 방식이 이 장의 핵심이야.

개략적 아키텍처를 보면:

클릭 이벤트 → 메시지 큐(카프카) → 집계 서비스 → 집계 결과 DB
  1. 광고 클릭이 발생하면 이벤트가 카프카 토픽에 들어간다
  2. 집계 서비스(Flink, Spark Streaming 등)가 카프카에서 이벤트를 꺼내서 실시간 집계
  3. 집계 결과를 DB에 저장
  4. 질의 서비스가 DB에서 결과를 읽어 반환

실시간 집계에서 "지난 M분"을 어떻게 정의하느냐가 중요해. 윈도우(window) 개념이 쓰이지.

텀블링 윈도우(Tumbling Window) — 고정 크기 윈도우가 겹치지 않게 쭉 이어져. 예: 1분짜리 윈도우면 0:00-0:01, 0:01-0:02... 각 윈도우는 독립적으로 집계.

슬라이딩 윈도우(Sliding Window) — 윈도우가 겹쳐. "최근 5분"이라고 하면 현재 시각 기준으로 과거 5분을 보는 건데, 매 초마다 윈도우가 1초씩 밀리지.

세션 윈도우(Session Window) — 이벤트 간 간격이 일정 시간 이상 벌어지면 새 윈도우. 사용자 행동 분석에 유용하지.

광고 클릭 집계에서는 주로 텀블링 윈도우를 써. 1분 단위로 집계하고, 더 긴 기간은 1분 집계를 합산해서 구하지.

이게 이 장의 핵심 난이도야. "정확히 한 번(exactly-once)"은 세 가지 문제를 풀어야 해.

1. 중복 이벤트 방지

같은 클릭 이벤트가 카프카에 두 번 들어갈 수 있어. 네트워크 재시도 등으로. 이를 막으려면 각 이벤트에 고유 ID를 부여하고, 집계 서비스에서 이미 처리한 ID인지 확인하지. 분산 환경에서 이 중복 체크를 효율적으로 하려면 블룸 필터Redis의 Set 같은 자료구조를 활용해.

2. 집계 서비스 장애 시 데이터 유실 방지

집계 서비스가 카프카에서 이벤트를 가져와서 메모리에서 집계하다가 죽으면? 메모리의 집계 상태가 날아가지. 이를 방지하려면:

  • 카프카 오프셋을 집계 결과를 DB에 쓴 후에 커밋해. 죽으면 마지막 커밋된 오프셋부터 다시 읽으니까 데이터 유실은 없어.
  • 대신 중복 처리가 될 수 있어 — 집계 결과를 DB에 썼는데 오프셋 커밋 전에 죽으면. 이건 DB 쓰기를 **멱등적(idempotent)**으로 만들어서 해결하지. 같은 윈도우의 같은 집계를 다시 써도 결과가 달라지지 않게.

3. 다운스트림 중복 방지

집계 결과가 downstream 시스템(광고비 정산 등)으로 한 번만 전달돼야 해. 여기도 멱등성 키를 활용하지.

대규모에서 집계 서비스를 하나의 노드로 감당할 수 없으니 맵-리듀스(Map-Reduce) 패턴을 써.

Map 단계 — 각 노드가 자기에게 할당된 카프카 파티션의 이벤트를 읽어서 (ad_id, count) 형태로 변환.

Aggregate 단계 — 같은 ad_id를 가진 중간 결과를 하나의 노드로 모아서 합산. ad_id를 해싱해서 어느 Aggregate 노드로 보낼지 결정하지.

이 구조면 Map 노드와 Aggregate 노드를 각각 독립적으로 확장할 수 있어.

아무리 잘 설계해도 버그나 장애로 집계가 틀어질 수 있어. 그래서 원시 데이터를 별도로 저장해두고, 주기적으로 원시 데이터에서 다시 집계한 결과와 실시간 집계 결과를 비교하는 재조정(reconciliation) 과정이 필요하지.

차이가 발견되면 원시 데이터 기반의 결과로 보정해. 이게 "소스 오브 트루스는 원시 데이터"라는 원칙이야.

특정 광고(바이럴 광고 등)가 엄청나게 많은 클릭을 받으면 해당 ad_id를 처리하는 Aggregate 노드에 부하가 집중돼. 이걸 핫스팟(hot spot) 문제라고 하지.

해결 방법 — 핫한 ad_id를 감지하면 해당 키에 랜덤 접미사를 붙여서 여러 Aggregate 노드로 분산시켜. 최종 집계 시 이 분산된 부분 결과를 다시 합산. 추가적인 집계 단계가 생기지만 핫스팟을 해소할 수 있지.

광고 클릭 집계는 "정확하게 세기"가 핵심인 시스템이야. 카프카 + 스트림 처리 조합으로 실시간 집계를 하되, 멱등성과 트랜잭션으로 정확히 한 번 처리를 보장하고, 원시 데이터 기반 재조정으로 최종 보루를 만드는 거지.


정리

6장 읽고 기억할 거 세 가지:

  1. 실시간 집계는 카프카 + 스트림 처리(Flink 등) 조합이 표준이야. DB에 원시 데이터를 넣고 GROUP BY 하는 건 대규모에서 비현실적이지.
  2. 정확히 한 번 처리는 중복 감지 + 멱등적 쓰기 + 오프셋 관리의 조합으로 달성해. 어느 한 곳이 빠져도 정확성이 깨지거든.
  3. 원시 데이터는 반드시 별도로 저장해서 재조정(reconciliation)에 사용해. 실시간 집계와 배치 집계를 비교해서 차이를 보정하는 게 최종 안전장치야.