icon

안동민 개발노트

14장 : NoSQL과 분산 데이터베이스

분산 데이터베이스 기초


데이터를 여러 서버에 분산하는 두 가지 핵심 기법이 레플리케이션(Replication)샤딩(Sharding)입니다. 단일 서버의 한계(처리량, 가용성, 저장 용량)를 극복하기 위해 분산 아키텍처를 도입하며, 이때 발생하는 일관성·가용성·파티션 내성 사이의 트레이드오프를 이해해야 합니다.


분산 데이터베이스가 필요한 이유

단일 서버의 한계
┌──────────────────────────────────────────────────────┐
│                  단일 서버 아키텍처                  │
│                                                      │
│  ┌─────────────┐                                     │
│  │ Application │ ────→  [ DB Server ]                │
│  └─────────────┘        (1대)                        │
│                                                      │
│  한계:                                               │
│  * 처리량: CPU/메모리 한계 → 스케일업 비용 급증      │
│  * 가용성: 서버 1대 장애 = 서비스 중단               │
│  * 저장량: 디스크 용량 한계                          │
│  * 지연: 먼 지역 사용자 → 네트워크 지연              │
│                                                      │
│  해결: 분산 아키텍처 (레플리케이션 + 샤딩)           │
└──────────────────────────────────────────────────────┘
분산 목적기법설명
읽기 확장레플리케이션읽기 요청을 여러 복제본에 분산
쓰기 확장샤딩데이터를 분할하여 쓰기 분산
고가용성레플리케이션한 서버 장애 시 다른 서버가 대체
지역 분산멀티 리전 복제사용자와 가까운 서버에서 응답

레플리케이션

레플리케이션은 같은 데이터를 여러 서버에 복사하여 가용성과 읽기 성능을 높이는 기법입니다.


마스터-슬레이브 (Primary-Replica)

가장 일반적인 레플리케이션 구조입니다. 쓰기는 마스터만, 읽기는 슬레이브에서 처리합니다.

마스터-슬레이브 레플리케이션
마스터-슬레이브 구조
┌────────────┐
│   Master   │  ← 쓰기 (INSERT, UPDATE, DELETE)
│  (Primary) │
└──────┬─────┘
       │ 복제 (Binlog / WAL / Redo)
       ├──────────────→ Slave1 (Read)
       └──────────────→ Slave2 (Read)

애플리케이션 관점
App ──W──→ Master
App ──R──→ Slave1
App ──R──→ Slave2

   Load Balancer로 읽기 분산
장점단점
읽기 부하 분산쓰기는 Master만 가능
Master 장애 시 Slave가 승격복제 지연(Replication Lag)
구현이 비교적 단순Master가 단일 장애점(SPOF)
백업을 Slave에서 수행 가능쓰기 확장 불가

복제 방식: 동기 vs 비동기

복제의 동기화 수준에 따라 데이터 일관성과 성능 사이의 트레이드오프가 달라집니다.

동기 vs 비동기 vs 반동기 복제
──── 동기 복제 (Synchronous) ────
Master: INSERT → Slave에 전송 → Slave 응답 대기 → COMMIT
장점: 데이터 손실 0 (RPO=0)
단점: 쓰기 지연 증가, Slave 장애 시 Master도 멈춤

──── 비동기 복제 (Asynchronous) ────
Master: INSERT → COMMIT → 나중에 Slave 전송
장점: 쓰기 성능 최상
단점: Master 장애 시 미복제 데이터 유실 가능

──── 반동기 복제 (Semi-Synchronous) ────
Master: INSERT → 최소 1개 Slave 수신 확인 → COMMIT
장점: 적어도 1개 Slave에는 데이터 존재
단점: 동기보다 빠르지만 비동기보다 느림

──── 각 DBMS 설정 ────
MySQL:  INSTALL PLUGIN rpl_semi_sync_master;
        SET GLOBAL rpl_semi_sync_master_enabled = 1;

