테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

문서·와이드 컬럼: MongoDB·Cassandra

스키마가 유연한 문서 데이터베이스와, 여러 노드에 쓰기를 퍼뜨리는 와이드 컬럼 데이터베이스의 내부 구조다.

목차 (8)
  1. MongoDB: 행 대신 문서
  2. MongoDB: WiredTiger와 레플리카 세트
  3. MongoDB: 샤딩과 청크
  4. Cassandra: 주인 없는 토큰 링
  5. Cassandra: 쓰기는 덧붙이기, 삭제는 툼스톤
  6. Cassandra: 쿼리마다 고르는 일관성
  7. ScyllaDB: 코어마다 하나의 샤드
  8. 고르는 법

MongoDB: 행 대신 문서

관계형 데이터베이스는 주문 하나를 주문 표, 주문 항목 표, 배송지 표에 나눠 담고 읽을 때 조인으로 다시 붙인다. 문서 데이터베이스는 앱이 다루는 모양 그대로, 주문 하나를 중첩된 객체 하나로 담는다. MongoDB의 문서는 JSON처럼 생겼지만 실제로는 BSON이라는 이진 형식으로 저장된다. BSON은 길이 정보가 앞에 붙어 필드를 건너뛰며 읽기 쉽고, 날짜·64비트 정수·소수(Decimal128)·이진 데이터처럼 JSON에 없는 자료형을 담는다. 문서마다 고유한 _id가 있고 크기는 최대 16MB다.

MongoDB 문서 설계: 왼쪽은 주문 문서 하나에 주문 항목 배열과 배송지 객체를 내장해 한 번에 읽고 한 번에 원자적으로 쓴다. 오른쪽은 사용자와 주문을 다른 컬렉션에 두고 주문의 user_id가 사용자의 _id를 가리키며, 함께 읽으려면 두 번 묻거나 $lookup으로 합친다내장 (embed)참조 (reference)orders 컬렉션{ _id: 1001,user: "kim",items: [{ sku: "A1", qty: 2 },{ sku: "B7", qty: 1 } ],ship: { city: "서울" } }users 컬렉션{ _id: "kim",name: "김", tier: "gold" }orders 컬렉션{ _id: 1001,user_id: "kim",total: 42000 }한 번 읽기, 한 문서 쓰기는 원자적함께 읽고 함께 바뀌는 것은 한 문서에 둔다 (문서 하나 최대 16MB)끝없이 자라는 목록이나 여러 곳에서 공유하는 값은 따로 두고 참조한다
문서 설계의 기본 질문은 내장할지 참조할지다. 같은 컬렉션의 문서끼리 필드가 달라도 되지만, 설계 원칙은 '함께 읽는 것을 함께 둔다'이다.

스키마가 없다는 말은 반만 맞다. 데이터베이스가 스키마를 강제하지 않을 뿐(원하면 JSON Schema 검증을 걸 수 있다), 설계는 쿼리 모양에서 출발해야 한다. 데이터 모델링 문서가 권하는 대로 함께 읽고 함께 바뀌는 것은 내장하고, 한없이 자라는 목록(댓글 수만 개)이나 여러 문서가 공유하는 값은 따로 두고 참조한다. 문서 하나에 대한 쓰기는 늘 원자적이라, 내장을 잘하면 트랜잭션 없이도 일관성이 맞는다. 여러 문서에 걸친 트랜잭션도 4.0부터 지원하지만 비용이 크므로 기본 도구는 아니다.

MongoDB: WiredTiger와 레플리카 세트

MongoDB의 저장 엔진은 WiredTiger다. 컬렉션과 인덱스가 각각 B-트리 파일이고, 동시성은 문서 단위로 다룬다. 두 클라이언트가 같은 컬렉션의 다른 문서를 동시에 고칠 수 있고, 읽기는 MVCC 스냅샷을 본다. 컬렉션은 기본으로 snappy 블록 압축, 인덱스는 접두사 압축을 쓴다. 내구성은 저널(WAL)과 60초마다의 체크포인트가 함께 지킨다.

MongoDB 쓰기 단계 그림: 드라이버가 primary에 쓰고, primary는 WiredTiger 캐시와 oplog, 저널에 기록한다. secondary 둘이 oplog를 가져가 적용하고, 과반이 기록하면 응답한다. 체크포인트가 캐시를 데이터 파일에 쓴다드라이버primaryWiredTiger 캐시저널(WAL)oplog데이터 파일 (체크포인트)secondary 1oplog 적용secondary 2oplog 적용insertOneoplog를 끌어와 적용응답 (과반 기록)primary + secondary 1 = 과반
1 / 5

