LLM WikiAccess-protected knowledge portal
← 스터디 홈
6편 · 약 22분

분산 스토리지 패턴: 샤딩, 파티셔닝, 데이터 지역성

단일 노드의 한계

단일 MySQL 인스턴스가 수백 GB의 데이터를 처리하는 것은 가능하다. 하지만 초당 수십만 건의 쓰기와 수 TB의 활성 데이터가 쌓이기 시작하면 수직 확장(더 큰 서버)만으로는 부족해진다. 가장 강력한 단일 서버에도 물리적 IOPS 한계, 메모리 한계, 네트워크 대역폭 한계가 존재한다.

수평 확장(horizontal scaling)은 데이터를 여러 노드에 분산시킨다. 이때 데이터를 어떻게 나누느냐, 관련 데이터를 같은 노드에 모으느냐, 노드를 추가하거나 제거할 때 어떻게 재분배하느냐가 핵심 설계 결정이다.

이 설계 결정들이 나중에 운영 비용, 쿼리 성능, 장애 범위, 스케일아웃 가능성을 결정한다.


파티셔닝의 두 차원: 수평과 수직

혼용되는 용어들을 먼저 정리한다.

수평 파티셔닝(horizontal partitioning, sharding): 행(row)을 기준으로 데이터를 분리한다. 테이블의 일부 행이 노드 A에, 나머지가 노드 B에 간다. 각 샤드는 같은 스키마를 갖는다.

수직 파티셔닝(vertical partitioning): 열(column)을 기준으로 분리한다. 자주 함께 접근하는 열들을 별도 테이블로 분리하거나(정규화), 무거운 BLOB 컬럼을 별도 저장소로 이동하는 것이 예다.

이 장에서 다루는 것은 수평 파티셔닝—즉 샤딩(sharding)이다.


범위 기반 파티셔닝 (Range Partitioning)

범위 파티셔닝은 파티션 키(shard key)의 값 범위를 기준으로 데이터를 나눈다.

사용자 ID 기준 범위 샤딩:
  샤드 1: user_id 1 ~ 10,000,000
  샤드 2: user_id 10,000,001 ~ 20,000,000
  샤드 3: user_id 20,000,001 ~ 30,000,000

장점:

  • 범위 쿼리가 효율적이다. WHERE user_id BETWEEN 1 AND 5000000은 샤드 1 하나만 조회한다.
  • 라우팅 로직이 단순하다: 어떤 범위가 어떤 샤드인지 정적인 규칙으로 결정된다.
  • 파티션 키 기준 정렬된 스캔이 단일 샤드 안에서 완결된다.

단점—핫스팟:

범위 파티셔닝의 가장 큰 문제는 핫스팟(hotspot)이다. 타임스탬프를 파티션 키로 쓰면, 가장 최신 시간 범위를 담당하는 샤드가 모든 현재 쓰기를 받는다. 나머지 샤드는 유휴 상태가 된다.

이벤트 로그 테이블, created_at 기준 범위 샤딩:
  샤드 1: 2023년 데이터 → 거의 읽기 없음
  샤드 2: 2024년 데이터 → 일부 읽기
  샤드 3: 2025~현재 → 모든 쓰기가 집중됨 (핫스팟)

HBase는 이 문제를 "핫 리전"이라 부른다. 범위 파티셔닝에서 핫스팟을 피하려면 키를 의도적으로 섞거나(salting), 해시 파티셔닝으로 전환해야 한다.


해시 기반 파티셔닝 (Hash Partitioning)

해시 파티셔닝은 파티션 키에 해시 함수를 적용한 결과로 샤드를 결정한다.

hash(user_id) % N (N = 샤드 수)
user_id=42:   hash(42) = 1234567 → 1234567 % 4 = 3 → 샤드 3
user_id=43:   hash(43) = 9876543 → 9876543 % 4 = 3 → 샤드 3
user_id=100:  hash(100) = 4567890 → 4567890 % 4 = 2 → 샤드 2

장점:

  • 키가 고르게 분산된다. 핫스팟이 사라진다.
  • 특정 키가 어디 있는지 연산만으로 알 수 있다. 디렉터리 조회가 없다.

단점:

  • 범위 쿼리가 비효율적이다. WHERE user_id BETWEEN 1 AND 1000은 모든 샤드를 스캔해야 한다.
  • 리샤딩 문제: 샤드 수 N이 바뀌면 hash(key) % N의 결과가 달라진다. 거의 모든 데이터를 재분배해야 한다.