PostgreSQL: synchronous_standby_names = 'standby1'
            synchronous_commit = on  -- 동기
            synchronous_commit = remote_apply -- 강한 동기

Oracle Data Guard:
  Maximum Performance   → 비동기
  Maximum Availability  → 반동기
  Maximum Protection    → 동기 (데이터 손실 0)

복제 지연(Replication Lag) 문제

비동기 복제에서 마스터와 슬레이브의 데이터가 일시적으로 다른 현상입니다.

복제 지연 시나리오
시나리오: 사용자가 프로필 사진을 변경한 직후 새로고침

1. APP → Master: UPDATE users SET avatar='new.jpg' (쓰기)
2. Master: 커밋 완료 ✓
3. APP → Slave: SELECT avatar FROM users (읽기)
4. Slave: 아직 복제 안 됨 → avatar='old.jpg' 반환!
   → 사용자: "변경이 안 됐나?" 혼란

해결 방법
┌─────────────────────────────────────────────────────┐
│ 1. Read-Your-Writes 보장                            │
│    → 방금 쓴 사용자의 읽기는 Master에서 조회        │
│    → 다른 사용자의 읽기는 Slave에서 조회            │
│                                                     │
│ 2. 세션 기반 라우팅                                 │
│    → 쓰기 후 일정 시간(예: 3초) Master에서 읽기     │
│    → 이후 Slave로 전환                              │
│                                                     │
│ 3. 반동기/동기 복제 사용                            │
│    → 최소 1개 Slave 복제 확인 후 응답               │
│                                                     │
│ 4. GTID 기반 읽기 대기                              │
│    → 쓰기 시 GTID 기록                              │
│    → 읽기 시 해당 GTID가 Slave에 반영될 때까지 대기 │
└─────────────────────────────────────────────────────┘

Failover (장애 조치)

마스터 장애 시 슬레이브를 새로운 마스터로 승격하는 과정입니다.

Failover 흐름
정상 상태:
  [Master] ─복제→ [Slave 1]
           ─복제→ [Slave 2]

Master 장애 감지:
  1. 주기적 헬스체크 (Heartbeat)
  2. 연속 N회 실패 → 장애 판정

Failover 수행:
  1. 가장 최신 데이터를 가진 Slave 선택
  2. 해당 Slave를 새 Master로 승격 (Promote)
  3. 나머지 Slave를 새 Master에 연결
  4. 애플리케이션의 쓰기 대상을 새 Master로 전환
  5. VIP(Virtual IP) 또는 DNS 전환

  [Slave 1 → New Master] ← 쓰기 전환
     ─복제→ [Slave 2]

Failover 도구:
  MySQL:      MHA, Orchestrator, ProxySQL
  PostgreSQL: Patroni, repmgr, pg_auto_failover
  Oracle:     Data Guard Broker (DGMGRL)

마스터-마스터 (Multi-Master)

마스터-마스터 레플리케이션
Master A ↔ Master B
┌────────┐
│Master A│
│ 쓰기   │
└────────┘
    ↕ 양방향 복제
┌────────┐
│Master B│
│ 쓰기   │
└────────┘

결과: 둘 다 쓰기 가능

양쪽에서 동시에 같은 데이터를 수정하면 충돌이 발생합니다. 충돌 해결 정책이 필요합니다.

Multi-Master 충돌 해결 전략
충돌 시나리오
  Master A: UPDATE users SET name='Kim' WHERE id=1;
  Master B: UPDATE users SET name='Lee' WHERE id=1;
  → 동시에 실행 → 어느 값이 최종?

해결 전략
  1. 타임스탬프 우선 (Last Write Wins)
     → 더 늦은 타임스탬프 값이 우선
     → 단순하지만 데이터 유실 가능

  2. 노드 우선순위
     → Master A > Master B로 우선순위 설정
     → 충돌 시 높은 우선순위 노드의 값 채택

  3. 애플리케이션 레벨 해결
     → 충돌을 기록하고 사용자에게 선택 제시
     → Git의 merge conflict와 유사

  4. 충돌 회피 설계
     → 각 Master가 다른 데이터를 담당
     → 지역 A 사용자 → Master A, 지역 B → Master B