드라이버가 insertOne을 primary로 보낸다. 레플리카 세트에서 쓰기를 받는 것은 primary 하나뿐이다.

w: majority 쓰기의 길. 응답이 오는 시점에는 과반의 멤버가 이 쓰기를 갖고 있으므로, primary가 죽어 다른 멤버가 뽑혀도 이 쓰기는 되돌려지지 않는다.

운영 환경의 MongoDB는 거의 늘 레플리카 세트다. primary 하나가 쓰기를 받고, 바뀐 내용을 oplog라는 특별한 고정 크기 컬렉션에 남긴다. secondary는 그 oplog를 끌어와 같은 순서로 적용한다. oplog 항목은 몇 번 재생해도 결과가 같게(멱등) 적힌다. primary가 응답하지 않으면 남은 멤버가 Raft와 비슷한 프로토콜로 선거를 해 새 primary를 뽑는다.

얼마나 확실해야 응답할지는 앱이 고른다. write concern은 몇 멤버가 받아야 쓰기를 성공으로 볼지이고, 기본값은 w: "majority"다. w: 1이면 primary만 받아도 응답하므로 빠르지만, 그 직후 primary가 죽으면 새 primary에 없는 쓰기는 롤백된다. read concern은 어떤 데이터를 읽을지다. local은 이 멤버가 가진 최신 값(롤백될 수도 있음), majority는 과반이 가진 값만, linearizable은 읽는 순간 가장 최근의 과반 커밋을 보장한다. 그리고 read preference로 secondary에서 읽게 하면 부하는 나뉘지만 조금 늦은 값을 볼 수 있다.

MongoDB: 샤딩과 청크

한 레플리카 세트를 넘어서면 샤딩으로 나눈다. 컬렉션마다 샤드 키를 정하면, 키 공간이 연속된 범위(청크)로 쪼개져 여러 샤드에 흩어진다. 앱은 라우터인 mongos에 연결하고, mongos는 config 서버(이것도 레플리카 세트)에 있는 "어느 범위가 어느 샤드에" 표를 보고 쿼리를 보낸다.

MongoDB 샤딩: 앱은 라우터 mongos에 묻고, mongos는 config 서버의 표에서 샤드 키 범위(청크)마다 어느 샤드에 있는지 찾아 그 샤드로만 쿼리를 보낸다. 샤드 A는 MinKey부터 f, f부터 k, 샤드 B는 k부터 t, 샤드 C는 t부터 MaxKey를 맡고, 밸런서가 청크가 많은 샤드에서 적은 샤드로 범위를 옮긴다. 샤드마다 레플리카 세트다앱mongosconfig 서버 (청크 표)find({ user: "lee" }) → 범위 k–p → 샤드 B로만샤드 A청크 MinKey–f청크 f–k샤드 B청크 k–p청크 p–t샤드 C청크 t–MaxKey밸런서: 청크가 많은 샤드에서 적은 샤드로 옮긴다샤드 하나 = 레플리카 세트 하나. 샤드 키가 없는 쿼리는 모든 샤드로 퍼진다
샤드 키가 들어간 쿼리는 해당 범위의 샤드 하나로만 간다. 키가 없는 쿼리는 모든 샤드에 물어 결과를 합친다(scatter-gather).

청크의 기본 크기는 128MB이고, 백그라운드의 밸런서가 샤드 사이 데이터 양 차이가 기준을 넘으면 범위를 옮긴다. 샤드 키가 모든 것을 정한다. 값의 종류가 적거나(국가 코드), 한쪽으로만 늘어나는 키(생성 시각)는 마지막 범위 하나에 쓰기가 몰린다. 이럴 때는 해시 샤딩으로 키를 고르게 흩거나, 자주 묻는 조건과 잘 흩어지는 값을 묶은 복합 키를 쓴다.

라이선스도 알아 둘 만하다. MongoDB Community Server는 2018년 10월부터 SSPL이다. 소스는 공개되어 있고 자기 서비스에 쓰는 데는 제약이 없지만, MongoDB 자체를 서비스로 제공하면 그 서비스 전체 소스를 공개해야 하는 조건 때문에 OSI 기준의 오픈 소스로 인정되지 않는다.