리샤딩 문제를 해결하는 것이 일관된 해싱(consistent hashing)이다.


일관된 해싱과 가상 노드

일관된 해싱(consistent hashing)은 해시 공간을 원형 링으로 표현한다. 각 노드와 각 키 모두 같은 해시 함수로 링 위의 위치를 결정받는다. 키는 링을 시계방향으로 따라가다 처음 만나는 노드에 배정된다.

Hash Ring 0 ~ 2³² Node A vnode ×3 Node B vnode ×3 Node C vnode ×3 Node D vnode ×3 Key X Key Y → Node B 노드 추가 시 이전 방식(% N): 거의 전체 키 이동 일관된 해싱: K/N 비율만 이동
일관된 해싱 링과 가상 노드

일관된 해싱의 핵심 장점: 노드가 N개일 때 새 노드를 추가하면 평균적으로 전체 키의 1/(N+1)만 이동하면 된다. 기존 hash % N 방식처럼 거의 모든 키를 재분배하지 않아도 된다.

가상 노드 (Virtual Nodes, VNodes)

물리 노드 하나를 링 위의 단일 지점으로 표현하면, 노드 분포가 불균등하게 됩니다. 가상 노드(vnode)는 각 물리 노드를 링 위의 여러 지점으로 표현해 분산을 균등하게 만든다.

Cassandra는 기본적으로 물리 노드당 256개의 vnode를 사용한다. vnode의 장점:

  • 데이터 분포가 훨씬 균등해진다.
  • 노드 장애 시 해당 노드의 vnode들이 여러 생존 노드로 흩어지므로 부하가 골고루 분산된다.
  • 이기종 클러스터에서 강력한 노드에 더 많은 vnode를 할당해 가중치를 줄 수 있다.

디렉터리 기반 파티셔닝 (Directory-Based Sharding)

디렉터리 샤딩은 별도의 조회 서비스(샤드 맵)가 어떤 키가 어느 샤드에 있는지 매핑을 관리한다.

Application ↓ "user:alice"
Shard Directory
alice → Shard 2
bob → Shard 1
carol → Shard 3
↓ Shard 2
Shard 1 (DB) Shard 2 (DB) ← 선택됨 Shard 3 (DB)
샤드 맵이 중앙화되어 유연하게 재배치 가능. 샤드 맵 자체가 단일 장애점이 될 수 있음
디렉터리 기반 샤딩 아키텍처

장점:

  • 특정 사용자(고트래픽 계정)를 전용 샤드로 이동시킬 수 있다.
  • 샤드 구조를 바꿔도 애플리케이션 로직을 수정하지 않는다.
  • 단계적, 외과적 재균형이 가능하다.

단점:

  • 모든 쿼리에 샤드 맵 조회가 추가된다(레이턴시 증가).
  • 샤드 맵이 단일 장애점(SPOF)이 될 수 있다. 캐싱과 복제가 필요하다.
  • 샤드 맵 자체가 일관성을 유지해야 하는 분산 데이터가 된다.

세 전략 비교와 선택 기준

전략범위 쿼리핫스팟 위험리샤딩 비용라우팅 복잡도
범위효율적높음낮음낮음
해시비효율낮음높음낮음
일관된 해싱비효율낮음낮음 (K/N)중간
디렉터리정책에 따름제어 가능유연높음

실제 시스템은 단일 전략만 쓰지 않는다. Cassandra는 일관된 해싱(vnode)으로 행을 분배하고, 동일 파티션 키를 가진 행들은 하나의 파티션 내에서 클러스터링 키로 정렬된다. MongoDB는 복합 샤드 키(hash + range 조합)를 지원한다.


샤드 키 설계: 가장 중요한 결정

샤드 키 선택은 나중에 바꾸기 매우 어렵다. 잘못 선택하면 핫스팟, 크로스샤드 쿼리 폭증, 재균형 불능으로 이어진다.

좋은 샤드 키의 조건

높은 카디널리티: 값의 종류가 많아야 데이터를 충분히 분산시킬 수 있다. 성별(M/F)은 샤드 키로 적합하지 않다.

균등한 분포: 특정 값에 쓰기/읽기가 집중되지 않아야 한다. 타임스탬프는 현재 값에 집중된다.