비교Master-SlaveMulti-Master
쓰기 확장불가가능
복잡도낮음높음
충돌없음발생 가능
가용성Failover 필요즉시 전환 가능
주요 사용대부분의 시스템멀티 리전, 특수 요구

샤딩

샤딩(Sharding)은 데이터를 여러 서버에 나누어 저장하는 기법입니다. 레플리케이션이 복사라면, 샤딩은 분할입니다. 단일 서버로 감당할 수 없는 쓰기 부하나 저장량을 해결합니다.

샤딩 개념
전체 데이터: [A B C D E F G H I J K L]

샤드 1: [A B C D]     → 서버 1 (4건)
샤드 2: [E F G H]     → 서버 2 (4건)
샤드 3: [I J K L]     → 서버 3 (4건)

각 서버가 1/3의 부하만 처리
→ 수평 확장(Scale-Out) 가능

샤드 키 선택

샤딩의 성패는 샤드 키(Shard Key) 선택에 달려 있습니다.

좋은 샤드 키의 조건
1. 높은 카디널리티 (값의 종류가 많음)
   좋음: user_id (수백만 개)
   나쁨: gender (2개) → 2개 샤드로만 분할

2. 균등한 분포 (데이터 편중 없음)
   좋음: UUID, user_id
   나쁨: 국가 코드 → 특정 국가에 편중

3. 자주 사용되는 쿼리에 포함
   좋음: 대부분 쿼리가 WHERE user_id = ? 형태
   나쁨: 쿼리와 무관한 키 → Cross-Shard 쿼리 증가

4. 변경되지 않는 값
   좋음: user_id (불변)
   나쁨: email (변경 가능) → 샤드 이동 필요

해시 기반 샤딩

해시 기반 샤딩
샤드 키: user_id
샤드 수: 3

hash(user_id) % 3 = 0 → 샤드 0
hash(user_id) % 3 = 1 → 샤드 1
hash(user_id) % 3 = 2 → 샤드 2

장점
  * 데이터 균등 분배
  * 핫스팟 방지
  * 특정 키의 샤드를 즉시 계산 가능

단점
  * 범위 쿼리 비효율 (전체 샤드 스캔)
  * 샤드 추가/제거 시 대량 데이터 재배치
  * 재배치 비용: 샤드 3개→4개 시 75% 데이터 이동

범위 기반 샤딩

범위 기반 샤딩
샤드 키: 날짜

2023년 데이터 → 샤드 1
2024년 데이터 → 샤드 2
2025년 데이터 → 샤드 3

장점:
  * 범위 쿼리 효율적 (해당 샤드만 스캔)
  * 구현이 직관적
  * 오래된 데이터 아카이빙 용이

단점:
  * 핫스팟 발생 (최신 데이터에 쓰기 집중)
  * 데이터 불균형 가능
디렉토리 기반 샤딩
샤드 조회 테이블 (Lookup Table)
┌───────────────┬───────────┐
│ 샤드 키 범위  │ 샤드 위치 │
├───────────────┼───────────┤
│user 1~1000    │  샤드 A   │
│user 1001~3000 │  샤드 B   │
│user 3001~10000│  샤드 C   │
└───────────────┴───────────┘

장점
  * 유연한 샤드 배정 (불균형 보정 가능)
  * 특정 키를 다른 샤드로 이동 가능

단점
  * Lookup Table이 SPOF
  * 조회 오버헤드 (매 요청마다 참조)

일관성 해싱

해시 기반 샤딩에서 샤드를 추가하거나 제거할 때 일관성 해싱(Consistent Hashing)은 데이터 재배치를 최소화합니다.

