실시간 CTR 파이프라인 성능개선기
들어가기
실시간 CTR 집계 파이프라인을 처음 구축했을 때는 Kafka, Flink, Redis, Serving API, ClickHouse를 모두 로컬에서 띄우는 구조였다. 실험 환경은 MacBook Air M1 · 8GB RAM이었다. 구성 자체는 동작했지만, 한 사이클을 돌릴 때마다 리소스 한계가 먼저 드러났다.
문제는 노트북 사양 자체보다 개인 프로젝트의 검증 규모에 비해 로컬 아키텍처가 너무 무거웠다는 점이었다. Redis와 Serving API까지 함께 유지하자 Flink에 줄 수 있는 메모리가 줄었고, 스키마나 집계 결과가 바뀔 때마다 수정해야 할 경로도 늘어났다.
그래서 장비를 바꾸는 대신 파이프라인 구성을 다시 설계했다. 이 글은 Redis와 Serving API를 덜어내는 과정에서 어떤 대안을 검토했고, ClickHouse 중심 구조로 바꾼 뒤 성능과 운영 복잡도가 어떻게 달라졌는지 정리한 기록이다.

1. 문제의 시작: 로컬 환경이 먼저 병목이 됐다
개인 프로젝트로 CTR 데이터 파이프라인을 구축하고 실험을 이어가던 중, Docker 구성이 먼저 부담으로 다가왔다. 한 사이클을 돌리려면 컨테이너를 빠짐없이 올려야 했고, 그때마다 발열과 메모리 사용량이 빠르게 증가했다.
기존 시스템 아키텍처는 이렇게 구성되어 있었다.

Kafka로부터 조회수와 클릭수 이벤트를 받아 집계하고 저장하는 과정에서 ClickHouse와 Redis에 데이터를 함께 사용했다. ClickHouse는 분석용으로, Redis + Serving API는 관련 부서에 데이터를 전달하는 용도로 두려 했다.
그런데 이렇게 구성해 보니 세 가지 문제가 생겼다.
- 리소스 부족: Redis + Serving API 컨테이너가 차지하는 메모리와 CPU 때문에 정작 중요한 Flink Job이 OOM으로 죽는 경우가 있었다.
- 관리 포인트 증가: 집계 결과 변경으로 스키마가 바뀌면 Flink를 수정하고, Redis 데이터를 초기화한 뒤, API 코드까지 수정해야 했다.
- 네트워크 오버헤드: 로컬 환경임에도 Flink -> Redis -> API 경로에서 불필요한 직렬화/역직렬화 비용이 계속 발생했다.
API를 조회하는 주요 사용자를 ML 팀으로 가정했는데, 사내 팀에서 ClickHouse로 직접 조회하는 것이 가능하다면 Redis와 Serving API를 제거할 수 있겠다고 판단했다.
이 생각을 가지고 있던 중, 대기업에서 근무하는 친구에게 구축한 시스템에 대한 피드백을 받을 기회가 생겼다. 그 과정에서 Redis와 Serving API가 꼭 필요한지, 이 시스템에서 고려하는 상품 수는 얼마인지, Redis HashTable 특성상 리사이징 작업이나 키 업데이트가 일어날 때 조회 요청이 동시에 발생하면 어떤 문제가 생길 수 있는지 질문을 받았다. 이 피드백을 계기로 Redis의 존재 이유를 다시 검토했다.
설계 의도를 함께 논의했고, 결론적으로 두 가지 대안이 나왔다.
- 지연 없이 적재 가능한 스토리지 포맷 사용 (예: Iceberg)
- ClickHouse의 Materialized View 활용 (약어로 MV라고 많이 사용함.)
2. 가설과 설계: Redis + Serving API를 제거한다
물론 Redis는 1ms 수준으로 빠르지만, 프로젝트의 목표와 제약을 다시 생각했다.
- 목표: 초당 10만건의 이벤트를 유실 없이 처리할 수 있는 시스템 구축
- 제약: MacBook Air M1, 8GB 기반의 로컬 개발환경, 리소스 최소화
- 가설: Redis 대신 다른 저장 경로를 사용해도 실시간성을 충분히 보장하면서 복잡도를 낮출 수 있다.
또한 아키텍처 관점에서 보면, Serving Layer와 Storage Layer를 분리하는 관행이 시스템 초기 단계에서는 불필요하게 결합도만 높일 수 있다고 봤다.
3. 대안 선택: ClickHouse Materialized View
새로운 스토리지 포맷을 도입하는 것도 좋은 방법이지만, 우선 로컬 제약을 줄이는 것이 더 중요했다. 그래서 기존 구성에 이미 포함된 ClickHouse를 활용하기로 결정했다.
ClickHouse의 Materialized View를 활용하면 별도 애플리케이션 로직 없이도 데이터가 들어오는 순간 “미리 계산된 상태”를 만들 수 있다. 즉, ClickHouse 자체가 캐시 서버 역할까지 수행하는 구조가 된다.
데이터 흐름

