데이터를 샤드 키로 나눠 여러 노드에 분산하는 것. 복제가 못 하는 쓰기·용량 확장을 해결한다.
복제와의 역할 분담
| 복제 | 같은 데이터를 복사한다 | → 가용성 · 읽기 확장 |
|---|---|---|
| 샤딩 | 데이터를 쪼갠다 | → 쓰기 확장 · 용량 확장 |
- 실제 시스템은 둘을 함께 쓴다 (각 샤드를 다시 복제한다)
샤드 키 선택이 전부다
좋은 샤드 키
- 값이 고르게 분산된다
- 대부분의 질의가 한 샤드로 간다
나쁜 샤드 키가 만드는 것
- 핫스팟 — 한 샤드에만 트래픽이 몰린다 (예: 국가 코드로 샤딩했는데 90%가 한 나라)
- 교차 샤드 질의 — 여러 샤드에서 모아야 답이 나온다 (가장 느린 샤드에 묶인다)
범위 vs 해시
범위 샤딩 (날짜·ID 구간)
- ✅ 범위 질의가 한 샤드에서 끝난다
- ❌ 최신 데이터에 쏠린다 (오늘 자 샤드만 뜨겁다)
해시 샤딩 (hash(key) % N)
- ✅ 고르게 분산된다
- ❌ 범위 질의가 모든 샤드로 흩어진다
리밸런싱 — 단순 나머지 연산의 함정
- hash(key) % 3 — → 노드를 4대로 늘리면 hash(key) % 4
- 거의 모든 키의 목적지가 바뀐다 → 전체 데이터 이동
일관된 해싱(consistent hashing)
- 링 위에 노드를 배치해, 노드 하나 추가 시
- 인접 구간만 이동한다 → 이동량 최소화
무엇을 포기하는가
-- 교차 샤드 조인 — 샤드가 다르면 DB 가 조인해 줄 수 없다
SELECT * FROM orders o JOIN member m ON o.member_id = m.id;
-- → 애플리케이션이 두 번 조회해 메모리에서 합치거나,
-- 회원 정보를 각 샤드에 복제해 두거나(반정규화),
-- member_id 를 샤드 키로 삼아 같은 샤드에 놓는다
-- 교차 샤드 트랜잭션 — 원자성이 공짜가 아니다
-- → 2PC(느리고 블로킹) 또는 Saga(보상 트랜잭션)
샤딩은 조인과 분산 트랜잭션을 포기하거나 비싸게 사는 거래다. 그래서 순서가 있다.
- ① 인덱스·쿼리 튜닝
- ② 읽기 복제본 추가
- ③ 수직 분할 (테이블 단위로 DB 분리)
- ④ 캐시
- ⑤ 그래도 안 되면 샤딩
면접 함정
- ❌ "샤딩하면 성능이 좋아진다" → 단일 샤드로 끝나는 질의만 그렇다. 교차 샤드 질의는 오히려 느려진다.
- ❌ "샤드 키는 나중에 바꾸면 된다" → 전체 재분배라 사실상 서비스 이전 수준의 작업이다.
실제로 어떻게 구현하나
- ① 애플리케이션 레벨 라우팅
- 샤드 키로 어느 DataSource 를 쓸지 코드가 정한다. 단순하지만 전부 직접 만든다
- ② 프록시 계층
- ProxySQL · Vitess(MySQL) · Citus(PostgreSQL 확장) 가 라우팅을 대신한다
- ③ 네이티브 분산 DB
- CockroachDB · TiDB — 처음부터 분산으로 설계된 제품을 쓴다
-- Citus: PostgreSQL 을 그대로 두고 분산 테이블로 만든다
SELECT create_distributed_table('orders', 'tenant_id');
-- tenant_id 가 같은 행은 같은 노드에 모인다 → 테넌트 안의 조인은 로컬로 끝난다
마지막 줄이 샤드 키 설계의 정석이다 — 함께 조회되는 데이터를 같은 샤드에 모은다(colocation).
전역 유일성이 깨진다
AUTO_INCREMENT 는 샤드마다 따로 돈다 → id 가 겹친다
대안
- 샤드별 시작값을 다르게 (1, 2, 3... / 1001, 1002...)
- UUIDv7 · ULID (시간순 정렬 + 전역 유일)
- Snowflake ID (타임스탬프 + 노드ID + 시퀀스)