Cassandra — 대규모 쓰기에 최적화된 Wide-Column DB
초당 수십만 건의 쓰기를 감당해야 할 때, RDBMS로는 왜 한계가 오는 걸까? Cassandra는 이 문제를 어떤 구조로 해결하는지, 쓰기 최적화 원리부터 파티션 설계, 일관성 트레이드오프까지 전부 정리합니다.
Cassandra란
Apache Cassandra는 Facebook에서 만들어 오픈소스로 공개한 분산 NoSQL 데이터베이스 예요. 분류상으로는 Wide-Column Store에 해당합니다. Google의 Bigtable 논문에서 데이터 모델을, Amazon의 Dynamo 논문에서 분산 아키텍처를 가져왔다고 보면 돼요.
CAP 이론 관점에서 Cassandra는 AP 시스템 입니다. 일관성(Consistency)보다 가용성(Availability)과 파티션 내성(Partition Tolerance)을 우선시해요. 물론 일관성 수준을 QUORUM으로 올리면 사실상 CP처럼 동작하게 만들 수도 있긴 한데, 기본 철학은 "일단 쓰기는 받아들이고 나중에 맞추자"에 가깝습니다.
특히 대규모 쓰기 워크로드 에 최적화되어 있어요. Netflix, Discord, Apple 같은 회사들이 대용량 로그, 메시지, 시계열 데이터를 저장할 때 Cassandra를 쓰고 있습니다.
아키텍처 — 마스터가 없다
P2P 구조
RDBMS의 Primary-Replica 구조와 달리, Cassandra에는 마스터 노드가 없습니다. 모든 노드가 동등한 피어(peer) 예요. 클라이언트는 어느 노드에 접속하든 읽기/쓰기가 가능하고, 접속한 노드가 코디네이터(coordinator) 역할을 맡아서 요청을 처리합니다.
마스터가 없으니 SPOF(Single Point of Failure)도 없어요. 노드 하나가 죽어도 다른 노드가 그 역할을 대신합니다.
Gossip 프로토콜
노드끼리 서로의 상태를 어떻게 알까요? Gossip 프로토콜 을 씁니다. 1초마다 랜덤으로 다른 노드를 골라서 자기가 알고 있는 클러스터 상태 정보를 교환해요. 바이러스가 퍼지듯이 정보가 전파되는 거예요.
각 노드는 다른 노드의 heartbeat 카운터, 상태(NORMAL, LEAVING, JOINING 등)를 추적합니다. 일정 시간 동안 heartbeat이 안 올라가면 해당 노드를 죽은 것으로 판단해요.
가상 노드(vnode)
초기 Cassandra는 각 물리 노드에 토큰 범위를 하나만 할당했어요. 노드 추가/제거 시 데이터 재분배가 고통스러웠습니다. 지금은 가상 노드(vnode) 방식을 써요.
물리 노드 하나에 기본 256개의 토큰 범위를 할당합니다. 덕분에 새 노드가 추가되면 여러 기존 노드에서 조금씩 데이터를 가져오게 되고, 부하 분산이 훨씬 균일해져요.
데이터 모델
Keyspace
RDB의 데이터베이스(스키마)에 해당해요. 복제 전략(SimpleStrategy, NetworkTopologyStrategy)과 복제 팩터(replication factor)를 여기서 설정합니다.
CREATE KEYSPACE my_app
WITH replication = {
'class': 'NetworkTopologyStrategy',
'dc1': 3, 'dc2': 2
};
Table
RDB의 테이블과 비슷해 보이지만 내부 구조는 완전히 달라요. 각 행(row)은 파티션 키로 식별되고, 파티션 내부에서 클러스터링 키 순서대로 정렬됩니다.
Partition Key
** 데이터가 어느 노드에 저장될지 결정 **하는 키예요. 파티션 키 값을 해시 함수(Murmur3)에 넣으면 토큰 값이 나오고, 이 토큰이 속하는 범위를 담당하는 노드에 데이터가 들어갑니다.
Clustering Key
하나의 파티션 안에서 ** 데이터의 물리적 정렬 순서 **를 결정합니다. 클러스터링 키가 있으면 해당 파티션 내에서 디스크에 정렬된 채로 저장되기 때문에 범위 조회가 빨라요.
CREATE TABLE sensor_data (
sensor_id text,
recorded_at timestamp,
value double,
PRIMARY KEY (sensor_id, recorded_at)
) WITH CLUSTERING ORDER BY (recorded_at DESC);
여기서 sensor_id가 파티션 키, recorded_at이 클러스터링 키예요. 같은 센서의 데이터가 한 파티션에 모이고, 시간 역순으로 정렬됩니다.
파티션 키 설계가 왜 중요한가
Cassandra에서 파티션 키 설계를 잘못하면 끝납니다. 과장이 아니에요.
핫스팟 문제
파티션 키가 편향되면 특정 노드에 데이터가 몰려요. 예를 들어 country를 파티션 키로 쓰면 한국 트래픽의 데이터가 전부 한 노드로 갑니다. 그 노드만 디스크 꽉 차고, CPU 터지고, 나머지 노드는 놀고 있어요.
너무 큰 파티션
하나의 파티션에 데이터가 수백 MB 이상 쌓이면 읽기 성능이 급격히 떨어집니다. Compaction도 느려지고 메모리도 많이 먹어요. 일반적으로 파티션 하나에 100MB, 10만 행 이하를 권장합니다.
복합 파티션 키
핫스팟을 방지하려면 복합 파티션 키를 씁니다.
PRIMARY KEY ((sensor_id, date_bucket), recorded_at)
sensor_id만 쓰면 데이터가 무한히 쌓이니까, 날짜 버킷을 추가해서 파티션을 쪼개는 거예요. 이렇게 하면 조회할 때 sensor_id와 date_bucket 둘 다 지정해야 하는 제약이 생기지만, 파티션 크기가 제어됩니다.
CQL — SQL 같지만 SQL이 아니다
Cassandra Query Language(CQL)은 문법이 SQL과 비슷해서 처음엔 RDB처럼 쓰면 될 것 같은 착각이 들어요. 하지만 근본적인 제약이 있습니다.
JOIN 없음
테이블 간 조인이 안 됩니다. 정규화된 스키마를 쓸 수 없어요. 대신 ** 쿼리 기반으로 테이블을 설계 **해야 합니다. 같은 데이터를 여러 테이블에 중복 저장하는 게 정상이에요. RDB 출신 개발자가 처음에 가장 적응하기 어려운 부분입니다.
WHERE 제약
WHERE 절에 아무 컬럼이나 넣을 수 없어요. 파티션 키는 반드시 포함해야 하고, 클러스터링 키는 선언된 순서대로만 조건을 걸 수 있습니다.
-- 가능
SELECT * FROM sensor_data WHERE sensor_id = 'A' AND recorded_at > '2026-01-01';
-- 불가능 (파티션 키 없이 클러스터링 키만 조건)
SELECT * FROM sensor_data WHERE recorded_at > '2026-01-01';
ALLOW FILTERING을 붙이면 되긴 하는데, 이건 전체 테이블 스캔이라 프로덕션에서 쓰면 안 돼요. 한 줄로 정리하면 "ALLOW FILTERING = 풀 스캔이니까 프로덕션에서는 쓰지 말 것"입니다.
Secondary Index
파티션 키 외 컬럼에 인덱스를 걸 수 있긴 해요. 하지만 카디널리티가 높은 컬럼에는 비효율적이고, 내부적으로 숨겨진 테이블을 하나 더 만듭니다. 차라리 Materialized View나 별도 테이블을 만드는 게 나아요.
쓰기 경로 — 왜 이렇게 빠른가
Cassandra의 쓰기가 빠른 이유는 디스크에 순차 쓰기만 하기 때문 이에요. RDB처럼 B-Tree 인덱스를 업데이트하면서 랜덤 I/O를 발생시키지 않습니다.
쓰기 순서
Client → Coordinator Node → Replica Nodes
↓
1. Commit Log (순차 쓰기)
↓
2. Memtable (메모리)
↓
3. SSTable (디스크 flush)
1단계: Commit Log 요청이 들어오면 먼저 Commit Log에 append합니다. 순차 쓰기(sequential write)라서 매우 빨라요. 노드가 죽었다 살아나면 Commit Log를 재생해서 Memtable을 복구합니다. WAL(Write-Ahead Log)이랑 같은 개념이에요.
2단계: Memtable 메모리에 있는 정렬된 자료구조에 데이터를 넣어요. 여기까지 오면 클라이언트에게 "성공" 응답을 보냅니다. 디스크 flush를 기다리지 않아요.
3단계: SSTable Memtable이 일정 크기에 도달하면 디스크에 SSTable(Sorted String Table) 로 flush됩니다. SSTable은 한번 쓰면 변경 불가(immutable)해요. 업데이트와 삭제도 새로운 SSTable에 기록됩니다.
이 구조 때문에 Cassandra의 쓰기 레이턴시는 거의 메모리 쓰기 수준이에요. RDB에서는 상상하기 어려운 수준의 쓰기 처리량이 나옵니다.
읽기 경로
쓰기가 빠른 대신 읽기는 상대적으로 복잡해요. 여러 SSTable에 같은 키의 데이터가 흩어져 있을 수 있으니까요.
읽기 순서
Client 요청
↓
1. Memtable 확인 (메모리)
↓
2. Row Cache 확인 (있으면 즉시 반환)
↓
3. Bloom Filter → 이 SSTable에 데이터가 있을 가능성?
↓
4. Key Cache → 파티션 인덱스 위치를 캐시에서 확인
↓
5. Partition Index → SSTable 내 오프셋 찾기
↓
6. SSTable에서 실제 데이터 읽기
↓
7. 여러 SSTable 결과를 머지 (가장 최신 타임스탬프 우선)
Bloom Filter 가 핵심입니다. 확률적 자료구조로, "이 SSTable에 해당 파티션 키의 데이터가 없는 건 확실히 알려주고, 있는 건 확률적으로 알려줘요." 덕분에 데이터가 없는 SSTable을 디스크 I/O 없이 건너뛸 수 있습니다. False positive는 있지만 false negative는 없어요.
Key Cache 는 파티션 키 → SSTable 내 오프셋 매핑을 메모리에 캐시합니다. 히트하면 디스크에서 인덱스 파일을 안 읽어도 돼요.
Compaction
SSTable은 immutable이라 시간이 지나면 같은 키에 대한 데이터가 여러 SSTable에 흩어져요. Compaction은 이걸 합쳐서 정리하는 과정입니다. 오래된 버전의 데이터를 제거하고, tombstone(삭제 마커)도 이때 실제로 지워요.
Size-Tiered Compaction (STCS)
기본 전략이에요. 비슷한 크기의 SSTable이 일정 개수(보통 4개) 모이면 하나로 합칩니다. 쓰기가 많은 워크로드 에 적합해요. 단점은 compaction 중 일시적으로 디스크를 2배까지 쓸 수 있다는 것입니다.
Leveled Compaction (LCS)
SSTable을 레벨별로 관리해요. L0에서 시작해서 L1, L2로 올라가면서 크기가 10배씩 커집니다. 각 레벨 내에서 키 범위가 겹치지 않도록 유지해요. 읽기가 많은 워크로드 에 적합합니다. 같은 키를 찾을 때 확인해야 할 SSTable 수가 적어지니까요. 대신 쓰기 증폭(write amplification)이 큽니다.
Time-Window Compaction (TWCS)
시계열 데이터 에 최적화된 전략이에요. 시간 윈도우(예: 1시간)별로 SSTable을 묶어서 compaction합니다. 오래된 데이터를 TTL로 만료시키는 패턴과 궁합이 좋아요. 윈도우가 닫히면 그 안의 SSTable은 더 이상 compaction 대상이 아닙니다.
핵심만 정리하면 이 세 가지 전략과 각각 어떤 워크로드에 적합한지를 알면 충분해요.
일관성 수준 (Consistency Level)
Cassandra에서 일관성은 요청 단위 로 설정합니다. 테이블이나 클러스터 단위가 아니에요.
| 레벨 | 의미 |
|---|---|
| ONE | 복제본 중 1개만 응답하면 성공. 가장 빠르지만 일관성 낮음 |
| QUORUM | 복제본의 과반수가 응답해야 성공. (RF=3이면 2개) |
| ALL | 모든 복제본이 응답해야 성공. 가장 느리지만 강한 일관성 |
| LOCAL_QUORUM | 로컬 데이터센터 내에서만 QUORUM 적용 |
실무에서 가장 많이 쓰는 조합은 ** 쓰기 QUORUM + 읽기 QUORUM**입니다. 이렇게 하면 W + R > RF가 성립해서 강한 일관성(strong consistency)을 보장해요. RF=3일 때 W=2, R=2니까 2+2=4 > 3. 최소 하나의 노드에서 최신 데이터를 읽게 됩니다.
그런데 ONE으로 쓰고 ONE으로 읽으면? 오래된 데이터를 읽을 수 있어요. 이걸 eventual consistency라고 합니다. 로그 데이터처럼 약간의 지연이 괜찮은 경우에 써요.
Tombstone — 삭제가 특이하다
Cassandra에서 데이터를 DELETE 하면 ** 즉시 삭제되지 않아요.** 대신 tombstone이라는 삭제 마커를 남깁니다.
왜 이렇게 할까요? 분산 시스템이라서 그래요. 노드 A에서 삭제했는데 노드 B가 그 시점에 다운되어 있었다면? 노드 B가 살아났을 때 "어, A한테 이 데이터 없네? 내가 가지고 있으니까 복구해줘야지" 하고 되살려버릴 수 있습니다. tombstone이 있으면 "아, 이건 삭제된 거구나"라고 인식해요.
gc_grace_seconds
tombstone은 gc_grace_seconds(기본 10일) 동안 유지됩니다. 이 기간이 지나면 compaction 시 실제로 제거돼요. 10일 안에 다운된 노드가 복구되어 repair를 받아야 한다는 뜻이기도 합니다.
Tombstone이 쌓이면?
문제가 심각해집니다. 읽기할 때 tombstone도 전부 스캔해야 하니까 읽기 성능이 바닥을 쳐요. Cassandra가 tombstone_warn_threshold(기본 1000)을 넘으면 경고를 뱉고, tombstone_failure_threshold(기본 10만)을 넘으면 아예 쿼리를 거부합니다.
그래서 Cassandra에서 잦은 삭제는 안티 패턴 이에요. 삭제가 많은 워크로드라면 TTL을 활용하거나, 아예 Cassandra가 적합하지 않은 건 아닌지 다시 생각해봐야 합니다.
주의할 점 — 안티 패턴과 깊은 비교
Cassandra vs HBase
둘 다 Wide-Column Store인데 뭐가 다를까요?
| 항목 | Cassandra | HBase |
|---|---|---|
| 아키텍처 | P2P, 마스터 없음 | Master-RegionServer 구조 |
| CAP | AP (가용성 우선) | CP (일관성 우선) |
| 쓰기 | 매우 빠름 | 빠름 (LSM-Tree) |
| 읽기 | 상대적으로 느림 | 강한 일관성 읽기 가능 |
| 운영 | 상대적으로 단순 | Hadoop/ZooKeeper 의존 |
| 적합한 경우 | 쓰기 많은 워크로드, 멀티 DC | 강한 일관성 필요, Hadoop 에코시스템 |
간단히 말하면, 쓰기 비중이 높고 멀티 데이터센터가 필요하면 Cassandra, 강한 일관성이 필요하고 Hadoop 생태계를 이미 쓰고 있으면 HBase입니다.
안티 패턴
Cassandra의 대표적인 안티 패턴 세 가지를 정리하면 이래요.
- **큰 파티션 **: 파티션 하나에 수백 MB 이상 쌓이면 읽기 성능 저하, compaction 병목. 파티션 키 설계로 해결합니다.
- ** 잦은 삭제 **: tombstone 누적 → 읽기 성능 저하. TTL을 쓰거나 삭제가 적은 모델로 설계해야 해요.
- **RDB처럼 모델링 **: 정규화하고 JOIN하려고 하면 안 됩니다. 쿼리 패턴 먼저 정하고, 그에 맞게 비정규화된 테이블을 만들어야 해요.
시계열 데이터에 왜 적합한가
이건 Cassandra의 설계 철학과 직결되는 부분이에요. 정리하면 이렇습니다.
- ** 쓰기 패턴 **: 센서, 로그 같은 시계열 데이터는 쓰기가 압도적으로 많아요. Cassandra의 쓰기 최적화와 맞아떨어집니다.
- ** 자연스러운 파티션 키 **: 디바이스 ID + 시간 버킷으로 파티션 키를 잡으면 데이터가 균일하게 분산돼요.
- ** 클러스터링 키로 시간 정렬 **: 타임스탬프를 클러스터링 키로 쓰면 시간 범위 쿼리가 빨라요. 디스크에 이미 정렬되어 있으니까요.
- TTL: 오래된 데이터를 자동 만료시킬 수 있어요.
INSERT INTO ... USING TTL 86400이런 식으로요. - TWCS: Time-Window Compaction은 시계열 데이터의 TTL 만료 패턴에 딱 맞는 compaction 전략입니다.
파생 개념
| 개념 | 설명 |
|---|---|
| NoSQL / CAP 이론 | Cassandra를 이해하려면 반드시 알아야 하는 기본 개념 |
| ** 분산 시스템** | 합의 알고리즘, 일관성 모델, 장애 감지 등 Cassandra 아키텍처의 바탕 |
| ** 시계열 DB** | InfluxDB, TimescaleDB 등 시계열 전용 DB와의 비교 포인트 |
| LSM-Tree | Cassandra 쓰기 경로의 근간이 되는 자료구조 |
정리
Cassandra를 한 문장으로? ** 마스터 없는 P2P 아키텍처에 LSM-Tree 기반 쓰기 최적화를 얹은 AP 시스템 **입니다.
Cassandra를 제대로 이해하려면, 단순히 "NoSQL이고 빠르다" 수준에서 끝내지 말고 쓰기 경로(Commit Log → Memtable → SSTable), 파티션 키 설계의 중요성, 일관성 수준의 트레이드오프까지 연결해서 설명할 수 있어야 해요. tombstone이랑 compaction 전략까지 이해하면 상당히 깊은 수준입니다.