RDB(Relational Database)와 NoSQL은 데이터를 저장하는 방식과 철학이 근본적으로 다르다.
RDB는 정해진 규칙 안에서 데이터의 정확성을 보장하고, NoSQL은 그 규칙을 유연하게 풀어 확장성을 얻는다.
각각 어떤 특징을 가지고 있는지, 어떤 상황에서 어떤 선택이 맞는지 살펴보자.
RDB의 핵심은 ACID다. Atomicity, Consistency, Isolation, Durability — 네 가지 보장을 묶은 이름이다.
Atomicity(원자성) 은 트랜잭션을 하나의 작업 단위로 묶어, 전부 성공하거나 전부 실패하도록 보장한다. commit 아니면 rollback만 있다. 중간 상태가 존재하지 않는다.
Consistency(일관성) 는 트랜잭션 전후로 DB가 항상 유효한 상태를 유지한다는 것이다. 스키마 제약(PK, FK)뿐 아니라 비즈니스 규칙까지 포함한다. 계좌 이체 후 전체 잔액 합산이 변하면 안 된다는 규칙도 여기에 해당한다.
Isolation(격리성) 은 동시에 실행되는 트랜잭션들이 서로 영향을 주지 않도록 한다. A가 이체하는 도중 다른 트랜잭션이 A의 잔액을 읽어도 중간 상태가 보이지 않는다.
Durability(지속성) 는 커밋된 데이터는 서버가 꺼져도 사라지지 않는다는 것이다.
이 보장들이 왜 필요한지는 계좌 이체를 보면 명확해진다. A가 B에게 10만원을 보낸다.
UPDATE accounts SET balance = balance - 100000 WHERE id = 'A'; -- A 출금
UPDATE accounts SET balance = balance + 100000 WHERE id = 'B'; -- B 입금
첫 번째 쿼리가 성공한 뒤 서버가 죽으면 A 계좌에서는 10만원이 빠졌는데 B에게는 들어오지 않는다. Atomicity가 없으면 이런 중간 상태가 그대로 저장된다. 이처럼 데이터 정합성이 절대 어긋나면 안 되는 금융, 주문, 결제 같은 정보를 저장할 때 사용한다.
NoSQL은 scale-out 방식으로 데이터를 저장하고 처리할 수 있도록 설계된 DB다. RDB는 서버 한 대를 더 좋은 것으로 교체하는 수직 확장(scale-up)에 의존하는데, 세상에서 가장 좋은 서버에도 한계가 있다. 반면 scale-out은 서버를 여러 대로 늘려 데이터를 나눠 저장하고 요청을 분산 처리하는 방식이다. 이론적으로 서버를 계속 추가하는 한 용량과 처리량을 무한히 늘릴 수 있다.
RDB도 샤딩으로 여러 서버에 데이터를 나눌 수 있지만, 샤드를 넘나드는 트랜잭션이 생기면 두 서버가 동시에 합의해야 하는 과정(2PC)이 필요하다. 서버가 늘수록 이 조율 비용이 기하급수적으로 커져, ACID를 분산 환경에서 유지하는 것은 현실적으로 감당하기 어렵다. 그래서 NoSQL은 ACID 대신 BASE 철학을 택했다.
Basically Available 은 항상 응답을 준다는 것이다. 일부 노드에 장애가 생겨도 전체 서비스가 멈추지 않는다.
Soft state 는 일시적으로 노드마다 데이터가 다를 수 있다는 것이다. 모든 노드가 항상 동일한 상태일 것을 요구하지 않는다.
Eventually Consistent 는 결국엔 모든 노드가 같은 데이터를 갖게 된다는 것이다. 즉각적인 일관성이 아닌, 시간이 지나면 일관성이 보장된다.
이처럼 ACID를 포기하되, 분산 구조를 기반으로 대용량 데이터를 수평으로 확장하며 처리해야 하는 서비스에서 사용한다.
이 부분에 대해 조금 더 구체적으로 살펴보자면,
[한국 서버] 게시물 업로드 → 저장 완료 ✅
한국 서버 → 미국 서버 복제 중... (0.3초)
한국 서버 → 유럽 서버 복제 중... (0.8초)
그 0.3초 사이에 미국 유저가 조회 → 게시물이 없다
0.8초 뒤 → 모든 서버 동일 ✅
데이터가 사라진 것이 아니다. 전 세계 서버에 퍼지는 속도 차이로 잠깐 다른 화면이 보이는 것이다. 좋아요 수가 잠깐 1개 덜 보이는 것은 서비스에 치명적이지 않다. 그래서 이 방식을 허용하는 것이 합리적인 선택이다.
NoSQL이라는 이름으로 묶여 있지만 내부 구조와 특화 방향은 제각각이다. 크게 네 가지로 나뉜다.
Key-Value (Redis, DynamoDB)는 key로 value를 꺼내는 구조로, 해시맵 방식으로 O(1)에 조회한다. 메모리 기반까지 더하면 DB 중 가장 빠른 조회 속도가 나오기 때문에, 세션 저장이나 캐싱처럼 단순하고 빠르게 꺼내야 하는 데이터에 적합하다.
Document (MongoDB)는 JSON 형태로 데이터를 중첩 저장하는 구조로,
{
"_id": "post_123",
"title": "오늘 클라이밍 후기",
"author": { "name": "지수", "email": "jisu@example.com" },
"comments": [
{ "user": "user1", "text": "좋다!", "likes": 5 }
]
}
관련 데이터를 하나의 문서에 담아 한 번에 가져올 수 있다. 스키마가 고정되지 않아 문서마다 필드가 달라도 되기 때문에, 상품 카탈로그처럼 항목마다 데이터 구조가 유동적인 경우에 적합하다.
Column (Cassandra, HBase)는 컬럼 단위로 데이터를 저장하는 구조로, 쓰기 속도가 매우 빠르고 특정 기간 범위 조회에 강하다. 로그, 활동 이력, IoT 센서 데이터처럼 시간 순서로 계속 쌓이는 대용량 데이터에 적합하다.
Graph (Neo4j)는 노드와 엣지로 관계를 표현하는 구조로, 관계를 따라가는 탐색이 기본 연산이다. RDB로 3촌 탐색을 짜면 JOIN이 여러 번 중첩되지만, Graph DB는 엣지 구조 자체가 관계 탐색에 최적화되어 있기 때문에 소셜 네트워크처럼 복잡한 관계를 다루는 서비스에 적합하다.
분산 시스템이 확산되면서 한 가지 의문이 생겼다. 서버를 여러 대로 나눠 운영할 때, 어떤 보장을 얼마나 할 수 있는가. 2000년 Eric Brewer가 이 질문에 이론적인 틀을 제시한 것이 CAP theorem이다. 여기서는 세 가지를 보장하는데, 이 세 가지를 동시에 만족할 수 없다.
Consistency 는 모든 노드가 항상 같은 데이터를 본다는 것이다. 어느 서버에 요청을 보내도 동일한 결과를 반환한다.
Availability 는 항상 응답을 준다는 것이다. 일부 노드에 장애가 생겨도 요청에 대한 응답이 반드시 온다.
Partition Tolerance 는 서버 간 통신이 끊겨도 시스템이 계속 동작한다는 것이다. 네트워크 장애가 발생해도 서비스가 멈추지 않는다.
세 가지 중 두 가지만 선택한다면 이론적으로 CP, AP, CA 세 조합이 가능하다. 그런데 실제 분산 환경에서 P(Partition Tolerance)는 포기할 수 있는 선택지가 아니다. 서버가 여러 대인 이상 네트워크 장애는 언제든 생길 수 있고, P를 포기한다는 건 장애 상황에서 시스템이 아예 멈춰도 된다는 뜻이기 때문이다. 그래서 실제 선택지는 CP와 AP 두 가지로 좁혀진다.
CP — Partition 상황에서 응답을 거부하고 일관성을 지킨다. 데이터가 틀리는 것보다 응답 못 주는 게 낫다는 선택이다. RDB, MongoDB가 여기에 해당한다.
AP — Partition 상황에서도 일단 응답을 준다. 대신 노드마다 데이터가 잠깐 다를 수 있다. Cassandra, DynamoDB가 여기에 해당한다.
AP는 맞지 않는 상황에서 쓰면 치명적인 문제가 생긴다. 계좌 이체가 대표적이다. A 계좌에 10만원이 있다. B에게 9만원을 보냈는데 아직 다른 노드에 반영되기 전이다. 이 상태에서 C에게 9만원을 보내면, 잔액이 여전히 10만원으로 보이기 때문에 통과된다. 결국 10만원으로 18만원이 나가는 이중 지불(double spend) 문제가 생긴다. 일관성을 일시적으로 포기한 대가가 데이터 손실로 이어지는 것이다.
인스타그램을 예로 들면, 유저 계정 정보는 RDB에 둔다. 한 명의 계정 정보가 여러 곳에서 참조되고, 데이터 정합성이 중요하다. 게시물 좋아요 수와 피드 캐싱은 Redis가 담당한다. 빠른 응답이 중요하고, 잠깐 1개 덜 보이는 것이 치명적이지 않다. 팔로워 기반 "아는 사람 추천"이 필요하다면 Graph DB가 어울린다. RDB로 3촌 탐색을 짜면 JOIN이 3번 중첩되고, 팔로워 수억 명에서는 감당이 되지 않는다.
하나의 서비스에도 데이터 성격이 다르면 DB도 달라진다. 중요한 건 다루는 데이터가 어떤 성격인지 파악하고 그에 맞는 저장소를 찾는 것이다.