광고주가 화났다

"하루 3번까지로 캡을 걸었는데, 우리 광고가 한 사용자한테 열 몇 번씩 뜬다"는 항의가 들어왔다. 설정은 분명 3번이었다. 코드도 멀쩡했다. 그런데 실제 노출은 캡을 한참 넘기고 있었다.

범인은 산수였다. 광고 서버가 한 대가 아니라 수십 대였고, 사용자 요청은 그 중 아무 서버로나 흩어졌다. 각 서버가 자기 메모리에서 "이 사용자에게 3번까지" 세고 있으니, 서버가 10대면 같은 광고가 최대 30번 나갈 수 있었다. 캡 자체가 서버 수만큼 곱해져 새고 있던 것이다.

단일 서버면 이런 일은 없다. 메모리에 (user, ad) → count 하나 두고 증가시키면 끝이다. 문제는 우리가 서버 한 대로 굴러가는 회사가 아니라는 것이었다.

  • 광고 서버 수십 대 — 요청이 아무 서버로나 감
  • 초당 수만~수십만 요청
  • 카운트는 모든 서버가 같은 값을 공유해야 캡이 의미를 가짐

해법은 자명해 보였다. 모든 서버가 공유하는 카운터 하나를 두면 된다. 그런데 매 요청마다 중앙 저장소를 읽고 쓰는 순간, 이번엔 그 카운터가 전체 광고 서빙의 병목이 됐다. 정확성을 사면 비용이 따라왔다. 여기서부터가 진짜 문제였다.

정확성을 얼마나 살 것인가

광고주에게 다시 답을 주려면 먼저 내가 답해야 했다. "정확히 3번"을 보장하는 건 얼마나 비싼가, 그리고 우리는 정말 "정확히"가 필요한가. 캡을 거는 쪽 입장에서 보면 광고는 대개 "정확히 N번"이 아니라 "대략 N번"이면 매출도 사용자 경험도 흔들리지 않는다. 그래서 선택지는 단순한 on/off가 아니라, 정확성을 어디까지 사고 어디서부터 근사로 갈아탈지의 스펙트럼이었다. 그 스펙트럼을 우리가 실제로 밟은 순서대로 적는다.

1단계 — 중앙 Redis (정확, 비쌈)

매 노출마다 Redis INCR.

INCR freq:{user}:{ad}:{date}
EXPIRE freq:{user}:{ad}:{date} 86400
  • 정확함 (모든 서버가 같은 카운터)
  • 비용: 매 요청 Redis 왕복. 초당 수만이면 Redis가 핫스팟.

소규모거나 정확성이 매출에 직결되면 이 단계.

2단계 — 로컬 캐시 + 주기 동기화 (근사)

각 서버가 로컬 카운트 + 주기적으로 중앙에 flush.

  • 비용 ↓ (Redis 왕복이 매 요청 → 주기적)
  • 부정확 ↑ (flush 사이의 윈도우에서 over-delivery)

cap이 3인데 서버 10대가 각자 로컬로 세다 flush하면 일시적으로 3 초과 가능. "대략 3번"으로 충분하면 OK.

3단계 — 확률적 카운팅 (대규모 근사)

수억 사용자 × 수만 광고면 (user, ad) 카운터 자체가 메모리 폭발.

  • HyperLogLog: 고유 노출 카운트 근사 (오차 ~2%)
  • Count-Min Sketch: 빈도 근사, 고정 메모리
  • 확률적 capping: cap 근처에서 확률적으로 drop (정확한 카운트 없이)

정확도를 일부 포기하고 메모리를 고정. 대규모 플랫폼의 현실적 선택.

4단계 — 계층 (hot/cold 분리)

  • hot: 활성 캠페인은 정확하게 (Redis)
  • cold: 저빈도 캠페인은 근사 (로컬+flush)

캠페인 특성에 따라 단계를 섞는다. 모든 캠페인에 같은 정확도를 적용하지 않음.

시간 윈도우 처리

"하루 3번"의 "하루"도 함정.

  • 고정 윈도우: 자정 리셋. 23:59에 3번 + 0:01에 3번 = 2분에 6번 노출 (경계 폭발)
  • 슬라이딩 윈도우: 최근 24시간. 정확하지만 비쌈 (타임스탬프 목록 유지)
  • 슬라이딩 윈도우 근사: 시간 버킷 + 가중치. 비용/정확성 절충

frequency capping에서 고정 윈도우의 경계 폭발은 사용자 경험을 해친다(짧은 시간 도배). 슬라이딩 근사가 보통 적절.

eventual consistency 받아들이기

분산 frequency capping의 핵심 통찰: 약간의 over-delivery를 받아들인다.

  • cap 3에 실제 3.2번 평균 노출 → 매출/UX 영향 작음
  • 정확히 3번을 위해 매 요청 강한 일관성 → 비용 폭발

"정확히 N번"이 비즈니스 요구인지 "대략 N번"이면 되는지를 먼저 확정. 대부분 후자다. 강한 일관성은 비용이 정당화될 때만.

함정

  • 단일 서버 코드를 분산에 그대로: 로컬 카운터는 서버 수만큼 cap 초과. 공유 카운터 필요.
  • 매 요청 중앙 Redis: 초당 수만이면 Redis 핫스팟. 로컬 캐시 + flush 고려.
  • 고정 윈도우 경계 폭발: 자정 리셋이 경계에서 도배. 슬라이딩 윈도우.
  • 모든 캠페인 같은 정확도: hot/cold 분리로 비용 차등.
  • "정확히 N번" 과잉 요구: 대부분 "대략"이면 충분. over-delivery 허용 범위를 비즈니스와 합의.
  • 카운터 TTL 누락: 날짜별 키가 영원히 남으면 메모리 증가. EXPIRE 필수.

핵심

광고주에게는 결국 이렇게 답했다. "정확히 3번이 아니라 평균 3번 안팎으로 맞추겠다. 그 대신 광고는 멈추지 않고 비용도 안 터진다." 그게 받아들여졌고, 캡은 잘 동작했다.

엔지니어링 문제처럼 보이던 "캡이 샌다"는 사실 비즈니스 질문이었다 — 정확히 N번이 정말 필요한가, 아니면 대략 N번이면 되는가. 답이 후자인 순간, 약간의 over-delivery를 허락하는 대가로 비용은 비선형으로 줄어든다. 분산 시스템에서 가장 비싼 요구사항은 "강한 일관성"이 아니라 검증 안 된 채로 들고 있는 '정확히'라는 단어다.

관련

/notes/ad-metric-traps — 노출 카운트의 정의 문제 /essays/two-sided-market-decisions — over-delivery가 매체·광고주에 미치는 영향