일반 해시 vs 일관성 해싱 비교
──── 일반 해시 (Modular) ────
3개 노드: hash(key) % 3
4개 노드로 변경: hash(key) % 4
→ 거의 모든 키의 위치가 변경! (재배치 75%)

──── 일관성 해싱 ────
해시 링(Hash Ring) 위에 노드 배치


       N1
      / |
    /   |
  N4    | 90°
  |     N2
  |   /
   \ /
    N3
   180°

규칙: 키는 시계 방향으로 가장 가까운 노드에 저장

노드 추가 시
       N1
     / | \
   N5  |  \        ← N1과 N2 사이에 N5 추가
   |   |   N2
   |   | /
    \  |/
     N3

→ N5 추가 시 N1~N5 구간의 키만 N5로 이동
→ 나머지 키는 이동 없음!
→ 재배치 비율: K/N (전체 키 수 / 노드 수)
가상 노드 (Virtual Node)
물리 노드가 적으면 데이터 편중이 심할 수 있음
→ 가상 노드로 해결

물리 노드 3개, 가상 노드 각 4개
  N1 → N1-1, N1-2, N1-3, N1-4  (링 위 4곳)
  N2 → N2-1, N2-2, N2-3, N2-4  (링 위 4곳)
  N3 → N3-1, N3-2, N3-3, N3-4  (링 위 4곳)

→ 12개 지점이 링 위에 균등 배치
→ 데이터가 더 고르게 분산

사용 제품
  * Amazon DynamoDB
  * Apache Cassandra
  * Redis Cluster (해시 슬롯 16384개)

샤딩의 과제

샤딩은 강력하지만 여러 운영 과제를 수반합니다.

샤딩의 핵심 과제
1. Cross-Shard 쿼리
   → 여러 샤드를 걸치는 쿼리 → 성능 저하
   예: SELECT * FROM orders WHERE date BETWEEN ...
       AND user_id IN (...)
   → 모든 샤드에 쿼리 후 애플리케이션에서 병합

2. Cross-Shard JOIN
   → 서로 다른 샤드의 테이블 JOIN 불가
   → 해결: 애플리케이션 레벨 JOIN, 또는 데이터 비정규화

3. Cross-Shard 트랜잭션
   → 여러 샤드에 걸친 ACID 트랜잭션 어려움
   → 해결: 2PC (느림), Saga 패턴 (Eventually Consistent)

4. 리밸런싱
   → 데이터 편중 시 샤드 간 데이터 이동 필요
   → 서비스 운영 중 리밸런싱 복잡

5. 샤드 키 변경 불가
   → 초기 설계가 중요
   → 잘못된 샤드 키 → 전체 재설계 필요

6. Auto-Increment ID
   → 샤드별 ID 중복 방지 필요
   → UUID, Snowflake ID, 또는 ID 생성 서비스 사용
분산 ID 생성 전략
┌──────────────────┬──────────────────────┬──────────────────┐
│       전략       │         예시         │       특징       │
├──────────────────┼──────────────────────┼──────────────────┤
│ UUID v4          │ 550e8400-e29b-41d4   │ 충돌 거의 없음   │
│                  │ -a716-446655440000   │ 정렬 불가, 인덱스│
│                  │                      │ 효율 낮음        │
│ Snowflake ID     │ 1771836590123456789  │ 시간순 정렬 가능 │
│ (Twitter)        │ [시간|워커|시퀀스]   │ 64비트 정수      │
│       ULID       │ 01ARZ3NDEKTSV4R...   │ UUID + 시간 정렬 │
│ 샤드 ID + Seq    │ [샤드번호][시퀀스]   │ 단순, 샤드 식별  │
│ 중앙 발급        │ ID 서버에서 배치 발급│ SPOF 위험        │
└──────────────────┴──────────────────────┴──────────────────┘

레플리케이션 + 샤딩 조합

실무에서는 레플리케이션과 샤딩을 함께 사용합니다.

