NoSQL과 CAP 정리 — RDBMS만으로 안 되는 순간
트래픽이 폭발적으로 늘어날 때, RDBMS의 수직 확장만으로는 왜 한계가 오는 걸까?
RDBMS 하나로 모든 걸 해결할 수 있으면 좋겠지만, 현실은 그렇지 않아요. 트래픽이 폭발하거나, 데이터 구조가 유동적이거나, 관계형 모델로는 표현하기 어려운 데이터를 다뤄야 할 때가 옵니다. 그래서 NoSQL이 등장했고, "RDBMS 대신 NoSQL을 쓰는 이유는 뭔가요?" 이 질문에 답하려면 각각의 트레이드오프를 이해해야 합니다.
RDBMS의 한계
RDBMS는 여전히 대부분의 서비스에서 중심이 되는 DB입니다. 하지만 몇 가지 상황에서 명확한 한계가 드러나요.
수평 확장이 어렵다
RDBMS는 기본적으로 수직 확장(Scale-Up) 에 의존합니다. CPU 더 좋은 거, 메모리 더 큰 거 — 장비를 업그레이드하는 방식이에요. 수평 확장(Scale-Out)을 하려면 샤딩을 직접 구현하거나 미들웨어를 끼워야 하는데, JOIN이나 트랜잭션이 샤드를 걸쳐야 하는 순간 복잡도가 급격히 올라갑니다. 분산 트랜잭션(2PC)까지 들어가면 성능도 크게 떨어져요.
스키마가 고정되어 있다
테이블을 만들 때 컬럼과 타입을 미리 정의해야 합니다. 요구사항이 바뀌어서 컬럼 하나 추가하려면 ALTER TABLE을 날려야 하고, 데이터가 수억 건이면 그 DDL 하나가 서비스 장애로 이어질 수도 있어요. 스타트업처럼 스키마가 자주 바뀌는 환경에서는 이게 꽤 부담입니다.
비정형 데이터를 다루기 힘들다
로그, IoT 센서 데이터, 소셜 미디어 피드처럼 구조가 제각각인 데이터를 RDBMS에 넣으려면 억지로 정규화하거나 JSON 컬럼에 때려 넣어야 합니다. 가능은 한데, 그게 최적의 선택인 경우는 드물어요.
NoSQL의 종류
NoSQL은 "Not Only SQL"의 약자입니다. SQL을 안 쓴다는 뜻이 아니라, SQL만으로는 부족한 영역을 커버한다는 의미에 가까워요. 데이터 모델에 따라 크게 네 가지로 나뉩니다.
Key-Value Store
가장 단순한 구조입니다. 키 하나에 값 하나. 해시맵이랑 본질적으로 같아요.
user:1001 → {"name": "홍길동", "age": 28}
session:abc123 → {"userId": 1001, "loginAt": "2026-03-15T09:00:00Z"}
** 대표 **: Redis, Memcached, Amazon DynamoDB
** 특징 **:
- 조회가 O(1)이라 극도로 빠릅니다
- 구조가 단순해서 수평 확장이 쉬워요
- 값에 대한 질의(쿼리)는 기본적으로 불가능합니다. 키를 모르면 찾을 수 없어요
** 용도 **: 캐시, 세션 저장소, 랭킹 보드, Rate Limiting
Document Store
JSON(또는 BSON) 형태의 문서를 저장합니다. Key-Value와 비슷하지만, 값의 내부 구조를 알고 있어서 필드 단위 질의가 가능하다는 게 핵심 차이예요.
{
"_id": "ObjectId('...')",
"title": "NoSQL과 CAP 정리",
"author": { "name": "홍길동", "email": "hong@dev.com" },
"tags": ["데이터베이스", "면접"],
"comments": [
{ "user": "김철수", "text": "잘 읽었습니다", "createdAt": "2026-03-15" }
]
}
** 대표 **: MongoDB, CouchDB, Amazon DocumentDB
** 특징 **:
- 스키마가 유연합니다. 같은 컬렉션 안에서도 문서마다 구조가 달라도 돼요
- 중첩 구조를 자연스럽게 표현할 수 있어서, RDBMS에서 여러 테이블로 나눠야 할 데이터를 하나의 문서에 담을 수 있습니다
- 인덱스를 걸 수 있어서 필드 기반 검색도 빠릅니다
** 용도 **: CMS, 사용자 프로필, 상품 카탈로그처럼 필드 구조가 유동적인 데이터
Wide-Column Store
행(Row)마다 컬럼이 다를 수 있는 구조입니다. 2차원 Key-Value라고 생각하면 돼요 — Row Key로 행을 찾고, 그 안에서 컬럼 이름으로 값을 찾습니다.
Row Key: user:1001
├── profile:name → "홍길동"
├── profile:age → 28
├── activity:lastLogin → "2026-03-15"
└── activity:postCount → 42
** 대표 **: Apache Cassandra, HBase, Google Bigtable
** 특징 **:
- 대용량 쓰기에 최적화되어 있습니다. Cassandra는 쓰기가 거의 항상 O(1)이에요
- 행마다 컬럼 구성이 달라도 되니까 스키마 유연성이 높아요
- 시계열 데이터에 잘 맞습니다
** 용도 **: 시계열 데이터(메트릭, 로그), 대규모 분석 플랫폼, 추천 시스템
Graph Store
노드(Node)와 엣지(Edge)로 데이터 간의 ** 관계** 자체를 일급 시민으로 다룹니다. RDBMS에서 JOIN 여러 번 해야 할 관계 탐색이 여기서는 자연스러워요.
(홍길동)-[:FOLLOWS]->(김철수)
(김철수)-[:LIKES]->(게시글A)
(게시글A)-[:TAGGED_WITH]->(데이터베이스)
** 대표 **: Neo4j, Amazon Neptune, ArangoDB
** 특징 **:
- 관계 탐색이 인덱스 프리 인접성(index-free adjacency)으로 O(1)입니다. RDBMS에서 JOIN 체인이 길어질수록 느려지는 것과 대조적이에요
- Cypher 같은 그래프 전용 쿼리 언어를 사용합니다
- 노드/엣지가 많아져도 탐색 깊이에 따라 성능이 결정되지, 전체 데이터 크기에 비례하지 않아요
** 용도 **: 소셜 네트워크, 추천 엔진, 지식 그래프, 사기 탐지
CAP 정리 (CAP Theorem)
분산 시스템에서 반드시 알아야 하는 이론입니다. 2000년에 Eric Brewer가 제안했고, 이후 증명됐어요.
분산 시스템은 다음 세 가지 속성 중 ** 최대 두 가지만** 동시에 만족할 수 있습니다.
| 속성 | 설명 |
|---|---|
| Consistency (일관성) | 모든 노드가 같은 시점에 같은 데이터를 본다. 쓰기가 완료되면 어느 노드에 읽기 요청을 해도 최신 값이 반환된다 |
| Availability (가용성) | 장애가 나지 않은 노드는 항상 응답한다. 에러 없이 읽기/쓰기가 가능하다 |
| Partition Tolerance (분할 내성) | 네트워크가 일부 끊겨서 노드 간 통신이 불가능해져도 시스템이 계속 동작한다 |
현실에서 네트워크 파티션은 ** 반드시 발생합니다 **. 클라우드 환경이든, 데이터센터 간 통신이든, 네트워크는 언제든 끊길 수 있어요. 그래서 P를 포기한다는 건 사실상 "분산 시스템을 안 쓰겠다"는 말과 같습니다. 결국 실질적인 선택은 CP vs AP 가 돼요.
CP 시스템 — 일관성 + 분할 내성
파티션이 발생하면 일관성을 지키기 위해 일부 요청을 거부 합니다. 최신 데이터를 보장하지 못할 바에야 차라리 에러를 반환하는 쪽을 택하는 거예요.
- **예시 **: MongoDB(기본 설정), HBase, Zookeeper, etcd
- ** 적합한 경우 **: 금융 거래, 재고 관리 — 잘못된 데이터를 보여주느니 잠깐 멈추는 게 낫습니다
AP 시스템 — 가용성 + 분할 내성
파티션이 발생해도 ** 항상 응답 **합니다. 대신 노드마다 데이터가 잠시 다를 수 있어요(Eventually Consistent).
- ** 예시 **: Cassandra, DynamoDB, CouchDB
- ** 적합한 경우 **: SNS 타임라인, 좋아요 수, 상품 리뷰 — 잠깐 옛날 데이터가 보여도 서비스가 죽는 것보다 낫습니다
CA 시스템 — 일관성 + 가용성
네트워크 파티션이 없다는 전제 하에서만 성립합니다. 단일 노드 RDBMS가 여기에 해당해요. 분산이 아니니까 파티션 자체가 없고, 일관성과 가용성을 모두 보장할 수 있습니다. 하지만 분산 환경에서는 사실상 존재하지 않는 조합이에요.
- ** 예시 **: 단일 노드 PostgreSQL, 단일 노드 MySQL
BASE vs ACID
RDBMS가 ACID를 보장한다면, NoSQL은 BASE를 지향하는 경우가 많습니다.
| ACID | BASE | |
|---|---|---|
| 풀네임 | Atomicity, Consistency, Isolation, Durability | Basically Available, Soft state, Eventually consistent |
| 핵심 | 트랜잭션의 정확성 보장 | 가용성 우선, 최종적 일관성 |
| 철학 | "틀린 데이터를 보여주느니 실패한다" | "일단 응답하고, 데이터는 곧 맞춰진다" |
| 대표 | MySQL, PostgreSQL, Oracle | Cassandra, DynamoDB, CouchDB |
BASE는 "약한 일관성"이 아니라 "최종적 일관성(Eventual Consistency)" 입니다. 시간이 지나면 모든 노드가 결국 같은 데이터를 갖게 된다는 보장이 있어요. 다만 그 "시간이 지나면"이 언제인지는 시스템마다 다릅니다 — 밀리초 단위일 수도 있고, 수 초가 걸릴 수도 있어요.
실무에서의 DB 선택 기준
"이 상황에서 어떤 DB를 쓸 건가요?" 이 질문에는 아래 세 가지 축으로 판단하면 됩니다.
1. 데이터 구조
- 관계가 명확하고 정규화가 가능 → RDBMS
- 필드가 유동적이고 중첩 구조 → Document (MongoDB)
- 단순 키-값 접근 → Key-Value (Redis)
- 관계 탐색이 핵심 → Graph (Neo4j)
- 대용량 시계열/로그 → Wide-Column (Cassandra)
2. 트래픽 패턴
- 읽기 위주 + 복잡한 JOIN → RDBMS가 유리
- 쓰기가 압도적으로 많다 → Cassandra가 강합니다
- 읽기가 극도로 빠르게 돌아야 한다면 → Redis 캐시 레이어
- 읽기/쓰기 비율이 비슷하고 수평 확장이 필요 → MongoDB, DynamoDB
3. 일관성 요구사항
- 강한 일관성 필수 (결제, 재고) → RDBMS 또는 CP 시스템
- 최종적 일관성으로 충분 (피드, 좋아요) → AP 시스템
- 세션, 캐시처럼 유실돼도 복구 가능한 데이터 → Redis
실무에서는 하나의 DB만 쓰는 경우가 거의 없습니다. Polyglot Persistence — MySQL로 핵심 비즈니스 데이터를 관리하고, Redis로 캐싱하고, Elasticsearch로 검색하고, Kafka로 이벤트를 흘리는 식이에요.
Redis 캐시 전략
Redis는 캐시로 가장 많이 쓰입니다. 캐시 전략에 따라 데이터 정합성과 성능이 크게 달라지니까, 자주 헷갈리는 부분이에요.
Cache Aside (Lazy Loading)
가장 보편적인 전략입니다.
- 애플리케이션이 Redis에 먼저 조회
- 캐시 히트 → 바로 반환
- 캐시 미스 → DB에서 읽어온 뒤, Redis에 저장하고 반환
GET user:1001 → Cache Miss
→ SELECT * FROM users WHERE id = 1001
→ SET user:1001 {name: "홍길동", ...} EX 3600
→ 반환
**장점 **: 실제로 요청된 데이터만 캐싱하니까 메모리 효율이 좋습니다 ** 단점 **: 첫 요청은 항상 느리고(Cold Start), DB가 업데이트돼도 캐시에 반영되기까지 TTL만큼 지연이 생길 수 있어요
Write Through
쓰기 시 DB와 캐시를 ** 동시에** 업데이트합니다.
UPDATE users SET name = '김철수' WHERE id = 1001
→ SET user:1001 {name: "김철수", ...}
** 장점 **: 캐시와 DB가 항상 동기화되어 있습니다 ** 단점 **: 쓰기 레이턴시가 늘어납니다. 읽히지도 않을 데이터까지 캐시에 올리게 되어 메모리 낭비가 생길 수 있어요
Write Behind (Write Back)
쓰기를 캐시에만 먼저 반영하고, ** 비동기로 DB에 나중에 반영 **합니다.
SET user:1001 {name: "김철수", ...}
→ (비동기) UPDATE users SET name = '김철수' WHERE id = 1001
** 장점 **: 쓰기 성능이 매우 빠릅니다. 동일 키에 대한 연속 쓰기를 배치로 묶을 수도 있어요 ** 단점 **: 캐시가 날아가면 DB에 반영되지 않은 데이터가 유실됩니다. 구현 복잡도도 높아요
주의할 점
"MongoDB가 ACID를 지원하나요?"
4.0부터 ** 멀티 도큐먼트 트랜잭션 **을 지원합니다. 단일 문서에 대해서는 그 전부터 원자적 쓰기를 보장했고, 4.0에서 여러 문서에 걸친 트랜잭션이 가능해졌어요. 다만 RDBMS만큼 트랜잭션을 자주 쓰도록 설계된 건 아니라서, 트랜잭션을 남용하면 성능이 떨어집니다. MongoDB의 철학은 관련 데이터를 하나의 문서에 임베딩해서 트랜잭션이 필요 없게 만드는 거예요.
"Redis를 세션 저장소로 쓰는 이유는?"
세 가지 이유가 있습니다.
- ** 빠릅니다 **: 인메모리라서 세션 조회가 서브 밀리초 단위예요
- **TTL 지원 **:
EXPIRE명령으로 세션 만료를 DB 레벨에서 자동 처리합니다. 별도 배치 없이 만료 세션이 알아서 삭제돼요 - ** 수평 확장 시 세션 공유 **: 서버가 여러 대인 환경에서 서버 로컬 세션을 쓰면 Sticky Session이나 세션 복제가 필요합니다. Redis에 세션을 두면 어느 서버로 요청이 가든 같은 세션에 접근할 수 있어요
"샤딩과 레플리카의 차이는?"
| 샤딩 (Sharding) | 레플리카 (Replication) | |
|---|---|---|
| 목적 | ** 쓰기 부하 분산 **, 데이터 분산 저장 | ** 읽기 부하 분산 **, 고가용성 |
| 방식 | 데이터를 기준(샤드 키)에 따라 여러 노드에 나눠 저장 | 같은 데이터를 여러 노드에 복제 |
| 각 노드의 데이터 | 전체 데이터의 일부분만 보유 | 전체 데이터를 동일하게 보유 |
| 장애 시 | 해당 샤드의 데이터에 접근 불가 (별도 레플리카 없으면) | 다른 레플리카가 대체 |
실무에서는 둘을 조합합니다. MongoDB의 경우 샤드마다 레플리카 셋을 구성하는 식이에요. 샤딩으로 쓰기를 분산하면서, 각 샤드 내에서 레플리카로 읽기 분산과 장애 복구를 동시에 가져가는 거예요.
파생 개념
이 주제에서 뻗어나갈 수 있는 개념들을 가볍게 짚고 넘어갑니다.
분산 시스템
단일 서버의 한계를 넘기 위해 여러 노드가 네트워크로 연결되어 하나의 시스템처럼 동작하는 구조입니다. NoSQL 대부분이 분산 시스템을 전제로 설계되었고, CAP 정리가 여기서 의미를 갖습니다. 분산 시스템의 핵심 과제는 합의(Consensus), 장애 감지, 데이터 복제 — 이 세 가지예요.
샤딩과 파티셔닝
넓은 의미에서 파티셔닝은 데이터를 나누는 모든 행위를 말하고, 샤딩은 ** 여러 물리적 노드에** 걸쳐서 나누는 것을 가리킵니다. 수평 파티셔닝이 샤딩과 거의 같은 개념이고, 수직 파티셔닝은 컬럼 단위로 테이블을 쪼개는 거예요.
메시지 큐
DB에 직접 쓰기 대신 Kafka, RabbitMQ 같은 메시지 큐를 사이에 두고, 비동기로 처리하는 패턴입니다. Write Behind 캐시 전략과도 연결되고, 서비스 간 결합도를 낮추는 데 핵심적인 역할을 해요.
MSA (Microservice Architecture)
서비스가 쪼개지면 각 서비스가 자기에게 맞는 DB를 독립적으로 선택하게 됩니다. 이게 바로 Polyglot Persistence가 자연스럽게 등장하는 맥락이에요. 주문 서비스는 MySQL, 검색 서비스는 Elasticsearch, 알림 서비스는 Redis — 이런 식으로요.
정리
| 항목 | RDBMS | Key-Value | Document | Wide-Column | Graph |
|---|---|---|---|---|---|
| 스키마 | 고정 | 없음 | 유연 | 유연 | 유연 |
| 확장 | 수직 | 수평 | 수평 | 수평 | 제한적 |
| 일관성 | 강함 (ACID) | 설정에 따라 | 설정에 따라 | 최종적 | 강함 |
| 대표 | MySQL, PostgreSQL | Redis | MongoDB | Cassandra | Neo4j |
| 적합한 용도 | 정형 데이터, 트랜잭션 | 캐시, 세션 | 유동적 스키마 | 대용량 쓰기, 로그 | 관계 탐색 |
NoSQL을 정리할 때는 "왜 RDBMS 대신 쓰는지", "CAP에서 어떤 선택을 하는지", "실무에서 어떻게 조합하는지"를 연결해서 이해하는 게 중요합니다. 단순히 종류를 나열하는 것보다, 트레이드오프를 이해하는 것이 깊이 있는 이해로 이어져요.