고객 타게팅 기능에 OLAP 시스템을 적용하기까지 여정
Hammer • Backend Engineer
채넡톡 마케팅은 다양하고 정교하게 고객을 타게팅할 수 있도록 합니다. 고객의 행동 데이터 및 개인화 데이터를 활용한 방법을 제공하고 있지만, 이러한 정보를 저장하는 구조에는 한계점이 있었습니다. 오늘 이 글에서는 고객 행동 데이터를 보다 정교하게 지원하기 위해 Clickhouse를 도입하게 된 과정을 적어봅니다. 또한 마주했던 한계를 설명합니다.
채널톡은 고객과 기업을 이어주는 B2B 상담 툴로써 자리매김하고 있지만, 그 외에도 다양한 기능을 제공합니다.
CRM 마케팅은 그중 하나로, 채널톡/문자/카카오 메시지를 발송해 상담으로 쌓인 고객을 다시 온사이트로 끌어들이게하고, 고객이 마케팅을 통해 마케팅 목표로 전환되면(예시: 제품을 구매) 고객의 행동 데이터들이 채널톡에도 반영되며, 더 많은 개인화 데이터가 적재되고 더 나은 상담을 할 수 있는 선순환 과정을 돕습니다.
고객의 매출을 책임지면서도 개인화된 고객 데이터를 통한 정교한 타게팅은 채널톡 CRM 마케팅의 큰 방향성이자 강점입니다.
그만큼 고객의 개인화 데이터는 중요합니다. 하지만, 기존 채널톡에서 지원하지만, 마케팅으로 타게팅하긴 어려운 데이터들이 존재했습니다. 그 중 가장 주요했던 것은 커머스 연동 데이터들입니다. 채널톡은 다양한 빌더사(Cafe24, Imweb 등)을 통해 연동할 수 있지만, 데이터 저장 구조의 한계로 고객이 어떤 물품을 샀고, 고객이 장바구니에 어떤 제품을 담고 있는지, 고객이 어떠한 쿠폰을 가지고 있지만 사용하지 않았는지 등을 타게팅 하기에는 어려운 점이 있었습니다.
채널톡의 데이터는 여러 데이터베이스 시스템에 저장합니다. 기본적으로 RDBMS(pSQL), NoSQL(DynamoDB) 등에 저장할 수 있지만, 검색을 위해 OpenSearch를 이용하거나 더 빠른 캐싱을 위해 Redis를 이용하기도 합니다. 더 정교하고 schemaless한 데이터의 쿼리를 위해 원본 테이블에 대한 보조 저장소를 따로 구성하여 쿼리하기도 합니다.(내부적으로 Memdb라고 부릅니다.)
여기에 더해 앞서 설명했던 추가 요구사항이 있었습니다. e-commerce에서 고객이 관심있는 상품은 구매 유도를 위한 좋은 기준입니다. 따라서, 고객이 어떤 물품을 장바구니에 담았는지, 고객이 어떤 쿠폰을 가지고 있는지 어떠한 상품들을 샀었는지에 대한 행동 데이터 역시 중요합니다.
한 번 아래 상황을 어떻게 테이블 설계하고 쿼리할 건지 상상해봅시다.
A 상품 쿠폰을 가지고 있는 고객이 장바구니에 A를 담았지만, 아직 구매하지 않은 경우
장바구니에 상품을 담고, 아직 해당 상품을 주문하지 않은 경우
그림 1. 채널톡의 커머스 데이터 세그먼트 작성 화면
위와 같은 쿼리 패턴을 지향하는 데이터베이스 시스템은 무엇일까요? 가장 먼저 RDMBS를 떠올리게 합니다만, 전통적인 RDBMS에서는 감당하기 어려운 데이터량을 가지고 있습니다. 예시로 현재 채널톡 고객사들이 발급한 쿠폰은 최소 20억 건의 이상으로 추정합니다.
적절한 인덱싱을 건다고해도, JOIN 쿼리 형식의 데이터를 최적화하기에는 Disk Spill이 일어날 가능성이 높습니다. I/O 병목에 따라 꼬리 지연이 길어지고, 제품의 안전성 저하시키게 됩니다.
또한 해당 데이터들은 기본적으로 Write Heavy합니다. 내부적으로 Write/Read 비율이 100:1 정도입니다. 특징을 간략히 정리해보면,
Write Heavy Workload입니다. 또한, 시의성에 따라 트래픽이 특정 시간대 혹은 날짜(예시: 블랙 프라이데이)에 몰릴 수 있습니다. Spiky한 트래픽 구조입니다.
기본적으로 하나의 테이블에 모든 정보를 담기에는(비정규화 하기) 어렵습니다. 모든 이벤트 및 히스토리를 하나의 테이블에 설명하기에는 어렵습니다. 즉, JOIN이 필요한 쿼리 패턴입니다.
고객사에서 진행된 주문 정보는 일종의 WAL(Write Ahead Log)처럼 웹훅 형식으로 전달 받습니다. Transactional 한 데이터 구조가 아닌 적재 및 LWW(Last Write Wins) 정도의 일관성 수준만 지원해도 됩니다.
ad-hoc성 데이터 쿼리를 하고, 준 실시간 데이터만을 처리해도 무방합니다.
이러한 구조는 기존 OLTP(Online Transaction Processing) 시스템에서 OLAP(Online Analytics Processing) 시스템으로의 전환을 내포하고 있었습니다. 대용량의 데이터 처리와 최종 일관성만을 제공하면 되는 대규모 워크로드에 어울리는 데이터 성질을 나타내고 있습니다.
여기서 OLAP 시스템이 대두됩니다. OLAP 시스템이 뭘까요? 그전에 앞서 기존 데이터베이스 시스템의 흐름을 설명해보겠습니다.
데이터베이스는 다 각자마다의 특성이 있습니다. 우리는 기본적으로 정규화된 잘 정의된 모델에 대해서 RDBMS를 통해서 자주 읽고 씁니다. 또한 강력한 트랜잭션 위해 쓰기도 하죠. 하지만 pSQL 보다 좀 더 빠르고, Random Access가 강한 쿼리 패턴에서 TPS(Transcation Per Second)를 보장해야한다면 Redis를 통해 in-memory 의 강점을 가져오고, 고장감내까지 지원해야 하는 Write Heavy 패턴이라면 LSM-Tree 구조의 key-value store 등을 써야합니다.
하나의 DB 머신에서 풀 수 없는 경우(Vertical Scaling이 불가능한 경우) 우리는 파티셔닝을 고민합니다. 데이터를 나눠서 저장하고 그 사이에서 트랜잭션까지 보장해야한다면 분산 트랜잭션이라는 매우 불편하고 복잡한 문제를 풀어야합니다. 이를 위해 코디네이터가 필요할 수도 있구요. 결국 데이터베이스는 만능이 아닙니다. write/read 모두 잘하고 복잡한 쿼리 또한 지원하는데 트랜잭션도 잘될 수 있으며 수평 확장도 용이한 만능 데이터베이스는 별로 없습니다(혹은 비쌉니다).
OLTP(Online Transaction Processing) 실시간으로 다수의 트랜잭션을 빠르게 처리하는 시스템입니다. 은행 송금, 주문 처리, 회원 가입처럼 짧고 빈번한 읽기/쓰기 작업이 중심입니다. 데이터 일관성이 중요합니다.
그에 반해 통계를 구현해야 하는 시스템들은 보통 OLAP(Online Analytics Procssing)이라고 부릅니다. 이들은 OLTP 시스템과는 다르게 준 실시간의 데이터를 쿼리하는데 초점이 있고, 트랜잭션 혹은 특정 item(혹은 row)를 업데이트하는 것에 대해서는 비교적 약합니다.
하지만, 대규모 데이터 파이프라인을 운용하고 데이터를 뽑아내는 입장에서는 집중하지 않는 일관성 보장을 느슨하게 하고 대규모 집계 및 쿼리에 집중하는 형태의 워크로드입니다. 언급한 ClickHouse 역시 OLAP 시스템 중 하나입니다.
OLAP 시스템에는 많은 특징이 이에는 컬럼형 데이터(Columnar Data Format) 및 최종 일관성 지원 등이 주요 특징이겠습니다.
ClickHouse는 비교적 최근에 나온 OLAP 시스템이지만 성숙도가 높다고 판단하고, 레퍼런스도 비교적 다양했습니다. 또한, 해당 기능을 구현하기 위해선 한 달 이내에 프로덕션 안전성까지를 보장해야하는 상황이었기에 ClickHouse Cloud를 이용할 경우 쉽게 확보할 수 있다고 판단했습니다.(최소한의 세팅과 최적화 역시 이와 동일한 이유입니다)
그와 별개로 ClickHouse는 MT(MergeTree)의 간단한 구조만을 유지하는 단순성과 명쾌함이 오히려 큰 매력이기도 했습니다. Sparse Index 및 Bloom Filter 등의 단순한 인덱스 구조와 맞물려서 빠른 이해와 구조 파악에도 용이했습니다.(물론 그로 인한 단점도 명확합니다, 이는 이후 섹션에서 다뤄봅시다.)
아래는 적절한 OLAP 시스템을 찾으면서 정리한 표입니다. 당시 결정에서는 관리 용이성 및 속도 그리고 유즈케이스 상에선 ClickHouse가 최선의 선택이라고 판단했습니다.
비교 항목 | PostgreSQL | Druid | BigQuery | StarRocks | ClickHouse |
|---|---|---|---|---|---|
저장 구조 | Row-based | Column-based | Column-based | Column-based | Column-based |
쿼리 유연성 | 높음 | 낮음 (스키마 고정) | 높음 | 높음 | 높음 |
Ad-hoc 조건 조합 | |||||
비용 효율성 | 쿼리당 과금 | 직접 운영 시 | |||
운영 복잡도 | 낮음 | 높음 | 없음 (Managed) | 높음 | 낮음 |
대용량 분석 성능 | 메모리 스필, 꼬리 지연 | ||||
에코시스템 성숙도 | 성장 중 | ||||
최종 판단 | 성능 한계 | 유연성 부족 | 비용 부담 | 운영 부담 | 채택 |
다만, 위에서 언급했듯이, ClickHouse가 가지는 단순 명료함은 오히려 문제가 될 수 있습니다. 실제 프로덕션 도입 과정에서 생긴 트러블슈팅 과정과 함께 설명합니다.
그림 2. MergeTree의 Insert 도식화 [1]
RMT(ReplacingMergeTree)는 Update가 있는 데이터를 관리하는 방식입니다. 기본적으로 ClickHouse의 데이터 저장구조는 MT(MergeTree)라는 데이터 구조 하에서 일어납니다. MT(MergeTree)는 매우 단순하고 명쾌합니다. 매 쓰기를 Disk로 바로 작은 Part로 적재하고, 이를 주기적으로 Merge하는 구조입니다.
다만, 이러한 구조는 Append Only의 데이터 쓰기에서 더욱 유리합니다. 특히, 기본 MT의 경우 Update 요청 시에 in-place 방식이 아닌 mutation 방식으로 part가 재작성되는 문제가 있기 때문에 이러한 구조를 방지하기 위해 제안된 것이 RMT(ReplacingMergeTree)입니다.
그림 2. ReplacingMergeTree의 Merge 및 Compress 과정 [2]
다만, RMT역시 문제가 있습니다. RMT는 중복에 대해서 즉시 제거하지 않고, 새 item이 적재되는 형식입니다. 이에 대한 중복 제거는 Merge 시점에 진행되는 특징이 있습니다. 그 의미는 매 Query 마다 특정 Item의 중복된 데이터가 쿼리될 수 있습니다. 이를 해결하기 위해서 매 쿼리마다 FINAL 키워드를 사용하여 Deduplication을 진행할 수 있지만, 이 또한 쿼리 성능을 잡아먹는 주 원인입니다.
이러한 빈번한 업데이트는 주문 데이터에서 일어날 수 있습니다. 주문은 언제든 취소/환불 등의 변경으로 인해 바뀔 수 있으니까요. 다만, 주문은 주문 확정 등 특정 시점 이후부터는 "상태가 변경되지 않음"이라는 특성을 가집니다. 이러한 상황에서는 이야기가 쉬워질 수 있습니다. ClickHouse 특성 상 Append Only에 특화된 OLAP 시스템이기에 이러한 제약은 어쩔 수 없습니다. 이를 개선하기 위해서 도입했던 영역은 Hot/Cold 분리였습니다.
기본적으로 Update 가능한 영역을 분리하고, Cold Storage 와 같이 쿼리하는 형식으로 이를 수행했습니다.
ClickHouse는 기본적으로 모든 JOIN을 잘 지원하는 OLAP 시스템은 아닙니다. 위에서 언급했듯이 이러한 복잡한 대규모 Fact Table에 대한 JOIN은 Starrocks와 같은 시스템이 더욱 잘 지원합니다.
ClickHouse가 JOIN 패턴에 대해서는 아래와 같이 권장합니다.
작은 dimension 테이블과의 JOIN (Dictionary 또는 IN subquery)
브로드캐스트 가능한 크기의 right-hand side 테이블
사전에 co-located된 데이터 간 JOIN
비교적 약한 영역은 아래와 같습니다.
양쪽 다 큰 테이블 간의 distributed JOIN
셔플이 필요한 JOIN (양쪽 테이블이 같은 키로 샤딩되지 않은 경우)
복잡한 multi-way JOIN
다만, JOIN 시 성능 문제는 예상했던 문제였고, 이에 대한 문제는 아래와 같은 쿼리 패턴이기에 해결할 수 있었습니다.
실제 주문/쿠폰/장바구니 정보 자체는 수억 건에서 수십억 건의 달하는 Fact Table입니다.
다만, 실제로 저희가 쿼리하는 데이터는 "특정 조건을 만족하는 고객의 집합"입니다.
전체 데이터에 대한 JOIN이라면 ClickHouse가 지원하기 어려운 형태였겠지만, 수백만 건 이하의 필터링된 고객 집합 간의 메모리 연산이라면, ClickHouse가 지원하는 Parallel Hash Join 등으로 쉽게 커버 가능합니다. 실제 쿼리의 지연 수준 또한 수백ms 수준에서 관리될 수 있습니다.
다만, 이는 JOIN 대상 집합이 1-2GB 이내로 관리될 때의 이야기입니다. 또한, Parallel Hash Join 은 그만큼 시스템 자원 또한 많이 소모하는 연산이기에 이러한 형태는 개선될 필요가 있습니다. 이에 관한 이야기는 추후 Starrocks에 대한 이야기와 함께 하려고 합니다.
추가적으로 ClickHouse 구조로는 어려운 연산이 있었습니다. 바로 Point Query입니다. 채널톡은 앞서 언급했던 특정 타게팅된 고객의 집합을 "세그먼트"라고 부릅니다. 최종 마케팅 발송 시에는 처음 세그먼트에 등록되었던 고객이 여전히 포함되는지 확인하는 Membership Check 과정이 있습니다. 이는 "해당 유저가 이 세그먼트에 속해있어?"라는 Point Query 형식으로 질의하게 됩니다.
ClickHouse는 내부적으로 데이터를 저장할 때, Granule이라는 단위를 사용합니다. 기본 설정은 8192개의 아이템을 담고 있습니다. 수십억개의 아이템을 스캔하는데에 효율성을 가지는 것도 이러한 부분입니다. 언급했듯, Sparse Index를 통해 Granule이 가진 데이터 범위를 파악하고, Skip 혹은 읽으며 쿼리를 진행합니다.
그림 3. granule의 저장 구조 [2]
즉, 하나의 아이템을 읽기 위해 큰 범위의 파일을 읽는다는 점입니다. 위 문제점 역시, ClickHouse 의 제약사항이지만 정확히 말하자면 대규모 처리 워크로드에서는 자연스럽게 수반되는 문제일 수 있습니다. 이를 우회하기 위해 저희는 ClickHouse와 Key-Value Store를 동시에 운영하는 하이브리드 아키텍처를 선택했습니다. 물론 동기화 과정에서의 높은 엔지니어링 비용과 복잡도를 올리는 선택이었지만, 관련 사용례 역시 레퍼런스를 찾기 쉬워 이와 같은 과정을 진행했습니다. [4][5]
ClickHouse를, 실제로 OLAP 시스템을 최초로 팀 내에서 도입하면서 느꼈던 건 ClickHouse의 단순 명료함과 그 구조에서 오는 이점이 있지만, 역시 또한 기술적 trade-off로써 실제 프로덕션 운영 중에 문제점이 발생했다는 점입니다. RMT와 JOIN의 한계는 도입 전부터 인지하고 있었습니다. 하지만, 구현 및 관리 난이도에 있어서 적절한 선택은 여전히 ClickHouse였을 거라 생각합니다.
다만, ClickHouse의 단순함으로 인해 하이브리드 설계로 인해 전체 아키텍처 자체는 복잡도가 높아지는 결과물이 수반될 수 있습니다. 이는 어디에 복잡도를 둘 것이냐에 대한 선택의 영역이기도 합니다.
여러 생산성 향상으로 인해 개발 속도나 도입이 빨라진 지금, 저희는 더 많은 trade-off에서 저울질하고 있습니다. 중요한 건 현재 시점에서 여러 단점이 있더라도 가장 좋은 후보군이 무엇인지를 결정하는 능력이 아닐지 싶습니다.
그와 별개로 고무적으로, OLAP 시스템을 도입하고 명확한 한계를 인지하게 되며 팀 내에서 이해도가 높아지고 있습니다. 저희 팀에서는 최근 출시한 커스텀 리포트에서 Update가 매우 빈번한 Workload 역시 Analytics를 제공해야하는 비즈니스 요구사항을 반영하기 위해 Starrocks를 도입했습니다. 이 글에서 설명한 Hot Storge들도 더 나은 개선을 위해 Strarrocks로 옮기는 걸 고려하고 있구요.
다음 아티클에서는 이와 관련되어 Starrocks에 대한 설명 및 도입 과정에 대해서 소개해보려고 합니다. 많은 관심 부탁드립니다.
[1] https://clickhouse.com/docs/academic_overview
[2] https://clickhouse.com/docs/merges
[3] https://clickhouse.com/docs/optimize/skipping-indexes
[4] https://engineering.brevo.com/segmentation-to-target-the-right-audience/
[5] https://hoop.dev/blog/what-clickhouse-redis-actually-does-and-when-to-use-it/
We Make a Future Classic Product