적용한 3가지 View 전략
ctr_latest_view: Redis의 Key-Value 조회를 대체. 항상 최신 CTR 상태만 유지 (ReplacingMergeTree).ctr_ml_view: 복잡한 집계 쿼리를 대체. 1분 단위로 미리 합계를 계산해 둠 (AggregatingMergeTree).- 효과: API 서버에서 처리하던
Merge로직이나Filtering로직이 모두 SQL 레벨로 내려가면서 API 컨테이너 자체가 불필요해짐.
4. 가설 검증
실제로 Redis와 Serving API 컨테이너를 제거하고 ClickHouse로 통합하자 로컬 실행 안정성과 처리량이 함께 개선됐다.

성능 검증: 로컬 벤치마크
성능을 측정하기 위해 테이블 캐시를 예열한 뒤 10회 조회하여 평균치를 계산했다. 측정 도구는 TABiX를 사용했다.
ex. SELECT count() FROM ctr_results_raw;- 단건 조회: 0.00 ~ 0.01s (Redis의 1ms보다는 느리지만, 대시보드용으로는 충분히 빠름)
- 집계 조회: 0.02 ~ 0.05s (기존 Python API에서 직접 집계할 때보다 오히려 10배 이상 빠름)
정확한 밀리초 단위까지 확인하고 싶었지만, 도구의 한계로 10ms 단위까지만 확인할 수 있었다.
아키텍처 단순화 효과

- Resource: Redis와 API 서버가 사용하던 약 1GB의 메모리를 확보해 Flink에 더 할당할 수 있었다.
- SQL 중심: 모든 데이터를 SQL 하나로 조회할 수 있어 디버깅 경로가 단순해졌다.
- Deployment:
docker-compose.yml과 프로젝트 내부 코드가 줄어들어 유지보수가 쉬워졌다.
5. 아키텍처 변경 전후 비교표
| 항목 | 기존 구조 (Redis + API) | 개선 구조 (ClickHouse MV) |
|---|---|---|
| 주요 저장소 | Redis + ClickHouse | ClickHouse 단일화 |
| API 필요 여부 | 필수 | 불필요 |
| 리소스 사용량 | 1GB 추가 | 0GB (삭제) |
| 쿼리 복잡도 | Python API 로직 필요 | SQL로 단순화 |
| 지연 시간 | 1~3ms | 10~30ms |
| 처리량 | ~4k | 평균 20k, 스파이크 55k |
6. 남은 기준
처음에는 MacBook Air M1 · 8GB RAM이라는 환경 때문에 생긴 문제라고 생각했다. 초당 833건의 데이터를 처리하는 상황에서도 발열과 메모리 압박이 컸기 때문이다.
하지만 구조를 다시 살펴보며 최적화를 진행하자 처리량은 초당 4천 건까지 늘어났다. 이후 Redis + Serving API를 제거해 리소스를 확보하고 Flink 앱에 자원을 더 할당하자 평균 1~2만 건을 처리했고, 약 5.5만 건의 스파이크 트래픽도 안정적으로 소화할 수 있었다.

처음부터 자원이 넉넉했다면 이 정도로 구조를 의심하지 않았을 가능성이 높다. 제한된 로컬 환경은 단순한 불편함이 아니라, Flink 병렬도와 상태 관리, 저장소 선택 기준을 더 구체적으로 검증하게 만든 제약이었다.
아직 이 시스템이 완성됐다고 보지는 않는다. 다음 기준은 최소 10만 건의 스파이크 트래픽을 견딜 수 있도록 병목을 더 좁히고, 성능 개선이 어떤 구조적 선택에서 나왔는지 계속 설명 가능한 상태로 유지하는 것이다.