3. 데이터 모델링: 기본키, 역정규화, CQL

카산드라를 처음 접하는 개발자가 가장 많이 하는 실수는 RDBMS처럼 데이터 모델링을 하는 것입니다. 카산드라의 성능을 100% 활용하기 위해서는 RDBMS의 상식을 버리고, 카산드라의 철학에 맞는 새로운 접근법을 배워야 합니다.

"어떻게 저장할까?"가 아닌 "어떻게 조회할까?"

RDBMS는 데이터를 정규화하여 중복을 최소화하는 데 집중합니다. 그리고 필요할 때 JOIN을 사용해 원하는 데이터를 조합하여 조회합니다. 하지만 카산드라는 JOIN을 지원하지 않으며, 분산 환경에서 JOIN은 극심한 성능 저하를 유발합니다.

따라서 카산드라의 데이터 모델링은 "애플리케이션에서 어떤 쿼리로 데이터를 조회할 것인가?"라는 질문에서 시작합니다. 즉, 쿼리 우선 모델링(Query-First Modeling)이 핵심입니다.

카산드라 모델링 3단계 원칙

  1. Workload 파악: 우리 서비스에서 가장 빈번하고 중요한 데이터 조회 패턴(Query)이 무엇인지 모두 나열합니다.
  2. 테이블 생성: 각 조회 패턴에 최적화된 테이블을 각각 만듭니다. 데이터가 중복되어도 괜찮습니다. 오히려 권장됩니다. (역정규화)
  3. 기본키 설계: 각 테이블의 조회 조건(WHERE절)에 맞춰 파티션 키와 클러스터링 키를 신중하게 설계합니다.

카산드라의 심장, 기본키 (Primary Key)

카산드라에서 기본키는 단순히 데이터를 식별하는 것을 넘어, 데이터가 클러스터의 어느 노드에 저장되고(분산), 그 노드 안에서 어떻게 정렬될지(정렬)를 결정하는 핵심적인 역할을 합니다.

기본키는 하나 이상의 컬럼으로 구성되며, 파티션 키(Partition Key)와 선택적으로 클러스터링 키(Clustering Key)로 나뉩니다.

PRIMARY KEY ( (partition_key_1, partition_key_2), clustering_key_1, clustering_key_2 )
-- 괄호로 묶인 부분이 파티션 키, 그 뒤가 클러스터링 키입니다.

1. 파티션 키 (Partition Key)

데이터가 어느 노드에 저장될지를 결정합니다. 카산드라는 파티션 키의 값을 해시하여 토큰을 만들고, 이 토큰을 기준으로 데이터를 링 위의 특정 노드에 분산시킵니다.

2. 클러스터링 키 (Clustering Key)

하나의 파티션 내에서 데이터가 어떤 순서로 디스크에 정렬될지를 결정합니다.

예시: 사용자별 게시물 테이블

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은 클러스터링 키입니다.

Partition Key: "gemini"
post_time: 2023-10-27 11:00 post_id: ..., title: "카산드라 최고!"
post_time: 2023-10-27 10:30 post_id: ..., title: "데이터 모델링"
post_time: 2023-10-26 18:00 post_id: ..., title: "첫 게시물"

"gemini"라는 사용자의 모든 게시물은 하나의 파티션에 모여있고, post_time을 기준으로 내림차순 정렬되어 저장됩니다. 따라서 "gemini의 최근 게시물 10개 가져오기" (SELECT * FROM user_posts WHERE username = 'gemini' LIMIT 10;) 같은 쿼리는 디스크를 순차적으로 딱 한 번만 읽어 매우 빠르게 처리됩니다.

역정규화 (Denormalization): 카산드라의 미덕

RDBMS에서는 죄악시되는 역정규화가 카산드라에서는 최고의 미덕입니다. JOIN이 없기 때문에, 필요한 모든 데이터를 읽기 편한 형태로 미리 테이블에 저장해두는 전략입니다.

시나리오: 동영상 서비스 모델링

RDBMS라면 videos, users, comments 테이블을 만들고 JOIN으로 해결하겠지만, 카산드라는 다릅니다.

필요한 쿼리 목록:

  1. 동영상 ID로 동영상 정보 조회하기
  2. 특정 사용자가 올린 최신 동영상 목록 조회하기
  3. 특정 태그가 달린 최신 동영상 목록 조회하기

각 쿼리에 맞는 테이블 생성:

-- 쿼리 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 (Cassandra Query Language)의 특징

CQL은 SQL과 매우 유사해 보이지만, 분산 시스템의 제약을 이해하고 사용해야 합니다.

면접관의 시선 💡

Q: "Cassandra 데이터 모델링에서 가장 중요한 원칙은 무엇이며, RDBMS와 어떻게 다른가요?"

A: "가장 중요한 원칙은 '쿼리 우선 모델링(Query-First Modeling)'입니다. RDBMS는 데이터의 중복을 최소화하는 정규화를 우선하지만, 카산드라는 애플리케이션의 데이터 조회 패턴, 즉 쿼리를 먼저 정의하고 각 쿼리에 최적화된 테이블을 만드는 것을 목표로 합니다. 이 과정에서 데이터 중복이 발생하는 '역정규화'는 성능을 위해 적극적으로 권장됩니다. 이는 분산 환경에서 극심한 성능 저하를 유발하는 JOIN 연산을 피하기 위한 근본적인 설계 철학의 차이에서 비롯됩니다."


Q: "역정규화를 통해 여러 테이블에 데이터를 중복 저장할 때, 쓰기 과정에서 일부만 성공하고 실패하면 데이터 불일치 문제가 발생할 수 있습니다. 이 문제는 어떻게 해결할 수 있나요?"

A: "그 문제는 `BATCH` 구문을 사용하여 해결할 수 있습니다. `BATCH`는 여러 개의 INSERT, UPDATE, DELETE 문을 하나의 원자적(Atomic) 단위로 묶어줍니다. 기본적으로 사용되는 `LOGGED BATCH`의 경우, 코디네이터 노드가 배치 작업을 실행하기 전에 배치 로그를 먼저 남깁니다. 만약 배치 실행 중 코디네이터에 장애가 발생하더라도, 다른 노드가 이 로그를 확인하고 중단된 작업을 마저 수행하여 '결국에는 모든 쓰기가 적용됨'을 보장해 데이터 불일치 문제를 방지할 수 있습니다. 다만 `BATCH`는 성능이 아닌 원자성을 위한 기능이므로, 여러 파티션에 걸친 소수의 쓰기 작업에 한해 신중하게 사용해야 합니다."