분산 데이터베이스와 CAP

src/content/documents/data-analysis/cloud-distributed-databases-cap.json

클라우드 DB를 분산 DB라고 부르는 것만으로 설계가 끝나지 않는다. 누가 운영하는지, 데이터를 어디에 나누는지, 몇 복제본이 승인해야 커밋인지, 장애 중 어떤 요청을 거부할지를 각각 정해야 한다. 핵심은 매치 기록과 결제 원장에 같은 일관성 정책을 강요하지 않는 것이다.

서로 다른 네 가지 결정을 분리한다

결정

해결하지 않는 것

관리형 클라우드 DB

공급자가 패치·백업·장애조치 일부를 운영

스키마·쿼리·권한·복구 검증 책임

읽기 복제본

주 복제본의 변경 로그를 적용해 읽기 분산

쓰기 수평 확장과 즉시 최신 읽기

파티셔닝

한 논리 DB 안에서 행을 파티션으로 분할

여러 독립 DB의 라우팅·분산 트랜잭션

샤딩

데이터를 여러 독립 노드·DB에 수평 분할

자동 일관성·리밸런싱·전역 JOIN

관리형은 무운영이 아니다. 자동 백업이 켜져 있어도 실제 시점 복구, 애플리케이션 재연결, DNS·자격 증명 전환, 데이터 정합성을 정기적으로 복구 훈련해야 한다.

복제와 합의는 같은 말이 아니다

복제는 같은 데이터를 여러 위치에 보관하는 메커니즘이다. 비동기 스트리밍 복제는 주 서버가 먼저 커밋한 뒤 WAL을 대기 서버에 보내므로 낮은 쓰기 지연과 복제 지연 가능성을 함께 가진다. 동기 복제는 지정된 대기 서버의 WAL 수신·기록·적용 단계까지 기다리도록 구성할 수 있어 데이터 손실 위험을 낮추는 대신 네트워크 지연을 커밋 경로에 넣는다.

합의 알고리즘은 장애가 있는 노드 집합이 로그 순서나 리더를 하나로 결정하도록 한다. Raft 같은 합의는 복제 로그와 과반수 승인을 사용하지만, 데이터베이스 전체의 트랜잭션 격리·보조 인덱스·다중 샤드 원자성을 자동으로 정의하지 않는다.

Q=N2+1Q=\left\lfloor\frac{N}{2}\right\rfloor+1

N개 투표 노드의 단순 과반수 정족수는 Q개다. N=5이면 Q=3이며 두 과반수 집합은 적어도 한 노드에서 교차한다.

R+W>NW>N2R+W>N \quad\land\quad W>\frac{N}{2}

복제본 N개 중 읽기 R개와 쓰기 W개를 확인하는 고전적 quorum 조건이다. 읽기·쓰기 집합 교차와 쓰기끼리 교차를 뜻하지만, 이것만으로 선형화 가능성이 자동 보장되지는 않는다. 버전 순서, 충돌 해결, 리더 임기와 실패 처리까지 프로토콜이 정의해야 한다.

CAP의 정확한 질문은 파티션 중 무엇을 포기할 것인가

CAP에서 C는 모든 클라이언트가 단일 최신 복사본처럼 관찰하는 원자적 일관성, A는 장애가 아닌 노드가 받은 모든 요청에 결국 응답하는 가용성, P는 노드 사이 메시지가 임의로 유실·지연되는 네트워크 파티션을 견디는 성질이다. 분산 시스템이 파티션을 무시할 수 없다면 파티션 동안 C 또는 A를 희생하는 선택이 생긴다.

파티션 중 선택

행동

게임 예

CP

정족수 없는 쪽의 읽기·쓰기를 거부·대기

결제 잔액과 아이템 소유권

AP

양쪽이 요청을 수용하고 나중에 병합

중복 허용 가능한 매치 텔레메트리

‘우리 DB는 CA’라는 표현은 네트워크 파티션 중 행동을 숨길 수 있다. 정상 상태에서는 일관성과 가용성을 모두 높일 수 있지만, CAP의 선택은 통신 단절이 실제로 발생한 구간에서 드러난다.