레플리케이션 + 샤딩 아키텍처
┌───────────────────────────────────────────────────┐
│               전체 아키텍처                       │
│                                                   │
│  ┌──────────────────────────────────────────────┐ │
│  │ 라우터 (ProxySQL / Vitess / ShardingSphere)  │ │
│  └──────────┬──────────┬──────────┬─────────────┘ │
│             │          │          │               │
│        샤드 1      샤드 2      샤드 3             │
│        ┌──────┐    ┌──────┐   ┌──────┐            │
│        │Master│    │Master│   │Master│            │
│        └───┬──┘    └───┬──┘   └───┬──┘            │
│        ┌───┴──┐    ┌───┴──┐   ┌───┴──┐            │
│        │Slave │    │Slave │   │Slave │            │
│        │Slave │    │Slave │   │Slave │            │
│        └──────┘    └──────┘   └──────┘            │
│                                                   │
│  각 샤드 = Master-Slave 레플리케이션 구성         │
│  * 샤딩으로 쓰기 확장                             │
│  * 레플리케이션으로 읽기 확장 + 가용성 확보       │
└───────────────────────────────────────────────────┘

합의 알고리즘

분산 시스템에서 여러 노드가 하나의 값에 합의하는 방법입니다. 노드 장애가 발생해도 시스템이 올바르게 동작하기 위해 필수적입니다.

알고리즘특징사용 제품
Raft이해하기 쉬움, 리더 기반etcd, CockroachDB, TiDB
Paxos이론적 근간, 구현 복잡Google Spanner, Chubby
ZABZookeeper 전용, Paxos 변형Apache Zookeeper
PBFT비잔틴 장애 허용블록체인 일부
Raft 합의 알고리즘 상세
──── 노드 상태 ────
1. Leader:    클라이언트 요청 처리, 로그 복제
2. Follower:  Leader의 로그 복제 수신
3. Candidate: Leader 선출 투표 참여

──── 정상 작동 ────
Client → Leader: 쓰기 요청
Leader: 로그에 기록
Leader → Followers: 로그 복제 전송
Followers: 로그 응답 (ACK)
Leader: 과반수 ACK → 커밋 → Client 응답

──── Leader 장애 시 ────
Follower: 헬스체크 타임아웃 (150~300ms)
  → Candidate 상태로 전환
  → 임기(Term) 증가
  → 자신에게 투표 + 다른 노드에게 투표 요청

투표 규칙:
  * 한 임기에 한 표만 가능
  * 로그가 더 최신인 Candidate에게만 투표
  * 과반수(N/2 + 1) 득표 → 새 Leader 당선

Split Brain 방지:
  * 과반수 합의 → 두 Leader 동시 존재 불가
  * 3노드 → 1대 장애 허용
  * 5노드 → 2대 장애 허용
  * 공식: (N-1)/2 대 장애 허용

CAP 정리와 분산 DB

분산 시스템에서는 CAP 3가지 속성을 동시에 모두 만족할 수 없습니다.

CAP 정리
          C (Consistency)
         / \
        /   \
       /     \
      /  CAP  \
     /  정리   \
    /           \
   A ─────────── P
(Availability)  (Partition Tolerance)

C: 일관성 → 모든 노드가 같은 데이터
A: 가용성 → 모든 요청에 응답
P: 파티션 내성 → 네트워크 분할에도 동작

네트워크 파티션(P)은 피할 수 없으므로,
실질적으로 C와 A 중 선택:

CP 시스템: 일관성 우선 (파티션 시 일부 요청 거부)
  → HBase, MongoDB (강한 일관성 설정)
  → 금융, 재고 관리

AP 시스템: 가용성 우선 (파티션 시 다른 데이터 반환 가능)
  → Cassandra, DynamoDB
  → SNS, 센서 데이터, 로그
PACELC 모델 (CAP 확장)
네트워크 파티션이 발생하면 (P):
  C(일관성) vs A(가용성) 선택
그렇지 않으면 (E = Else):
  L(지연시간) vs C(일관성) 선택