쿼리 패턴과 일치: 가장 빈번한 쿼리가 단일 샤드 내에서 완결되도록 설계한다. 예를 들어 사용자 기반 서비스라면 user_id를 샤드 키로 써서 한 사용자의 모든 데이터가 같은 샤드에 오도록 한다.

예시:

전자상거래 주문 테이블:

나쁜 선택: order_date (타임스탬프 핫스팟)
나쁜 선택: status (카디널리티 낮음: pending/shipped/delivered)
좋은 선택: customer_id (균등 분포 + 고객별 주문 조회가 단일 샤드)
더 좋은 선택: hash(customer_id) + order_id (복합, 범위 쿼리도 부분 지원)

데이터 지역성 (Data Locality)

데이터 지역성은 자주 함께 접근하는 데이터를 같은 노드(또는 가까운 노드)에 배치하는 원칙이다.

공동 배치 (Co-location)

주문(orders)과 주문 상세(order_items)가 별개의 테이블이라면, order_id를 기준으로 두 테이블을 같은 샤드에 배치하면 조인이 로컬에서 완결된다.

공동 배치 (권장)
Shard 1 (order_id 1~100만)
orders
id=42
JOIN ↔ order_items
order_id=42
로컬 조인. 네트워크 전송 없음
크로스샤드 조인 (위험)
Shard 1 orders
id=42

네트워크
Shard 3 order_items
order_id=42
네트워크 왕복 + 데이터 전송 비용
공동 배치 vs 크로스샤드 조인

Cassandra에서는 같은 파티션 키를 가진 행들이 자동으로 같은 노드에 저장된다. 이를 활용해 파티션 키를 (user_id, month)로 잡으면 특정 사용자의 한 달치 데이터가 항상 같은 파티션에 모인다.

계층적 데이터 배치

지리적 분산 시스템에서는 데이터 지역성이 네트워크 레이턴시와 직결된다.

  • 리전 지역성: 사용자와 가까운 리전에 해당 사용자의 데이터를 배치한다. 유럽 사용자 데이터는 EU 리전에, 아시아 사용자는 아시아 리전에.
  • 존(zone) 지역성: 동일 가용 영역(AZ) 내에서 연관 서비스와 데이터를 배치해 교차 AZ 트래픽 비용을 줄인다.

크로스샤드 쿼리 문제

샤딩의 대가는 크로스샤드 쿼리다. 파티션 키를 포함하지 않는 쿼리는 모든 샤드에 요청을 보내야 한다.

스캐터-개더 (Scatter-Gather)

SELECT * FROM orders WHERE status = 'PENDING';
-- status는 샤드 키가 아님
-- → 모든 샤드로 요청 전송 (scatter)
-- → 각 샤드 응답 수집 및 병합 (gather)
-- → 샤드 수에 비례해 비용 증가

스캐터-개더는 임시 분석에는 허용할 수 있지만, 초당 수천 번 실행되는 OLTP 쿼리에는 치명적이다.

보조 인덱스 전략

비파티션 키 컬럼으로 자주 쿼리한다면 별도의 보조 인덱스 샤드를 운영한다.

글로벌 보조 인덱스:
  email_to_shard 테이블 (별도 저장소 또는 Redis):
    "[email protected]" → 샤드 3
    "[email protected]"  → 샤드 1

쿼리: SELECT * FROM users WHERE email = '[email protected]'
  1. 글로벌 보조 인덱스 조회: alice → 샤드 3
  2. 샤드 3에만 쿼리
  3. 스캐터-개더 없음

DynamoDB의 GSI(Global Secondary Index)가 이 패턴의 관리형 구현이다.


리샤딩 전략

비즈니스가 성장하면 샤드를 추가해야 한다. 리샤딩은 운영 중 가장 위험한 작업 중 하나다.

온라인 리샤딩의 일반적 순서:

  1. 새 샤드를 읽기 전용 복제본으로 추가한다.
  2. 이전 샤드에서 새 샤드로 데이터를 증분 복사한다(이중 쓰기 기간).
  3. 트래픽의 일부를 새 샤드로 라우팅하기 시작한다(canary).
  4. 데이터가 완전히 동기화되면 이전 샤드의 해당 범위 데이터를 삭제한다.

MySQL + Vitess, MongoDB의 자동 밸런서, Cassandra의 nodetool move 등이 이 과정을 자동화한다.

핫스팟 샤드 분리: 특정 샤드가 불균형하게 증가하면 해당 샤드만 분할(split)한다. 일관된 해싱에서는 새 vnode를 추가해 해당 구간만 분리할 수 있다.


