샤딩 전략 설계: Vertical·Range·Hash·Directory 선택 기준
Vertical, Range, Key/Hash, Directory 샤딩 전략의 라우팅 방식과 재샤딩, 운영 설계 기준을 정리한다.
2026-08-14 · 최초 발행 2025-10-14
단일 DB의 한계를 넘을 때, 먼저 라우팅 기준을 정한다
샤딩은 데이터 세트를 여러 샤드에 나누어 독립적으로 저장하고 처리하는 분산 데이터 관리 기법이다. 수평 또는 수직으로 분할한 뒤, 각 요청이 어떤 샤드로 가야 하는지 결정하는 규칙이 설계의 중심이 된다.
목표는 단순히 저장 공간을 늘리는 데 있지 않다. 확장성을 확보하고, 성능 간섭을 분리하며, 장애 도메인을 줄여 가용성을 높이는 데 있다. 전략을 고를 때는 베랑키디, 즉 Vertical·Range·Key(Hash)·Directory라는 네 가지 관점을 함께 검토할 수 있다.
| 전략 | 라우팅 기준 | 강점 | 설계 시 주의점 |
|---|---|---|---|
| Vertical(수직) | 테이블 또는 도메인별 물리 분리 | 구현이 단순하고 팀 단위 소유권이 분명하다 | 데이터 크기 불균형, 교차 조인 비용 증가, 추가 수평 샤딩 필요 가능성 |
| Range(범위) | 날짜·숫자 등의 연속 구간 | 범위 질의와 아카이빙에 유리하다 | 신규 구간 집중으로 핫스팟이 생길 수 있고 재샤딩 때 대량 이동이 발생한다 |
| Key/Hash(키/해시) | user_id 등의 해시 값 | 균형 잡힌 분산과 핫스팟 완화에 유리하다 | 범위 질의가 비효율적이며 교차 샤드 집계가 필요하다 |
| Directory(디렉터리) | 룩업 테이블 또는 서비스 | 비균등 파티션에도 유연하게 대응한다 | 디렉터리의 일관성과 가용성을 관리해야 하며 컴포넌트가 늘어난다 |
요청이 샤드에 도달하는 경로
라우터 또는 Shard Manager는 도메인, 범위, 해시, 디렉터리 규칙을 바탕으로 대상 샤드를 선택한다. 특히 Directory 방식은 동적 매핑을 사용하므로 데이터량이 고르지 않거나 멀티테넌트 구조를 운영할 때 적합하다.
Vertical은 사용자·정산·로그처럼 도메인을 분리해 팀별 스키마와 스케일을 독립적으로 운영하는 방식에 맞는다. Range는 시계열 IoT, 로그, 거래 데이터를 월별로 나누고 콜드 스토리지로 아카이빙하는 상황에 활용할 수 있다.
Key/Hash는 SNS 타임라인이나 전자상거래 주문을 user_id 해시로 고르게 나누는 데 적합하다. Directory는 멀티테넌트 SaaS에서 테넌트와 샤드를 매핑하거나, 프리미엄 고객을 전용 고성능 샤드에 배치하는 식으로 쓴다.
증설은 재샤딩 절차까지 포함한다
샤드를 늘리거나 핫스팟을 해소할 때는 데이터 이동 자체보다 전환 중 정합성을 유지하는 일이 어렵다. 무중단 전환과 정합성 검증을 중심에 두고, 로그 리플레이 또는 CDC(Change Data Capture) 기반 백필을 사용한다.
라우팅 규칙을 먼저 갱신하고, 증분 마이그레이션과 검증을 거친 뒤 트래픽을 절체한다. 구 샤드를 해제하는 시점은 이 흐름의 마지막에 둔다.
라우팅 규칙은 전략별로 달라진다
function route(op, entity):
switch strategy(entity):
case VERTICAL:
return shard_for_domain(entity.domain)
case RANGE:
r = range_key(entity)
return shard_for_range(r)
case HASH:
k = shard_key(entity)
h = hash(k)
return shard_ring.locate(h)
case DIRECTORY:
k = routing_key(entity)
return directory.lookup(k)
Range와 Hash를 함께 쓰는 경우에는 범위로 큰 단위를 나눈 뒤, 내부 분산 방식을 별도로 설계할 수 있다.
PostgreSQL 파티셔닝
-- 범위 파티셔닝: 월별 파티션
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
amount NUMERIC(18,2) NOT NULL
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_2025_08 PARTITION OF orders
FOR VALUES FROM ('2025-08-01') TO ('2025-09-01');
-- 사용자 ID 해시 서브파티셔닝 (선택: DB 확장/확장자에 따라 구현)
-- 예: 파티션 내부를 해시 기반 서브 파티션으로 분할하는 설계 지침
Python 일관 해시 라우터
import bisect, hashlib
class HashRing:
def __init__(self, nodes, vnodes=128):
self.ring = []
self.nodes = []
for n in nodes:
for v in range(vnodes):
key = f"{n}:{v}".encode()
h = int(hashlib.md5(key).hexdigest(), 16)
self.ring.append((h, n))
self.ring.sort(key=lambda x: x[0])
self.sorted_hashes = [h for h, _ in self.ring]
def locate(self, k: str):
h = int(hashlib.md5(k.encode()).hexdigest(), 16)
i = bisect.bisect_left(self.sorted_hashes, h)
if i == len(self.ring):
i = 0
return self.ring[i][1]
# 사용 예: HashRing(["shard-a","shard-b","shard-c"]).locate("user:1234")
운영 중 드러나는 경계
샤드 키는 쓰기 집중과 범위 질의 특성을 함께 반영해야 한다. user_id+created_at처럼 복합 키를 고려할 수 있으며, Range 전략의 핫스팟은 salting 또는 bucketing으로 완화한다.
일관 해시에 가상노드(vnode)를 적용할 때는 노드당 100~200 vNode를 권장한다. 증설이나 장애가 발생했을 때 데이터 이동을 줄이기 위한 선택이다.
Directory 전략은 매핑을 캐시(예: Redis)와 영속 저장소로 이중화하고, TTL·버전 필드로 점진적으로 갱신한다. 장애 시 읽기 전용 폴백 경로도 마련한다.
교차 샤드 트랜잭션은 가능한 한 줄여야 한다. 도메인 경계를 다시 나누거나 사가(Saga) 패턴을 도입하는 방법이 있다. 교차 샤드 질의에는 fan-out + partial aggregation 패턴과 결과 캐시 계층을 적용할 수 있다. 백업과 복구는 샤드 단위 스냅샷으로 수행하되, 카탈로그(디렉터리)와 같은 시점의 정합성을 유지한다.
분리 기준에서 동적 매핑까지
도메인 경계를 Vertical로 정리하고, 시계열과 아카이브를 Range로 다루며, 균형 분산에는 Hash를 적용할 수 있다. 비균등한 데이터나 VIP 고객처럼 정적인 분산 규칙으로 처리하기 어려운 경우에는 Directory가 선택지가 된다.
실제 설계에서는 하나의 방식에 고정되기보다 조직과 데이터 특성에 맞춰 혼합 전략을 채택한다. 관측과 메트릭을 바탕으로 재샤딩 자동화 프레임까지 설계해야 장기 운영에서 비용·성능·민첩성의 균형을 유지할 수 있다.