Day 5: Redis 캐싱으로 밀리초 단위 광고 서빙 구현하기
Day 5: Redis 캐싱으로 밀리초 단위 광고 서빙 구현하기
오늘 한 일 요약
오늘은 실시간 광고 시스템에서 가장 중요한 “빠른 응답속도”를 달성하기 위해 Redis 캐싱 레이어를 구축했습니다. 결과적으로 ClickHouse 대비 72배 빠른 1.48ms의 응답속도를 달성했습니다!
왜 이렇게 복잡한 시스템이 필요한가요?
실제 광고 입찰은 “눈 깜빡일 시간”에 일어납니다
앱에서 광고가 뜨는 과정을 상상해보세요:
- 사용자가 앱을 열면 (0ms)
- 광고 서버에 “지금 보여줄 광고 추천해줘!” 요청 (10ms)
- 서버가 “이 사용자는 미국, 배너 광고 CTR 5%, 입찰가 $2” 계산 (??ms)
- 광고를 화면에 표시 (50ms)
문제: 만약 3번이 100ms 걸리면? → 사용자는 이미 떠났습니다.
실제 광고 입찰(RTB)은 100ms 이내에 모든 게 끝나야 합니다. 그래서 데이터 조회를 극도로 빠르게 만들어야 하는 거죠.
🏗️ 우리 시스템의 전체 구조 (쉽게 설명)
우리는 총 5개의 프로그램을 동시에 실행하고 있습니다. 각각이 서로 다른 역할을 합니다.
[데이터 생성기] → [Kafka] → [Spark Streaming] → [Redis + ClickHouse] → [FastAPI]
(터미널1) (저장소) (터미널2) (데이터베이스) (API서버)
1️⃣ 데이터 생성기 (터미널 1)
역할: 가짜 광고 이벤트를 계속 만들어냅니다.
# ad_log_generator.py가 하는 일
매초마다:
"미국 사용자가 배너 광고를 클릭했어요!" (JSON)
"한국 사용자가 리워드 비디오를 봤어요!" (JSON)
→ Kafka로 전송
왜 필요한가요? 실제 서비스라면 진짜 사용자 데이터가 들어오겠지만, 우리는 포트폴리오니까 “시뮬레이션”이 필요합니다.
실행 명령어:
python data-generator/ad_log_generator.py
# 출력: ✅ Sent 1,000 events | Rate: 83 events/sec
2️⃣ Kafka (백그라운드)
쉽게 말하면: “택배 보관함”입니다.
상황 비유:
- 데이터 생성기 = 택배 기사 (광고 이벤트를 계속 배달)
- Kafka = 택배 보관함 (
ad-events라는 이름의 보관함) - Spark Streaming = 택배를 꺼내가는 고객
왜 Kafka를 쓰나요? 데이터 생성기가 초당 83개 이벤트를 만드는데, Spark가 잠깐 멈추면? → Kafka가 데이터를 “보관”해둡니다. 안 잃어버려요!
실제 동작:
# Kafka에 들어있는 데이터 확인
docker exec adtech-kafka kafka-console-consumer \
--topic ad-events --from-beginning --max-messages 3
# 출력:
{"event_id": "abc123", "country": "US", "event_type": "click", ...}
{"event_id": "def456", "country": "KR", "event_type": "impression", ...}
{"event_id": "ghi789", "country": "JP", "event_type": "conversion", ...}
3️⃣ Spark Streaming (터미널 2)
쉽게 말하면: “실시간 계산기”입니다.
하는 일:
# spark_processor_with_redis.py가 하는 일
1. Kafka에서 이벤트 꺼내기 (스트리밍)
2. 5분마다 집계:
- "지난 5분간 미국 배너 광고 몇 번 봤지?"
- "그 중 몇 번 클릭했지?"
- CTR = (클릭 / 노출) × 100% 계산
3. 결과를 Redis와 ClickHouse에 동시 저장
왜 “5분 윈도우”인가요? 실시간이라고 해서 매 1초마다 계산하면 CPU가 폭발합니다. 5분 정도가 “적당히 실시간 + 효율적”입니다.
실행 명령어:
docker exec spark-master /opt/spark/bin/spark-submit \
--master "local[2]" \
--packages org.apache.spark:spark-sql-kafka-0-10_2.12:3.5.0 \
/opt/spark-apps/spark_processor_with_redis.py
# 출력:
✅ Batch 1: Redis에 105개 메트릭 저장됨
✅ Batch 1: ClickHouse에 105개 메트릭 저장됨
실제 계산 결과 예시:
+-------+--------------+-----------+------+-------+-------+
|country|ad_format |impressions|clicks|ctr |ecpm |
+-------+--------------+-----------+------+-------+-------+
|US |rewarded_video|358 |153 |42.74% |$26.27 |
|KR |banner |397 |24 |6.05% |$1.71 |
+-------+--------------+-----------+------+-------+-------+
4️⃣ Redis + ClickHouse (저장소)
쉽게 말하면: 각각 “메모”와 “일기장”입니다.
Redis (메모판)
- 특징: 초고속이지만 메모리에만 저장 (휘발성)
- 용도: 최근 5분 데이터만 저장 (TTL 1시간 후 자동 삭제)
- 속도: 1.48ms ⚡
비유: “미국 리워드 비디오 CTR이 얼마였지?” → 메모판에서 바로 확인 (1초도 안 걸림)
ClickHouse (일기장)
- 특징: 느리지만 디스크에 영구 저장
- 용도: 몇 달치 데이터 분석용
- 속도: 107ms 🐢
비유: “지난달 전체 광고 수익이 얼마였지?” → 일기장 뒤져서 계산 (좀 걸림)
Redis 데이터 확인:
# Redis CLI 접속
docker exec -it adtech-redis redis-cli
# 저장된 키 확인
127.0.0.1:6379> KEYS metrics:*
1) "metrics:US:rewarded_video:5m"
2) "metrics:KR:banner:5m"
...21개
# 실제 데이터 조회
127.0.0.1:6379> GET metrics:US:rewarded_video:5m
{
"impressions": 358,
"clicks": 153,
"ctr": 42.74,
"ecpm": 26.27
}
5️⃣ FastAPI (API 서버)
쉽게 말하면: “고객 상담 창구”입니다.
하는 일:
# fastapi_metrics_api.py가 하는 일
고객: "미국 리워드 비디오 성과 알려줘!"
API: Redis에서 즉시 조회 → 1.48ms 만에 응답
왜 FastAPI를 쓰나요?
- Python으로 빠르게 만들 수 있음
- 자동으로 API 문서(
/docs) 만들어줌 - 비동기 처리 지원 (빠름)
API 엔드포인트:
# 1. 특정 국가/광고포맷 조회
curl http://localhost:8000/metrics/US/rewarded_video
# 응답: {"ctr": 42.74, "response_time_ms": 7.36}
# 2. 전체 메트릭 조회
curl http://localhost:8000/metrics/all
# 응답: 21개 국가/포맷 조합 데이터
# 3. 성능 벤치마크
curl http://localhost:8000/benchmark/US/rewarded_video
# 응답:
{
"redis": {"response_time_ms": 1.48},
"clickhouse": {"response_time_ms": 107.13},
"comparison": {"redis_faster_by": "72.4x"}
}
🔥 핵심 성과: 72배 빠른 응답속도!
Before (ClickHouse만 사용)
사용자 요청 → ClickHouse 쿼리 (107ms) → 응답
- 문제: 107ms는 광고 입찰에 너무 느림
- 결과: 실시간 서비스 불가능
After (Redis 캐싱 추가)
사용자 요청 → Redis 조회 (1.48ms) → 응답
- 개선: 72배 빠름!
- 결과: 실시간 광고 입찰 가능
🧠 왜 터미널을 여러 개 써야 하나요?
답: 각 프로그램이 “계속 실행되어야” 하기 때문입니다.
터미널 1: 데이터 생성기
python ad_log_generator.py
# 이 프로그램은 "무한 루프"로 계속 데이터를 만듭니다
# Ctrl+C 누르기 전까지 멈추지 않음
터미널 2: Spark Streaming
spark-submit spark_processor_with_redis.py
# 이 프로그램도 "무한 루프"로 계속 Kafka를 읽습니다
# Ctrl+C 누르기 전까지 멈추지 않음
터미널 3: API 테스트
curl http://localhost:8000/metrics/US/banner
# 이건 "일회성" 명령어
# 실행하면 결과 받고 끝
비유:
- 터미널 1, 2 = 공장에서 계속 일하는 기계 (멈추면 안 됨)
- 터미널 3 = 손님이 주문하는 카운터 (필요할 때만 사용)
📊 실제 데이터 흐름 (타임라인)
00:00초
데이터 생성기: "미국 사용자가 배너 광고 봤어요!" → Kafka
00:01초
데이터 생성기: 83개 이벤트 더 생성 → Kafka
00:05초 (5분 윈도우 완료)
Spark: "자, 지난 5분 계산해볼까?"
- 미국 배너: 노출 1,234회, 클릭 45회
- CTR = 45/1234 = 3.64%
→ Redis 저장 (TTL 1시간)
→ ClickHouse 저장 (영구)
00:06초
고객: "미국 배너 CTR 알려줘!" (API 요청)
FastAPI: Redis에서 조회 (1.48ms) → "3.64%입니다!" 응답
🎯 주요 기술 용어 정리
Kafka
- 무엇: 분산 메시지 큐 (택배 보관함)
- 왜 씀: 데이터 유실 방지, 비동기 처리
- 실무 사용: 카카오톡 메시지, 넷플릭스 시청 기록
Spark Streaming
- 무엇: 실시간 대용량 데이터 처리 엔진
- 왜 씀: 초당 수천~수만 건 이벤트 집계
- 실무 사용: 우버 실시간 요금 계산, 쿠팡 재고 추적
Redis
- 무엇: 인메모리 키-값 저장소 (초고속 캐시)
- 왜 씀: 밀리초 단위 응답 필요할 때
- 실무 사용: 쿠팡 장바구니, 배민 실시간 주문
ClickHouse
- 무엇: 컬럼 기반 분석용 DB (빅데이터 저장)
- 왜 씀: 수억 건 데이터 집계 쿼리
- 실무 사용: 야놀자 예약 분석, 당근마켓 조회수 통계
FastAPI
- 무엇: Python 웹 프레임워크
- 왜 씀: 빠르게 REST API 만들기
- 실무 사용: ML 모델 서빙, 내부 백오피스 API
💡 AdTech에서 이 시스템이 중요한 이유
실제 광고 입찰 시나리오
1. 사용자가 유튜브 앱 실행 (0ms)
2. "광고 보여줘!" 요청 전송 (5ms)
3. 광고 입찰 시작:
┌─────────────────────────────┐
│ 1000개 광고주가 동시 입찰 │ ← 우리 시스템이 여기서 작동!
│ - 각자 "얼마 줄까?" 계산 │
│ - 과거 CTR 데이터 참고 │ ← Redis에서 1.48ms에 조회
│ - 최고가가 낙찰 │
└─────────────────────────────┘
4. 낙찰된 광고 표시 (50ms)
만약 Redis 없이 ClickHouse만 쓴다면?
3번 단계에서 107ms 소요
→ 총 시간: 5 + 107 + 50 = 162ms
→ 광고 입찰 제한시간 100ms 초과!
→ 입찰 실패, 수익 0원
Redis 있으면?
3번 단계에서 1.48ms 소요
→ 총 시간: 5 + 1.48 + 50 = 56.48ms
→ 여유있게 입찰 성공!
→ 광고 수익 발생 💰
🔧 트러블슈팅: 겪었던 문제들
문제 1: Kafka 토픽이 없어요!
에러: UnknownTopicOrPartitionException
원인: 데이터 생성기를 안 돌렸음 해결: 생성기 먼저 실행 → Kafka 토픽 자동 생성
문제 2: Spark 메모리 부족
에러: Block broadcast_4 does not exist
원인: local[*] (모든 CPU 사용) 설정이 과함
해결: local[2] (2코어만 사용)로 변경
📈 성능 측정 결과
API 응답 속도
| 엔드포인트 | 응답 시간 | 데이터 개수 |
|---|---|---|
/metrics/US/rewarded_video |
7.36ms | 1개 |
/metrics/CA/banner |
0.32ms | 1개 |
/metrics/all |
3.92ms | 21개 |
Redis vs ClickHouse 비교
| 저장소 | 평균 응답 시간 | 용도 |
|---|---|---|
| Redis | 1.48ms | 실시간 서빙 |
| ClickHouse | 107.13ms | 분석 쿼리 |
| 속도 차이 | 72.4배 | - |
🎓 배운 점
1. Hot/Cold Data 분리 패턴
- Hot Data (Redis): 최근 데이터, 자주 조회, 빠른 응답 필요
- Cold Data (ClickHouse): 오래된 데이터, 가끔 조회, 복잡한 분석
2. Cache-Aside 패턴
# 전통적인 방식 (느림)
def get_metrics(country, format):
return clickhouse.query(...) # 107ms
# Cache-Aside 패턴 (빠름)
def get_metrics(country, format):
data = redis.get(key) # 1.48ms
if data is None:
data = clickhouse.query(...)
redis.set(key, data, ttl=3600)
return data
3. Dual Write 전략
# Spark에서 동시 저장
def save_metrics(batch):
save_to_redis(batch) # 실시간용
save_to_clickhouse(batch) # 분석용
🚀 다음 단계 (Day 6 예고)
Day 5에서 “빠른 조회”를 완성했습니다. 이제 다음 중 하나를 선택할 수 있어요:
Option 1: 실시간 알림 시스템
CTR이 갑자기 떨어지면?
→ Slack으로 "미국 배너 CTR 50% 하락!" 알림
Option 2: Fraud Detection (사기 탐지)
같은 사용자가 1초에 100번 클릭?
→ "이상 패턴 감지!" 경고
Option 3: A/B Testing
배너 A vs 배너 B 중 어떤 게 나을까?
→ 통계적으로 비교
💼 포트폴리오 어필 포인트
면접에서 이렇게 말할 수 있어요:
“Redis 캐싱 레이어를 구축해서 광고 메트릭 조회 속도를 72배 개선했습니다. ClickHouse는 107ms 걸리던 쿼리가 Redis는 1.48ms로, sub-10ms 응답을 달성했습니다. 이를 통해 실시간 RTB(Real-Time Bidding) 시스템의 100ms SLA를 만족시킬 수 있었습니다.”
기술 스택:
- Kafka: 이벤트 스트리밍 (초당 83 events)
- Spark Streaming: 실시간 집계 (5분 윈도우)
- Redis: 인메모리 캐싱 (TTL 1시간)
- ClickHouse: 분석용 데이터 웨어하우스
- FastAPI: RESTful API 서버
📚 참고 자료
각 기술의 공식 문서
실무에서 이렇게 씁니다
- 카카오: Kafka로 하루 1조 건 메시지 처리
- 쿠팡: Redis로 실시간 재고 관리
- 당근마켓: ClickHouse로 로그 분석
🎉 마무리
오늘은 “빠르게 데이터를 조회하는 것”의 중요성을 직접 구현하며 배웠습니다.
AdTech 업계에서는 100ms가 매출에 직결됩니다. 우리가 만든 1.48ms 시스템은 실제 프로덕션에서도 충분히 쓸 수 있는 수준입니다!
다음 Day 6에서는 이 시스템을 더욱 고도화해봅시다! 🚀
프로젝트 저장소: [GitHub 링크] 작성일: 2025-12-18 소요 시간: Day 5 완료 (약 4시간)