문서·와이드 컬럼: MongoDB·Cassandra
스키마가 유연한 문서 데이터베이스와, 여러 노드에 쓰기를 퍼뜨리는 와이드 컬럼 데이터베이스의 내부 구조다.
목차 (8)
MongoDB: 행 대신 문서
관계형 데이터베이스는 주문 하나를 주문 표, 주문 항목 표, 배송지 표에 나눠 담고 읽을 때 조인으로 다시 붙인다. 문서 데이터베이스는 앱이 다루는 모양 그대로, 주문 하나를 중첩된 객체 하나로 담는다. MongoDB의 문서는 JSON처럼 생겼지만 실제로는 BSON이라는 이진 형식으로 저장된다. BSON은 길이 정보가 앞에 붙어 필드를 건너뛰며 읽기 쉽고, 날짜·64비트 정수·소수(Decimal128)·이진 데이터처럼 JSON에 없는 자료형을 담는다. 문서마다 고유한 _id가 있고 크기는 최대 16MB다.
스키마가 없다는 말은 반만 맞다. 데이터베이스가 스키마를 강제하지 않을 뿐(원하면 JSON Schema 검증을 걸 수 있다), 설계는 쿼리 모양에서 출발해야 한다. 데이터 모델링 문서가 권하는 대로 함께 읽고 함께 바뀌는 것은 내장하고, 한없이 자라는 목록(댓글 수만 개)이나 여러 문서가 공유하는 값은 따로 두고 참조한다. 문서 하나에 대한 쓰기는 늘 원자적이라, 내장을 잘하면 트랜잭션 없이도 일관성이 맞는다. 여러 문서에 걸친 트랜잭션도 4.0부터 지원하지만 비용이 크므로 기본 도구는 아니다.
MongoDB: WiredTiger와 레플리카 세트
MongoDB의 저장 엔진은 WiredTiger다. 컬렉션과 인덱스가 각각 B-트리 파일이고, 동시성은 문서 단위로 다룬다. 두 클라이언트가 같은 컬렉션의 다른 문서를 동시에 고칠 수 있고, 읽기는 MVCC 스냅샷을 본다. 컬렉션은 기본으로 snappy 블록 압축, 인덱스는 접두사 압축을 쓴다. 내구성은 저널(WAL)과 60초마다의 체크포인트가 함께 지킨다.
드라이버가 insertOne을 primary로 보낸다. 레플리카 세트에서 쓰기를 받는 것은 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 서버(이것도 레플리카 세트)에 있는 "어느 범위가 어느 샤드에" 표를 보고 쿼리를 보낸다.
청크의 기본 크기는 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의 분산 구조 문서대로, 파티션 키를 Murmur3로 해시한 64비트 토큰이 링 위의 자리다. 노드 하나가 링의 한 구간만 맡으면 노드를 더하거나 뺄 때 이웃 하나에만 부담이 몰리므로, 노드마다 토큰을 여러 개 가져 링을 잘게 나눠 맡는다. 이것이 vnode(가상 노드)이고, 4.0부터 기본 토큰 수는 16이다. 복제 계수(RF)는 키스페이스마다 정하며, 운영에서는 데이터센터별로 RF를 정하고 랙을 나눠 놓는 NetworkTopologyStrategy를 쓴다. 노드끼리는 gossip으로 서로의 상태를 퍼뜨린다.
Cassandra: 쓰기는 덧붙이기, 삭제는 툼스톤
어느 노드든 요청을 받으면 그 요청의 코디네이터가 되어 복제본들에 전달한다. 복제본 안의 저장 엔진은 LSM 구조 그대로다. commit log에 덧붙이고 memtable에 넣으면 쓰기가 끝나고, memtable은 차면 불변 SSTable 파일로 flush된다. 원리는 저장 엔진 노트의 LSM 트리와 같다.
클라이언트가 CONSISTENCY QUORUM으로 INSERT를 보낸다. 받은 노드(N1)가 코디네이터가 되어, 파티션 키의 토큰으로 복제본 셋(N2·N3·N4)을 찾아 동시에 보낸다.
세 가지를 기억할 만하다. 첫째, 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일 때 읽기와 쓰기가 적어도 한 복제본에서 겹치므로 방금 쓴 값을 읽는다.
복제본이 서로 어긋나면 세 장치가 맞춘다. 쓰기 때 죽어 있던 복제본 몫을 코디네이터가 들고 있다 전하는 hinted handoff, 읽다가 다른 값을 발견하면 고쳐 쓰는 read repair, 그리고 머클 트리로 복제본 전체를 비교하는 repair다. Cassandra의 보장 문서가 밝히듯 기본은 결국 일관성(eventual consistency)이고, 조건부 갱신(IF NOT EXISTS)처럼 꼭 필요한 곳에서만 Paxos 기반 경량 트랜잭션을 쓴다.
ScyllaDB: 코어마다 하나의 샤드
ScyllaDB는 Cassandra와 같은 데이터 모델, 같은 CQL, 같은 SSTable 계열 구조를 C++로 다시 구현한 데이터베이스다. 가장 큰 차이는 실행 구조다. Seastar 프레임워크 위에서 CPU 코어 하나를 샤드 하나로 다룬다. 노드가 맡은 토큰 범위를 다시 코어마다 나눠, 코어는 자기 메모리와 자기 데이터만 만지며 락 없이 돈다. 다른 코어의 데이터가 필요하면 공유 메모리를 건드리는 대신 그 코어에 메시지를 보낸다.
운영체제 페이지 캐시 대신 자체 캐시를 쓰고, 디스크 I/O도 자체 스케줄러로 우선순위를 나눠 compaction 같은 배경 작업이 사용자 쿼리 지연을 흔들지 않게 한다. 같은 하드웨어에서 더 적은 노드로 같은 처리량을 내는 것이 목표다. ScyllaDB의 아키텍처 문서에 세부가 있다.
라이선스는 바뀌었다. ScyllaDB는 오랫동안 AGPL 오픈 소스판을 냈지만, 2024년 12월 소스 공개(source-available) 라이선스로 옮긴다고 발표했다. AGPL 판은 6.2가 마지막이고, 2025년 4월의 2025.1부터는 한 가지 판이 소스 공개 라이선스로 나오며 일정 규모까지 무료로 쓸 수 있다. 오픈 소스 라이선스가 꼭 필요하면 Apache 2.0인 Cassandra가 남는다. (2026년 10월 기준)
고르는 법
| 기준 | MongoDB | Cassandra / ScyllaDB |
|---|---|---|
| 데이터 모델 | 중첩 문서, 필드마다 보조 인덱스 | 파티션 키 + 클러스터링 키, 쿼리마다 표 |
| 쓰기 구조 | primary 하나가 받음(샤드마다) | 모든 노드가 대등, 어디로든 씀 |
| 저장 엔진 | WiredTiger B-트리 | LSM(commit log · memtable · SSTable) |
| 일관성 | write/read concern으로 고름, 과반이 기본 | 쿼리마다 일관성 수준, 결국 일관성이 기본 |
| 잘 맞는 곳 | 모양이 다양하고 자주 바뀌는 객체, 풍부한 쿼리와 집계 | 쓰기가 매우 많은 시계열·이벤트·메시지, 여러 데이터센터에 걸친 상시 가용 |
| 조심할 점 | 샤드 키 선택, 무한히 자라는 배열 | 파티션 키 없는 쿼리 불가, 툼스톤과 repair, 큰 파티션 |
| 라이선스 | SSPL | Cassandra는 Apache 2.0, ScyllaDB는 소스 공개 |
쿼리 모양을 미리 다 알 수 없고 한 객체를 통째로 읽고 쓰는 일이 많다면 문서 데이터베이스가 편하다. 반대로 쿼리는 몇 가지로 정해져 있는데 쓰기가 초당 수십만 건이고 어느 데이터센터가 꺼져도 멈추면 안 된다면 Cassandra 계열이 맞다. 둘 다 조인이 약하므로, 관계가 복잡하게 얽힌 데이터라면 관계형 노트의 엔진부터 따져 본다.
- MongoDB Manual — MongoDB
- Documents — MongoDB Manual
- BSON Specification — bsonspec.org
- Data Modeling — MongoDB Manual
- WiredTiger Storage Engine — MongoDB Manual
- Journaling — MongoDB Manual
- Replication — MongoDB Manual
- Replica Set Oplog — MongoDB Manual
- Write Concern — MongoDB Manual
- Read Concern — MongoDB Manual
- Sharding — MongoDB Manual
- Data Partitioning with Chunks — MongoDB Manual
- Server Side Public License FAQ — MongoDB
- Bigtable: A Distributed Storage System for Structured Data — Chang 외, Google Research
- Cassandra: A Decentralized Structured Storage System — Lakshman·Malik, LADIS 2009
- Dynamo (Architecture) — Apache Cassandra Documentation
- Storage Engine — Apache Cassandra Documentation
- Guarantees — Apache Cassandra Documentation
- Seastar — seastar.io
- Shard-per-Core Architecture — ScyllaDB
- ScyllaDB Architecture — ScyllaDB Documentation
- Why We're Moving to a Source Available License — ScyllaDB 블로그(2024.12)