Cassandra: 주인 없는 토큰 링

와이드 컬럼 데이터베이스는 이름과 달리 "열이 많은 표"라기보다 "파티션 키로 나뉜, 정렬된 행 묶음"이다. Apache Cassandra는 Facebook에서 시작했고, 두 논문을 합친 설계로 유명하다. 데이터를 여러 노드에 나누고 복제하는 방식은 Amazon의 Dynamo 논문에서, 노드 하나 안의 저장 구조(memtable과 SSTable)는 Google의 Bigtable 논문에서 가져왔다. 이 조합은 처음 공개된 Lakshman·Malik의 Cassandra 논문에 정리되어 있다.

표의 기본 키는 두 부분이다. 파티션 키는 행이 어느 노드에 갈지를 정하고, 클러스터링 키는 한 파티션 안에서 행이 정렬될 순서를 정한다. 예를 들어 (센서 ID, 시각)을 키로 두면 센서 하나의 기록이 한 파티션에 시각 순서로 모여, "센서 7의 최근 100개"를 한 번에 읽는다. 대신 파티션 키 없이 묻는 쿼리는 원칙적으로 할 수 없다. 쿼리마다 맞는 표를 따로 만드는 것(비정규화)이 Cassandra의 설계 방식이다.

Cassandra 토큰 링: 노드 여섯 개가 링 위에 있고, 파티션 키 user:42를 Murmur3로 해시한 토큰은 N1과 N2 사이에 떨어진다. 그 자리에서 시계 방향으로 처음 만나는 N2, N3, N4가 복제 계수 3의 복제본이다N1N2N3N4N5N6토큰 −2⁶³ … 2⁶³−1시계 방향 →① 파티션 키 user:42Murmur3 해시 → 토큰(빨간 점)② 시계 방향으로 RF = 3개N2 · N3 · N4가 같은 행을 갖는다③ 주인도 리더도 없다아무 노드나 코디네이터가 되어세 복제본에 함께 보낸다vnode실제로는 노드마다 토큰을 여러 개 가져링을 잘게 나눠 맡는다(그림은 노드당 1개)
파티션 키의 토큰이 링 위의 자리를 정하고, 그 자리에서 시계 방향으로 RF개 노드가 복제본을 갖는다. 모든 노드가 대등해 리더 선거가 없다.

Cassandra의 분산 구조 문서대로, 파티션 키를 Murmur3로 해시한 64비트 토큰이 링 위의 자리다. 노드 하나가 링의 한 구간만 맡으면 노드를 더하거나 뺄 때 이웃 하나에만 부담이 몰리므로, 노드마다 토큰을 여러 개 가져 링을 잘게 나눠 맡는다. 이것이 vnode(가상 노드)이고, 4.0부터 기본 토큰 수는 16이다. 복제 계수(RF)는 키스페이스마다 정하며, 운영에서는 데이터센터별로 RF를 정하고 랙을 나눠 놓는 NetworkTopologyStrategy를 쓴다. 노드끼리는 gossip으로 서로의 상태를 퍼뜨린다.

Cassandra: 쓰기는 덧붙이기, 삭제는 툼스톤

어느 노드든 요청을 받으면 그 요청의 코디네이터가 되어 복제본들에 전달한다. 복제본 안의 저장 엔진은 LSM 구조 그대로다. commit log에 덧붙이고 memtable에 넣으면 쓰기가 끝나고, memtable은 차면 불변 SSTable 파일로 flush된다. 원리는 저장 엔진 노트의 LSM 트리와 같다.

Cassandra 쓰기·읽기 단계 그림: 코디네이터 N1이 복제본 N2·N3·N4에 쓰기를 보내고, 복제본 안에서는 commit log와 memtable에 기록된다. 과반이 답하면 성공이고, memtable은 SSTable로 flush된다. 삭제는 툼스톤으로 쓰이고, 읽기는 여러 곳의 셀을 타임스탬프로 합친다클라이언트N1코디네이터N2N3N42/3 응답= 성공N4 몫은 힌트로복제본 N2 안commit log (디스크)memtable (메모리)name='lee' @t5툼스톤 @t9① 이어 쓰기② 메모리에 넣기SSTable (불변)name='kim' @t1name='lee' @t5flusht9 > t5 > t1→ 없음
1 / 6

클라이언트가 CONSISTENCY QUORUM으로 INSERT를 보낸다. 받은 노드(N1)가 코디네이터가 되어, 파티션 키의 토큰으로 복제본 셋(N2·N3·N4)을 찾아 동시에 보낸다.