운영 관점 판단 기준

샤드 키를 선택할 때 확인할 것:

  1. 가장 빈번한 쿼리의 WHERE 절에 반드시 이 키가 들어가는가?
  2. 이 키의 카디널리티가 샤드 수보다 충분히 큰가?
  3. 이 키로 특정 값이나 값 범위에 쓰기가 집중되지 않는가?
  4. 비즈니스 성장 방향에서 이 키의 분포가 쏠리지 않을 것인가?

운영 모니터링:

샤드별 QPS, 지연, 디스크 사용량을 대시보드에 표시한다.
핫 샤드 경보: 한 샤드가 전체 QPS의 30% 이상을 처리하면 경보
샤드 크기 예측: 샤드별 성장 속도를 추적해 다음 리샤딩 시점 예측

피해야 할 안티패턴:

안티패턴문제대안
자동증가 ID를 샤드 키로한 샤드에 모든 쓰기 집중hash(id) 또는 UUID
낮은 카디널리티 컬럼 (status, type)값 몇 개가 전체 부하고카디널리티 키와 복합 사용
파티션 키 없는 범용 쿼리를 OLTP로스캐터-개더 폭증OLAP 분리, 글로벌 인덱스
리샤딩 계획 없이 샤드 추가리샤딩 중 장애온라인 리샤딩 계획 수립

실세계 구현 사례

Cassandra: 일관된 해싱(Murmur3) + vnode(256개/물리 노드). 파티션 키가 같은 행들은 같은 노드에 공동 배치. 클러스터링 키로 파티션 내 정렬.

DynamoDB: 파티션 키로 일관된 해싱. GSI(글로벌 보조 인덱스)로 비파티션 키 쿼리 지원. 파티션 크기가 10GB를 초과하거나 처리량 한도를 초과하면 자동 분할.

MongoDB: mongos 라우터가 샤드 클러스터의 진입점. 복합 샤드 키(해시 + 범위 조합) 지원. 청크(chunk) 단위로 자동 마이그레이션.

MySQL + Vitess: 애플리케이션 레이어 샤딩. VTGate가 SQL을 파싱해 적절한 샤드로 라우팅. 온라인 리샤딩 워크플로 지원.


기억할 문장

샤딩은 확장성의 도구이지, 기본 설정이 아니다. 단일 노드로 가능하다면 샤딩하지 않는 것이 옳다. 샤딩은 조인, 트랜잭션, 글로벌 쿼리의 단순함을 희생하고 확장성을 산다.

불가피하다면, 샤드 키 하나를 충분히 고민해서 선택하라. 그 선택이 이후 수년간의 운영 복잡도를 결정한다. 데이터 지역성을 의식적으로 설계하면 크로스샤드 쿼리를 크게 줄일 수 있고, 일관된 해싱과 vnode는 리샤딩의 비용을 관리 가능한 수준으로 낮춘다.

다음 장에서는 분산 시스템에서 노드 장애를 감지하고 복구하는 패턴—heartbeat, circuit breaker, bulkhead—을 살펴본다.

References

  • Kleppmann, M. (2017). Designing Data-Intensive Applications, Chapter 6: Partitioning — https://dataintensive.net/
  • Microsoft Azure Architecture Center: Sharding Pattern — https://learn.microsoft.com/en-us/azure/architecture/patterns/sharding
  • Apache Cassandra: Data Distribution using Virtual Nodes — https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/architecture/archDataDistributeDistribute.html
  • Apache Cassandra Architecture: Dynamo — https://cassandra.apache.org/doc/latest/cassandra/architecture/dynamo.html
  • MongoDB: Choose a Shard Key — https://www.mongodb.com/docs/manual/core/sharding-choose-a-shard-key/
  • Karger, D. et al. (1997). Consistent Hashing and Random Trees — https://dl.acm.org/doi/10.1145/258533.258660
  • Maymounkov, P. & Mazières, D. (2002). Kademlia: A Peer-to-peer Information System — https://dl.acm.org/doi/10.1007/3-540-45748-8_5
  • DeCandia, G. et al. (2007). Dynamo: Amazon's Highly Available Key-value Store — https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf
  • Last9: Database Sharding — How It Works and When You Actually Need It — https://last9.io/blog/database-sharding/
  • Aerospike: What Is Sharding and How It Works for Database Scale — https://aerospike.com/blog/what-is-sharding/