채널톡 CRM 마케팅의 고객 타게팅을 커머스 행동 데이터까지 넓히기 위해 ClickHouse를 도입한 과정과, 그 과정에서 부딪힌 한계를 함께 적었다. "A 상품 쿠폰을 가진 고객이 장바구니에 A를 담았지만 아직 구매하지 않은 경우" 같은 조건을 RDBMS로 풀기에는 데이터량이 감당하기 어려웠다. 고객사들이 발급한 쿠폰만 최소 20억 건 이상으로 추정된다.
핵심 포인트- 워크로드가 쓰기와 읽기 비율 100:1에 append-only 성격이고, 웹훅으로 받는 주문 정보는 LWW 정도의 일관성만 지원해도 되며 준 실시간 처리로 충분해 OLAP 전환을 내포하고 있었다.
- ClickHouse를 고른 이유는 성숙도와 레퍼런스, 그리고 한 달 이내에 프로덕션 안정성을 확보해야 하는 상황에서 매니지드 서비스로 빠르게 갈 수 있다는 판단이었다.
- MergeTree의 단순함이 매력이자 단점이다. ReplacingMergeTree는 중복을 즉시 제거하지 않고 Merge 시점에 정리해, 매 쿼리마다 중복 제거 처리를 얹어야 해 성능을 잡아먹는다.
- ClickHouse는 대규모 JOIN에 강한 시스템이 아니지만, 실제 쿼리 대상이 원본 수십억 건이 아니라 필터링된 고객 집합이라 감당할 수 있었다.
- 정말 어려웠던 것은 Point Query다. granule 단위로 읽는 구조라 아이템 하나를 읽기 위해 큰 범위를 읽는다.
상세 정리- 제품 맥락: CRM 마케팅은 채널톡·문자·카카오 메시지를 발송해 상담으로 쌓인 고객을 다시 온사이트로 끌어들이고, 전환되면 그 행동 데이터가 다시 채널톡에 반영돼 더 나은 상담으로 이어지는 선순환을 만든다.
- 한계: 여러 빌더사를 통해 커머스를 연동할 수 있지만 저장 구조의 한계로 고객이 장바구니에 무엇을 담았는지, 어떤 쿠폰을 갖고 있는데 안 썼는지를 타게팅하기 어려웠다.
- 기존 구성: 기본은 RDBMS이고 특정 쿼리를 위해 원본 테이블의 보조 저장소를 따로 구성해 쿼리하기도 했다.
- RDBMS로 풀 때의 문제: 이 쿼리 형식을 최적화하기에는 부담이 크고 I/O 병목에 따라 꼬리 지연이 길어져 제품 안정성이 떨어진다.
- 워크로드 성격 정리: append-heavy하고 시의성에 따라 특정 시간대나 블랙 프라이데이 같은 날짜에 트래픽이 몰릴 수 있으며, 모든 이벤트와 히스토리를 하나의 테이블로 비정규화하기 어렵다.
- OLTP와 OLAP 구분: OLTP는 은행 송금이나 주문 처리처럼 짧고 빈번한 읽기/쓰기가 중심이고 일관성이 중요한 반면, OLAP은 준 실시간 데이터 쿼리에 초점이 있고 트랜잭션이나 개별 row 업데이트에는 비교적 약하다.
- 저자는 만능 데이터베이스가 없다는 점을 짚는다. 높은 TPS가 필요하면 Redis의 인메모리 강점을, 단일 노드로 안 되면 파티셔닝을, 그 위에 트랜잭션까지 보장하려면 분산 트랜잭션이라는 복잡한 문제를 풀어야 한다.
- MergeTree의 동작: 매 쓰기를 디스크에 작은 Part로 바로 적재하고 주기적으로 병합하는 방식이라 append 성격 쓰기에 유리하다. 기본 MergeTree는 Update 시 in-place가 아닌 mutation으로 Part가 재작성되는 문제가 있어 ReplacingMergeTree가 제안됐다.
- ReplacingMergeTree의 대가: 중복 제거가 Merge 시점에 일어나므로 매 쿼리에서 특정 아이템의 중복된 데이터가 조회될 수 있다. 예컨대 특정 주문이 환불이면서 구매 완료인 상태로 동시에 존재할 수 있다.
- 빈번한 업데이트는 주문 데이터에서 발생한다. 주문은 취소나 환불로 언제든 바뀔 수 있지만, 주문 확정 등 특정 시점 이후에는 상태가 변경되지 않는다는 성질이 있다.
- 그래서 Update 가능한 영역을 분리하고 Cold Storage와 함께 쿼리하는 방식으로 개선했다.
- JOIN 제약: 복잡한 대규모 JOIN은 다른 시스템이 더 잘 지원하며, ClickHouse는 브로드캐스트 가능한 크기의 right-hand side 테이블이나 양쪽이 같은 키로 샤딩되지 않은 경우에 대한 권장 패턴을 따로 안내한다.
- 감당 가능했던 이유: 원본 주문·쿠폰·장바구니는 수억에서 수십억 건이지만 실제로 쿼리하는 것은 특정 조건을 만족하는 고객의 집합이다. 필터링된 집합 간의 메모리 연산이라면 커버 가능했고 실제 지연도 수백 ms 수준에서 관리됐다.
- 다만 JOIN은 시스템 자원을 많이 소모하는 연산이라 개선이 필요하다고 인정한다.
- Point Query 문제: 최종 마케팅 발송 시 처음 세그먼트에 등록된 고객이 여전히 포함되는지 확인하는 Membership Check가 있는데, 이는 "이 유저가 이 세그먼트에 속해 있나"를 묻는 형태다.
- ClickHouse는 기본 8192개 아이템을 담는 granule 단위로 데이터 범위를 파악해 skip하거나 읽는다. 수십억 건 스캔에는 효율적이지만, 아이템 하나를 읽기 위해 큰 범위의 파일을 읽어야 한다.
- 해법: Redis를 함께 운영하는 하이브리드 아키텍처를 택했다. 동기화 과정의 엔지니어링 비용과 복잡도가 올라가는 선택이었지만 레퍼런스를 찾기 쉬웠고 Hot/Cold 분리와 같은 성격의 보조 저장소 유지는 어렵지 않은 결정이었다고 밝힌다.
- 회고: ClickHouse의 단순 명료함이 이점이자 트레이드오프였고, 한계는 도입 전부터 인지하고 있었다. 그 단순함 때문에 하이브리드 설계가 필요해져 전체 아키텍처 복잡도는 오히려 올라가는데, 이는 복잡도를 어디에 둘 것이냐의 선택 영역이라고 정리한다.
- 이후 계획: 업데이트가 매우 빈번한 워크로드에도 Analytics를 제공해야 하는 요구를 반영하기 위해 커스텀 리포트에서는 다른 엔진을 도입했고, 이 글의 Hot Storage 개선도 이어서 다룰 예정이라고 예고한다.
왜 읽나RDBMS로는 감당 안 되는 세그먼트 조건을 OLAP으로 옮기려는 팀이 ClickHouse의 구조적 이점과 ReplacingMergeTree·JOIN·Point Query라는 세 한계를 미리 가늠할 수 있다.