┌──────────────┬────────────┬──────────────┐
│    시스템    │ P 시 선택  │ E 시 선택    │
├──────────────┼────────────┼──────────────┤
│ MySQL(InnoDB)│ C (일관성) │ C (일관성)   │
│ Cassandra    │ A (가용성) │ L (낮은 지연)│
│ MongoDB      │ C (일관성) │ C (일관성)   │
│ DynamoDB     │ A (가용성) │ L (낮은 지연)│
│ CockroachDB  │ C (일관성) │ C (일관성)   │
│ Spanner      │ C (일관성) │ L (TrueTime) │
└──────────────┴────────────┴──────────────┘

분산 DB 제품 비교

분산 DB 유형별 대표 제품
┌─────────────────────────────────────────────────────────┐
│               분산 DB 제품 비교                         │
├──────────────┬──────────┬──────────┬────────────────────┤
│     제품     │   유형   │   CAP    │        특징        │
├──────────────┼──────────┼──────────┼────────────────────┤
│ CockroachDB  │ NewSQL   │    CP    │ Raft, 분산 트랜잭션│
│     TiDB     │ NewSQL   │    CP    │ MySQL 호환, Raft   │
│Google Spanner│  NewSQL  │ CP(+A)   │ TrueTime, 전역 분산│
│ Vitess       │ 미들웨어 │ (MySQL)  │ MySQL 샤딩 계층    │
│ Citus        │   확장   │   (PG)   │PostgreSQL 분산 확장│
│ Amazon Aurora│ 클라우드 │    CP    │ MySQL/PG 호환      │
│ PlanetScale  │ 클라우드 │    CP    │ Vitess 기반 MySQL  │
├──────────────┼──────────┼──────────┼────────────────────┤
│ Cassandra    │ Wide-Col │    AP    │ 링 구조, 높은 쓰기 │
│ DynamoDB     │ Key-Value│    AP    │ AWS 관리형         │
│ MongoDB      │ Document │    CP    │ 자동 샤딩, 유연한  │
│              │          │          │       스키마       │
└──────────────┴──────────┴──────────┴────────────────────┘

분산 데이터베이스 종합 정리

분산 DB 설계 판단 기준
┌─────────────────────────────────────────────────────────┐
│              분산 DB 도입 의사결정                      │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Q1. 단일 서버로 감당 가능한가?                         │
│      YES → 분산 불필요 (단순함이 최고)                  │
│      NO  → Q2로                                         │
│                                                         │
│  Q2. 읽기 확장만 필요한가?                              │
│      YES → 레플리케이션 (Master-Slave)                  │
│      NO  → Q3로                                         │
│                                                         │
│  Q3. 쓰기도 확장이 필요한가?                            │
│      YES → 샤딩 (+ 레플리케이션 병행)                   │
│      NO  → 레플리케이션으로 충분                        │
│                                                         │
│  Q4. 강한 일관성이 필요한가?                            │
│      YES → NewSQL (CockroachDB, Spanner, TiDB)          │
│      NO  → NoSQL (Cassandra, DynamoDB)                  │
│                                                         │
│  Q5. 멀티 리전 분산이 필요한가?                         │
│      YES → Spanner, CockroachDB, Cassandra              │
│      NO  → 단일 리전 샤딩                               │
│                                                         │
└─────────────────────────────────────────────────────────┘
핵심 개념설명
레플리케이션같은 데이터 복사 → 읽기 확장, 가용성
샤딩데이터 분할 → 쓰기 확장, 저장량 확장
일관성 해싱노드 변경 시 최소 재배치
합의 알고리즘노드 장애에도 올바른 결정
CAP 정리C·A·P 중 2개만 선택 가능
Failover자동 장애 감지 후 대체 노드 승격
복제 지연비동기 복제에서 일시적 불일치
샤드 키데이터 분배 기준 → 설계 핵심

다음 절에서는 RDB와 NoSQL의 선택 기준을 다루겠습니다.

목차