카산드라를 처음 접하는 개발자가 가장 많이 하는 실수는 RDBMS처럼 데이터 모델링을 하는 것입니다. 카산드라의 성능을 100% 활용하기 위해서는 RDBMS의 상식을 버리고, 카산드라의 철학에 맞는 새로운 접근법을 배워야 합니다.
RDBMS는 데이터를 정규화하여 중복을 최소화하는 데 집중합니다. 그리고 필요할 때 JOIN을 사용해 원하는 데이터를 조합하여 조회합니다. 하지만 카산드라는 JOIN을 지원하지 않으며, 분산 환경에서 JOIN은 극심한 성능 저하를 유발합니다.
따라서 카산드라의 데이터 모델링은 "애플리케이션에서 어떤 쿼리로 데이터를 조회할 것인가?"라는 질문에서 시작합니다. 즉, 쿼리 우선 모델링(Query-First Modeling)이 핵심입니다.
WHERE절)에 맞춰 파티션 키와 클러스터링 키를 신중하게 설계합니다.카산드라에서 기본키는 단순히 데이터를 식별하는 것을 넘어, 데이터가 클러스터의 어느 노드에 저장되고(분산), 그 노드 안에서 어떻게 정렬될지(정렬)를 결정하는 핵심적인 역할을 합니다.
기본키는 하나 이상의 컬럼으로 구성되며, 파티션 키(Partition Key)와 선택적으로 클러스터링 키(Clustering Key)로 나뉩니다.
PRIMARY KEY ( (partition_key_1, partition_key_2), clustering_key_1, clustering_key_2 )
-- 괄호로 묶인 부분이 파티션 키, 그 뒤가 클러스터링 키입니다.
데이터가 어느 노드에 저장될지를 결정합니다. 카산드라는 파티션 키의 값을 해시하여 토큰을 만들고, 이 토큰을 기준으로 데이터를 링 위의 특정 노드에 분산시킵니다.
WHERE절에는 반드시 파티션 키에 대한 동등(=) 또는 IN 조건이 포함되어야 합니다. 그렇지 않으면 어떤 노드로 가야 할지 몰라 전체 클러스터를 스캔해야 하므로 쿼리가 실패합니다.하나의 파티션 내에서 데이터가 어떤 순서로 디스크에 정렬될지를 결정합니다.
>, <)나 정렬(ORDER BY)을 매우 효율적으로 수행할 수 있습니다.CREATE TABLE user_posts (
username TEXT,
post_time TIMESTAMP,
post_id UUID,
title TEXT,
content TEXT,
PRIMARY KEY ( (username), post_time )
) WITH CLUSTERING ORDER BY (post_time DESC);
위 테이블에서 username은 파티션 키, post_time은 클러스터링 키입니다.
"gemini"라는 사용자의 모든 게시물은 하나의 파티션에 모여있고, post_time을 기준으로 내림차순 정렬되어 저장됩니다. 따라서 "gemini의 최근 게시물 10개 가져오기" (SELECT * FROM user_posts WHERE username = 'gemini' LIMIT 10;) 같은 쿼리는 디스크를 순차적으로 딱 한 번만 읽어 매우 빠르게 처리됩니다.
RDBMS에서는 죄악시되는 역정규화가 카산드라에서는 최고의 미덕입니다. JOIN이 없기 때문에, 필요한 모든 데이터를 읽기 편한 형태로 미리 테이블에 저장해두는 전략입니다.
RDBMS라면 videos, users, comments 테이블을 만들고 JOIN으로 해결하겠지만, 카산드라는 다릅니다.
필요한 쿼리 목록:
각 쿼리에 맞는 테이블 생성:
-- 쿼리 1번용 테이블
CREATE TABLE videos_by_id (
video_id UUID PRIMARY KEY,
title TEXT,
uploader TEXT,
upload_time TIMESTAMP,
tags SET<TEXT>
);
-- 쿼리 2번용 테이블
CREATE TABLE videos_by_user (
uploader TEXT,
upload_time TIMESTAMP,
video_id UUID,
title TEXT,
PRIMARY KEY ( (uploader), upload_time )
) WITH CLUSTERING ORDER BY (upload_time DESC);
-- 쿼리 3번용 테이블
CREATE TABLE videos_by_tag (
tag TEXT,
upload_time TIMESTAMP,
video_id UUID,
title TEXT,
uploader TEXT,
PRIMARY KEY ( (tag), upload_time )
) WITH CLUSTERING ORDER BY (upload_time DESC);
하나의 동영상을 업로드할 때, 위 3개 테이블에 모두 데이터를 써야 합니다. 쓰기 비용이 비싸지는 대신, 어떤 조회든 단일 테이블에서 매우 빠르게 읽을 수 있습니다. 카산드라는 쓰기 성능이 매우 뛰어나므로 이러한 접근이 매우 효과적입니다.
매우 중요한 질문입니다. RDBMS의 트랜잭션에 익숙하다면 당연히 가져야 할 의문입니다. 만약 위 시나리오에서 videos_by_id, videos_by_user 테이블에는 쓰기 성공했지만, videos_by_tag 테이블 쓰기에서 장애가 발생하면 데이터 정합성이 깨지게 됩니다.
카산드라는 이 문제를 해결하기 위해 BATCH 구문을 제공합니다. BATCH는 여러 쓰기 작업을 하나의 원자적(Atomic) 단위로 묶어줍니다.
BEGIN BATCH
-- 쿼리 1번용 테이블에 쓰기
INSERT INTO videos_by_id (video_id, title, uploader, ...)
VALUES (uuid(), '카산드라 모델링', 'gemini', ...);
-- 쿼리 2번용 테이블에 쓰기
INSERT INTO videos_by_user (uploader, upload_time, video_id, title)
VALUES ('gemini', toTimestamp(now()), uuid(), '카산드라 모델링');
-- 쿼리 3번용 테이블에 쓰기
INSERT INTO videos_by_tag (tag, upload_time, video_id, title, uploader)
VALUES ('cassandra', toTimestamp(now()), '카산드라 모델링', 'gemini');
APPLY BATCH;
기본값인 LOGGED BATCH는 코디네이터 노드가 이 배치 작업을 실행하기 전에 배치 로그를 먼저 기록합니다. 만약 코디네이터 노드가 배치 실행 중 다운되더라도, 다른 노드가 이 로그를 보고 중단된 작업을 마저 실행하여 결국에는 모든 쓰기가 적용됨을 보장합니다.
주의: BATCH는 성능 향상을 위한 기능이 아닙니다. 오히려 코디네이터에 부하를 더 주므로, 여러 파티션에 걸친 소수의 쓰기 작업에 대한 원자성을 보장할 때만 사용해야 합니다. 단일 파티션에 대한 여러 쓰기는 BATCH 없이 실행하는 것이 더 효율적입니다.
CQL은 SQL과 매우 유사해 보이지만, 분산 시스템의 제약을 이해하고 사용해야 합니다.
WHERE절의 제약: 파티션 키 없이는 조회할 수 없습니다. 클러스터링 키는 파티션 키가 지정된 상태에서 범위 조회가 가능합니다.JOIN, 그룹 함수(일부) 미지원: JOIN은 지원되지 않으며, GROUP BY 같은 집계 연산도 제한적입니다. 이런 연산은 Spark 같은 별도의 분석 도구를 사용하는 것이 일반적입니다.ALLOW FILTERING 주의: 카산드라가 비효율적인 쿼리(e.g., 파티션 키 없이 클러스터링 키로만 조회)를 막지만, ALLOW FILTERING을 붙이면 강제로 실행할 수 있습니다. 이는 전체 클러스터를 스캔하는 재앙을 초래할 수 있으므로, 개발 환경에서 테스트 용도로만 사용하고 운영 코드에는 절대 포함해서는 안 됩니다.Q: "Cassandra 데이터 모델링에서 가장 중요한 원칙은 무엇이며, RDBMS와 어떻게 다른가요?"
A: "가장 중요한 원칙은 '쿼리 우선 모델링(Query-First Modeling)'입니다. RDBMS는 데이터의 중복을 최소화하는 정규화를 우선하지만, 카산드라는 애플리케이션의 데이터 조회 패턴, 즉 쿼리를 먼저 정의하고 각 쿼리에 최적화된 테이블을 만드는 것을 목표로 합니다. 이 과정에서 데이터 중복이 발생하는 '역정규화'는 성능을 위해 적극적으로 권장됩니다. 이는 분산 환경에서 극심한 성능 저하를 유발하는 JOIN 연산을 피하기 위한 근본적인 설계 철학의 차이에서 비롯됩니다."
Q: "역정규화를 통해 여러 테이블에 데이터를 중복 저장할 때, 쓰기 과정에서 일부만 성공하고 실패하면 데이터 불일치 문제가 발생할 수 있습니다. 이 문제는 어떻게 해결할 수 있나요?"
A: "그 문제는 `BATCH` 구문을 사용하여 해결할 수 있습니다. `BATCH`는 여러 개의 INSERT, UPDATE, DELETE 문을 하나의 원자적(Atomic) 단위로 묶어줍니다. 기본적으로 사용되는 `LOGGED BATCH`의 경우, 코디네이터 노드가 배치 작업을 실행하기 전에 배치 로그를 먼저 남깁니다. 만약 배치 실행 중 코디네이터에 장애가 발생하더라도, 다른 노드가 이 로그를 확인하고 중단된 작업을 마저 수행하여 '결국에는 모든 쓰기가 적용됨'을 보장해 데이터 불일치 문제를 방지할 수 있습니다. 다만 `BATCH`는 성능이 아닌 원자성을 위한 기능이므로, 여러 파티션에 걸친 소수의 쓰기 작업에 한해 신중하게 사용해야 합니다."