Cassandra는 쓰기를 읽기 없이 덧붙이기만 하므로 빠르다. 그 대신 읽을 때 여러 곳의 조각을 타임스탬프로 합치고, 삭제는 툼스톤으로 미뤄 처리한다.

세 가지를 기억할 만하다. 첫째, commit log는 기본 설정(periodic)에서 10초마다 fsync한다. 쓰기 응답이 디스크 동기화를 기다리지 않으므로, 한 노드의 내구성보다 여러 복제본이 함께 갖는 것에 기댄다. 둘째, 같은 셀에 대한 충돌은 타임스탬프가 큰 쪽이 이기는 last-write-wins다. 서버 시계가 어긋나면 늦게 쓴 값이 지는 일이 생길 수 있다. 셋째, 삭제는 툼스톤이다. 툼스톤은 gc_grace_seconds(기본 10일)가 지나야 compaction에서 지워지는데, 그 사이에 꺼져 있던 복제본이 돌아와 수리(repair)를 거치지 않으면 지운 데이터가 되살아난다. 그래서 Cassandra 운영에는 정기적인 anti-entropy repair가 필수이고, 큐처럼 넣고 지우기를 반복하는 쓰임은 툼스톤이 쌓여 읽기가 느려지는 대표적 안티 패턴이다.

Cassandra: 쿼리마다 고르는 일관성

Cassandra는 쓰기를 모든 복제본에 보내지만, 코디네이터가 몇 개의 답을 기다릴지는 쿼리마다 정하는 일관성 수준(consistency level)이다. ONE, QUORUM(과반), ALL, 그리고 데이터센터 안에서만 과반을 세는 LOCAL_QUORUM 등이 있다. 복제본 수를 RF, 쓰기와 읽기가 기다리는 수를 W와 R이라 하면 W + R > RF일 때 읽기와 쓰기가 적어도 한 복제본에서 겹치므로 방금 쓴 값을 읽는다.

Cassandra 일관성 수준 조합: 복제본 셋에서 쓰기 ONE과 읽기 ONE은 서로 다른 복제본을 볼 수 있어 옛 값을 읽을 수 있다. 쓰기 QUORUM과 읽기 QUORUM은 합이 4로 3보다 커서 적어도 한 복제본이 겹쳐 최신 값을 읽는다. 쓰기 ALL과 읽기 ONE도 겹치지만 복제본 하나만 죽어도 쓰기가 실패한다복제본 (RF = 3)쓰기가 닿음읽기가 물음W = ONE, R = ONEW + R = 2 ≤ 3옛 값을 읽을 수 있다W = QUORUM, R = QUORUMW + R = 4 > 3겹치는 곳에서 최신 값W = ALL, R = ONEW + R = 4 > 3하나만 죽어도 쓰기 실패
RF = 3에서 일관성 수준 조합. 쓰기와 읽기가 겹치는 복제본이 하나라도 있으면 최신 값을 만난다. 겹침을 늘릴수록 지연과 장애에 약해진다.

복제본이 서로 어긋나면 세 장치가 맞춘다. 쓰기 때 죽어 있던 복제본 몫을 코디네이터가 들고 있다 전하는 hinted handoff, 읽다가 다른 값을 발견하면 고쳐 쓰는 read repair, 그리고 머클 트리로 복제본 전체를 비교하는 repair다. Cassandra의 보장 문서가 밝히듯 기본은 결국 일관성(eventual consistency)이고, 조건부 갱신(IF NOT EXISTS)처럼 꼭 필요한 곳에서만 Paxos 기반 경량 트랜잭션을 쓴다.

ScyllaDB: 코어마다 하나의 샤드

ScyllaDB는 Cassandra와 같은 데이터 모델, 같은 CQL, 같은 SSTable 계열 구조를 C++로 다시 구현한 데이터베이스다. 가장 큰 차이는 실행 구조다. Seastar 프레임워크 위에서 CPU 코어 하나를 샤드 하나로 다룬다. 노드가 맡은 토큰 범위를 다시 코어마다 나눠, 코어는 자기 메모리와 자기 데이터만 만지며 락 없이 돈다. 다른 코어의 데이터가 필요하면 공유 메모리를 건드리는 대신 그 코어에 메시지를 보낸다.