일관성 모델은 사용자 관찰 규칙이다

  • 선형화 가능성: 완료된 쓰기 뒤 시작한 읽기는 그 쓰기 또는 더 최신 값을 본다. 결제 승인·재고 차감에 적합하다.

  • 인과적 일관성: 원인보다 결과가 먼저 보이지 않는다. 매치 종료 후 보상 표시 같은 인과 관계를 지킨다.

  • Read-your-writes: 사용자가 방금 저장한 덱·프로필을 다음 읽기에서 본다. 쓰기 후 잠시 주 리더로 읽기를 고정할 수 있다.

  • 최종 일관성: 새 쓰기가 멈추면 복제본이 결국 같은 값으로 수렴한다. 수렴 시간과 충돌 규칙을 별도로 정해야 한다.

글로벌 매치 기록 — 지역에서 받고 멱등하게 병합

매치 텔레메트리는 지역 네트워크가 끊겨도 게임 진행을 계속 기록할 가치가 크다. 각 리전이 event_id가 있는 불변 이벤트를 받아 로컬 저장하고 중앙 분석 저장소로 비동기 복제한다. 중복 전송은 기본키로 제거하고, 같은 match_id의 결과가 충돌하면 서버 서명·sequence·종료 상태 같은 결정 규칙으로 격리한다.

CREATE TABLE match_event (
  region_code text NOT NULL,
  occurred_at timestamptz NOT NULL,
  event_id uuid NOT NULL,
  match_id uuid NOT NULL,
  sequence_no integer NOT NULL CHECK (sequence_no >= 0),
  event_type text NOT NULL,
  payload jsonb NOT NULL,
  PRIMARY KEY (region_code, occurred_at, event_id),
  UNIQUE (region_code, occurred_at, match_id, sequence_no)
) PARTITION BY RANGE (occurred_at);

CREATE TABLE match_event_2026_08 PARTITION OF match_event
FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');

INSERT INTO match_event VALUES
('ap-northeast', '2026-08-11 12:00+09',
 '00000000-0000-4000-8000-000000000001',
 '10000000-0000-4000-8000-000000000001',
 1, 'match_started', '{"map":"citadel"}')
ON CONFLICT DO NOTHING;

PostgreSQL 파티션의 UNIQUE·PRIMARY KEY에는 모든 파티션 키 열이 포함되어야 하므로 예제 키에 occurred_at이 들어간다. 이것은 단일 클러스터의 선언적 파티셔닝이며, 여러 리전에 자동 분산되는 샤딩은 아니다. 샤드 키로 region_code를 택하면 지역 쓰기는 쉽지만 전 세계 player_id 조회와 리전 이동은 fan-out·재배치 비용을 만든다.

결제 원장 — 하나의 권위 경로에서 강한 커밋

결제는 파티션 중 양쪽 리전이 같은 잔액을 각각 소비하게 두면 안 된다. 계정별 홈 리전 또는 합의된 리더로 쓰기를 라우팅하고 정족수를 얻지 못하면 성공 응답 대신 재시도 가능한 오류를 반환한다. 읽기 복제본은 내역 탐색에 쓸 수 있지만 승인 직후 잔액 검증은 권위 리더나 일관 읽기를 사용한다.

CREATE TABLE wallet (
  account_id bigint PRIMARY KEY,
  balance bigint NOT NULL CHECK (balance >= 0)
);

CREATE TABLE ledger_entry (
  request_id uuid PRIMARY KEY,
  account_id bigint NOT NULL REFERENCES wallet(account_id),
  amount bigint NOT NULL CHECK (amount <> 0),
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE OR REPLACE FUNCTION spend(
  p_request_id uuid, p_account_id bigint, p_amount bigint
) RETURNS boolean LANGUAGE plpgsql AS $$
DECLARE v_inserted integer;
BEGIN
  IF p_amount <= 0 THEN RAISE EXCEPTION 'amount must be positive'; END IF;

  INSERT INTO ledger_entry(request_id, account_id, amount)
  VALUES (p_request_id, p_account_id, -p_amount)
  ON CONFLICT (request_id) DO NOTHING;
  GET DIAGNOSTICS v_inserted = ROW_COUNT;
  IF v_inserted = 0 THEN RETURN true; END IF;

  UPDATE wallet SET balance = balance - p_amount
  WHERE account_id = p_account_id AND balance >= p_amount;
  GET DIAGNOSTICS v_inserted = ROW_COUNT;
  IF v_inserted = 0 THEN
    RAISE EXCEPTION 'insufficient balance';
  END IF;
  RETURN true;
END;
$$;

예제 함수의 동일 request_id 재호출은 성공으로 처리하는 단순 멱등 계약이다. 운영에서는 기존 요청의 account_id와 amount도 비교해 같은 키에 다른 요청 내용이 들어오면 거부하고, 외부 결제사 호출은 DB 트랜잭션과 분리된 outbox·상태 머신으로 연결한다.

RPO와 RTO를 장애 시나리오에 붙인다

RPO=tfailuretlatest recoverable dataRPO=t_{failure}-t_{latest\ recoverable\ data}
RTO=tservice restoredtfailure detectedRTO=t_{service\ restored}-t_{failure\ detected}

RPO는 허용 가능한 데이터 손실 시간, RTO는 서비스 복구 목표 시간이다. 비동기 복제 지연이 12초인 순간 주 서버와 리전 전체가 사라지면 잠재 RPO는 최소 그 지연만큼 커질 수 있다. 동기 복제도 운영자 오삭제를 모든 복제본에 전파하므로 시점 복구용 백업이 별도로 필요하다.

RTOtotal=Tdetect+Telect+Tpromote+Troute+TwarmupRTO_{total}=T_{detect}+T_{elect}+T_{promote}+T_{route}+T_{warmup}

장애 감지, 리더 선출, 승격, 연결 라우팅, 캐시·풀 워밍 시간을 따로 측정하면 ‘자동 장애조치 지원’이라는 기능명을 실제 RTO로 바꿀 수 있다.

지연과 일관성의 개념적 교환

아래 그래프는 제품 벤치마크가 아닌 개념 예시다. 가로축은 클라이언트와 권위 리더 사이 왕복 지연이며, 지역 복제본의 오래된 읽기는 약 8ms로 일정하고 강한 읽기는 네트워크 왕복 영향을 받는다고 단순화했다. 실제 값은 토폴로지·프로토콜·부하로 측정한다.

리더 거리와 읽기 지연의 개념 예시
020406080100120140160180200050100150200리더까지 네트워크 RTT x (ms)개념적 읽기 지연 (ms)
최신성대기비용최신성 대기 비용

이 그래프는 CAP의 증명이 아니다. 파티션이 없는 정상 상태에서도 더 강한 조정은 지연 비용을 만들 수 있다는 PACELC 관점의 직관이다. 기능별로 결제 승인에는 최신성을, 공개 매치 기록에는 제한된 오래됨을 선택할 수 있다.

운영 검증 체크리스트

  1. 기능별 진실의 원천, 샤드 키, 쓰기 리더, 허용 읽기 모델을 표로 만든다.

  2. 리전 간 링크를 끊고 다수·소수 파티션에서 읽기·쓰기 응답이 설계한 CP/AP 정책과 같은지 확인한다.

  3. 복제 지연 p95/p99, 정족수 커밋 지연, stale read 비율, 충돌·중복 이벤트 수를 관찰한다.

  4. 백업 복원과 리전 장애조치를 실제로 실행해 복구 데이터 시점과 총 RTO를 기록한다.

  5. 샤드 하나에 유명 길드·대형 이벤트가 몰리는 hot key와 샤드 이동 중 이중 쓰기·라우팅 버전 문제를 부하 시험한다.

참고 자료

댓글 0

댓글을 불러오는 중…