ScyllaDB shard-per-core 구조: 왼쪽 Cassandra는 JVM 프로세스 하나에서 스레드 풀이 공유 힙을 함께 쓰므로 락과 가비지 컬렉션 멈춤이 생긴다. 오른쪽 ScyllaDB는 CPU 코어 하나가 샤드 하나로, 자기 메모리와 자기 토큰 범위, 자기 I/O 큐를 갖고 락 없이 돌며, 다른 코어의 데이터가 필요하면 메시지를 보낸다Cassandra (JVM)스레드스레드스레드스레드공유 힙 (memtable · 캐시)락 · GC 멈춤운영체제 페이지 캐시에 기댐ScyllaDB (C++, Seastar)코어 0자기메모리토큰범위I/O 큐코어 1자기메모리토큰범위I/O 큐코어 2자기메모리토큰범위I/O 큐코어 3자기메모리토큰범위I/O 큐다른 코어 데이터는 메시지로같은 CQL·같은 SSTable 모델, 다른 실행 구조
JVM의 스레드 풀과 공유 힙 대신, 코어마다 자기 몫을 따로 갖는 shared-nothing 구조. 락 경합과 가비지 컬렉션 멈춤이 사라지는 대신 코어 사이 데이터 쏠림을 조심해야 한다.

운영체제 페이지 캐시 대신 자체 캐시를 쓰고, 디스크 I/O도 자체 스케줄러로 우선순위를 나눠 compaction 같은 배경 작업이 사용자 쿼리 지연을 흔들지 않게 한다. 같은 하드웨어에서 더 적은 노드로 같은 처리량을 내는 것이 목표다. ScyllaDB의 아키텍처 문서에 세부가 있다.

라이선스는 바뀌었다. ScyllaDB는 오랫동안 AGPL 오픈 소스판을 냈지만, 2024년 12월 소스 공개(source-available) 라이선스로 옮긴다고 발표했다. AGPL 판은 6.2가 마지막이고, 2025년 4월의 2025.1부터는 한 가지 판이 소스 공개 라이선스로 나오며 일정 규모까지 무료로 쓸 수 있다. 오픈 소스 라이선스가 꼭 필요하면 Apache 2.0인 Cassandra가 남는다. (2026년 10월 기준)

고르는 법

문서·와이드 컬럼 엔진 고르기: 쓰기가 아주 많고 한시도 멈추면 안 되나? 예면 Cassandra / ScyllaDB. 모양이 제각각인 객체를 통째로 읽고 쓰나? 예면 MongoDB. 모두 아니면 관계가 얽혀 있으면 관계형부터 따진다쓰기가 아주 많고 한시도 멈추면 안 되나?예Cassandra / ScyllaDB아니오모양이 제각각인 객체를 통째로 읽고 쓰나?예MongoDB아니오관계가 얽혀 있으면 관계형부터 따진다
문서와 와이드 컬럼 사이에서 고르는 순서. 쓰기 규모와 가용성 요구가 먼저다.
기준MongoDBCassandra / ScyllaDB
데이터 모델중첩 문서, 필드마다 보조 인덱스파티션 키 + 클러스터링 키, 쿼리마다 표
쓰기 구조primary 하나가 받음(샤드마다)모든 노드가 대등, 어디로든 씀
저장 엔진WiredTiger B-트리LSM(commit log · memtable · SSTable)
일관성write/read concern으로 고름, 과반이 기본쿼리마다 일관성 수준, 결국 일관성이 기본
잘 맞는 곳모양이 다양하고 자주 바뀌는 객체, 풍부한 쿼리와 집계쓰기가 매우 많은 시계열·이벤트·메시지, 여러 데이터센터에 걸친 상시 가용
조심할 점샤드 키 선택, 무한히 자라는 배열파티션 키 없는 쿼리 불가, 툼스톤과 repair, 큰 파티션
라이선스SSPLCassandra는 Apache 2.0, ScyllaDB는 소스 공개

쿼리 모양을 미리 다 알 수 없고 한 객체를 통째로 읽고 쓰는 일이 많다면 문서 데이터베이스가 편하다. 반대로 쿼리는 몇 가지로 정해져 있는데 쓰기가 초당 수십만 건이고 어느 데이터센터가 꺼져도 멈추면 안 된다면 Cassandra 계열이 맞다. 둘 다 조인이 약하므로, 관계가 복잡하게 얽힌 데이터라면 관계형 노트의 엔진부터 따져 본다.