컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
db · 09

샤딩

DB샤딩분산

데이터가 한 서버의 용량과 처리량을 넘어서면, 인덱스를 잘 걸어도 한계가 올 수 있습니다. 샤딩(Sharding)은 같은 스키마의 데이터를 여러 서버(샤드)에 나눠 저장해서 하나의 큰 테이블을 여러 개의 작은 테이블로 수평 분할하는 방법입니다.

핵심 개념

수평 분할 (Horizontal Partitioning)

같은 구조의 테이블을 행(row) 단위로 여러 샤드에 나눠 저장합니다. 각 샤드는 전체 데이터의 일부만 가집니다.

핵심 개념

샤드 키 (Shard Key)

어떤 값을 기준으로 행을 어느 샤드에 보낼지 정하는 기준 컬럼입니다. 선택을 잘못하면 특정 샤드로 쏠릴 수 있습니다.

핵심 개념

해시 샤딩 (Hash Sharding)

샤드 키를 해시한 값으로 샤드를 결정합니다. 가장 단순한 방식은 해시값을 샤드 개수로 나눈 나머지를 쓰는 것이고, MongoDB나 Citus는 해시값의 범위를 샤드에 배정합니다. 데이터가 고르게 퍼지지만 범위 조회가 어렵습니다.

핵심 개념

레인지 샤딩 (Range Sharding)

샤드 키의 값 범위로 샤드를 결정합니다. 범위 조회는 쉽지만 특정 구간에 쓰기가 몰리는 핫스팟이 생길 수 있습니다.

핵심 개념

크로스 샤드 쿼리

여러 샤드에 걸친 JOIN이나 집계는 비용이 크고, 애플리케이션이 직접 샤딩하는 구조에서는 각 샤드 결과를 따로 모아 합쳐야 합니다. MongoDB의 mongos처럼 라우터가 대신 모아 주는 제품도 있습니다.

핵심 개념

리샤딩 (Resharding)

샤드 개수를 늘리거나 줄일 때 기존 데이터를 재분배해야 합니다. 서비스 규모가 커질수록 이 작업 비용도 커집니다.

해시 vs 레인지, 결국 트레이드오프 — 해시 샤딩은 데이터를 고르게 퍼뜨리는 대신 "최근 가입자 조회" 같은 범위 조회가 여러 샤드를 다 뒤져야 해서 비쌉니다. 레인지 샤딩은 그 반대입니다.
레인지 샤딩의 핫스팟 — 가입일 순으로 샤딩하면 신규 가입자 쓰기가 대부분 최신 샤드 하나에 몰립니다. 나머지 샤드는 한가한데 그 샤드만 바빠지는 불균형이 생깁니다.
샤드 키는 나중에 바꾸기 어렵습니다 — 샤드 키를 바꾸려면 이미 흩어져 저장된 데이터를 새 기준으로 다시 나눠야 해서 비용이 큽니다(일부 제품은 온라인 리샤딩을 지원하지만 여분의 저장 공간과 시간이 듭니다). 그래서 설계 초기에 신중하게 정합니다.
이 시각화에서 확인할 것 — "해시 샤딩"과 "레인지 샤딩" 탭에서 같은 예시 키들이 어느 샤드로 가는지 STEP으로 비교하고, 왜 결과가 다른지 확인하세요.

샤딩과 비슷한 말 구분하기

파티셔닝(Partitioning) — 큰 테이블을 기준에 따라 여러 조각으로 나누는 일입니다. PostgreSQL의 선언적 파티셔닝처럼 한 DB 서버 안에서 나눌 수도 있습니다.
샤딩(Sharding) — 파티셔닝된 조각(샤드)을 서로 다른 서버에 두어 저장 용량과 처리량을 함께 늘립니다.
복제(Replication) — 같은 데이터의 사본을 여러 서버에 둡니다. 읽기와 장애 대비에 도움이 되지만 데이터가 한 서버에 다 들어가야 한다는 한계는 그대로입니다.

샤드 수를 바꾸면 얼마나 옮겨야 할까

해시값을 샤드 수로 나눈 나머지(hash(key) % N)로 샤드를 정하면 N이 바뀔 때 대부분의 키가 다른 샤드로 이동합니다. 예를 들어 샤드가 3개에서 4개가 되면, 정수 키 기준으로 나머지가 그대로인 키는 12개 중 3개뿐이어서 약 75%의 키가 옮겨 가야 합니다. 이 문제를 줄이는 방법이 일관된 해싱(consistent hashing)으로, 샤드를 해시 공간의 원 위에 배치해 샤드를 추가해도 일부 키만 옮기게 합니다. Amazon의 Dynamo 논문이 이 방식을 사용한 설계로 알려져 있고, Cassandra 공식 문서도 노드를 해시 링 위의 토큰에 대응시키는 방식을 설명합니다.

자주 하는 오해

샤딩은 읽기와 쓰기를 모두 늘려 주는 만능 해법이다 — 데이터를 나누는 순간 여러 샤드에 걸친 조회, JOIN, 트랜잭션이 어려워지고 운영도 복잡해집니다.
파티셔닝과 샤딩은 같은 말이다 — 파티셔닝은 데이터를 나누는 일 자체를, 샤딩은 그렇게 나눈 조각을 여러 서버에 분산하는 것을 가리킵니다.
해시 샤딩이면 부하가 항상 고르게 퍼진다 — 키가 고르게 분포하면 데이터는 고르게 퍼지지만, 특정 키(예: 인기 계정) 하나에 요청이 몰리면 그 키가 있는 샤드는 여전히 바쁩니다.
샤드를 추가하면 성능이 샤드 수에 비례해 늘어난다 — 여러 샤드를 모아야 하는 조회와 분산 트랜잭션의 비용이 늘어서 보통 그보다 덜 늘어납니다.

면접에서 이렇게 물어요

면접 질문

좋은 샤드 키는 어떤 조건을 갖나요?

값의 종류가 많아 데이터를 잘게 나눌 수 있고, 값이 고르게 분포하며, 자주 쓰는 조회가 한 샤드 안에서 끝나게 하는 키입니다. 범위 샤딩에서 시간순으로 증가하는 값을 그대로 쓰면 새 데이터가 한 샤드에 몰립니다(해시 샤딩이면 완화됩니다).

면접 질문

여러 샤드에 걸친 JOIN과 트랜잭션은 어떻게 처리하나요?

자주 같이 쓰는 데이터를 같은 샤드 키로 모아 두어 샤드 안에서 끝나게 설계하는 것이 우선입니다. 불가피하면 라우터가 각 샤드에 질의해 결과를 합치거나, 비용이 큰 분산 트랜잭션을 씁니다.

면접 질문

샤드를 늘릴 때 데이터를 어떻게 옮기나요?

해시 나머지 방식은 대부분의 키가 이동하므로 일관된 해싱처럼 이동하는 키를 줄이는 방식을 씁니다. 서비스를 멈추지 않고 옮기려면 복사하는 동안 변경분을 따라잡는 절차가 필요합니다.

면접 질문

샤딩하기 전에 먼저 해 볼 것은 무엇인가요?

쿼리와 인덱스 개선, 읽기 부하를 나누는 복제, 서버 사양 증설 등이 있습니다. 샤딩은 데이터를 다시 나누는 비용이 크고 여러 샤드에 걸친 조회가 어려워지는 대가가 있습니다.

실제 시스템에서는

MongoDB — 샤드 클러스터에서 앱이 mongos라는 라우터에 요청하면 라우터가 알맞은 샤드로 보냅니다. 샤드 키는 해시 방식과 범위 방식 중에서 고를 수 있고, 값이 계속 커지는 키를 범위 방식에 쓰면 쓰기가 한 곳에 몰리므로 해시 샤딩을 쓰라고 안내합니다.
Citus — PostgreSQL 확장으로, 분산 테이블의 한 컬럼(분산 컬럼)을 기준으로 행을 여러 노드의 샤드에 나눕니다.
Cassandra와 Dynamo 계열 — 파티션 키를 해시해 노드에 분산하고, 일관된 해싱 계열의 방식을 씁니다.

함께 보면 좋은 토픽

복제 — 같은 데이터를 여러 서버에 두는 방법과 샤딩의 차이를 봅니다.
NoSQL — 수평 확장을 염두에 둔 저장소가 많은 NoSQL을 봅니다.
인덱스 — 조회 성능을 높이는 기본 수단인 인덱스를 봅니다.
트랜잭션 — 샤드를 넘나드는 트랜잭션이 왜 어려운지 봅니다.

참고 자료

PostgreSQL — 선언적 파티셔닝(Hash / Range)

-- 해시 샤딩과 같은 원리: 키를 나머지 연산으로 파티션에 배정
CREATE TABLE users (
    user_id BIGINT,
    name TEXT
) PARTITION BY HASH (user_id);

CREATE TABLE users_p0 PARTITION OF users
    FOR VALUES WITH (MODULUS 3, REMAINDER 0);
CREATE TABLE users_p1 PARTITION OF users
    FOR VALUES WITH (MODULUS 3, REMAINDER 1);

-- 레인지 샤딩과 같은 원리: 값 구간으로 파티션에 배정
CREATE TABLE users_by_range (
    user_id BIGINT,
    name TEXT
) PARTITION BY RANGE (user_id);

CREATE TABLE users_range_a PARTITION OF users_by_range
    FOR VALUES FROM (0) TO (150);

PostgreSQL 선언적 파티셔닝은 이 시각화의 해시/레인지 샤딩과 정확히 같은 개념을 단일 서버 안의 여러 테이블(파티션)로 구현한 것입니다. 실제 다중 서버 샤딩은 여기에 "어느 서버에 어느 파티션이 있는가"를 라우팅하는 계층이 하나 더 필요합니다.

Java — 애플리케이션 레벨 샤드 라우팅(개념)

public class ShardRouter {

    private static final int SHARD_COUNT = 3;

    // 해시 샤딩: 키 % 샤드 개수
    public int resolveShard(long userId) {
        return (int) (userId % SHARD_COUNT);
    }

    // 이 시각화의 ROUTER가 하는 일이 바로 이 메서드입니다
    public DataSource dataSourceFor(long userId) {
        return shardDataSources[resolveShard(userId)];
    }
}

실제로는 DataSource 여러 개를 미리 만들어두고, 요청마다 샤드 키로 라우팅 대상을 계산해 해당 샤드의 커넥션을 사용합니다. 샤드 키가 없는 쿼리(예: 이메일로 사용자 찾기)는 모든 샤드를 다 조회해야 해서(scatter-gather) 느립니다 — 그래서 샤드 키 선택이 중요합니다.

SQL — 크로스 샤드 집계는 애플리케이션에서 합친다

-- 각 샤드에서 개별 실행 (DB가 자동으로 합쳐주지 않음)
SELECT COUNT(*) FROM users; -- Shard 1에서 실행 → 1200
SELECT COUNT(*) FROM users; -- Shard 2에서 실행 → 980
SELECT COUNT(*) FROM users; -- Shard 3에서 실행 → 1450

-- 애플리케이션에서 세 결과를 더해 총합을 계산: 1200 + 980 + 1450 = 3630

단일 서버였다면 COUNT(*) 한 번이면 끝나지만, 샤딩된 환경에서는 각 샤드에 같은 쿼리를 따로 보내고 결과를 애플리케이션에서 합쳐야 합니다. 이런 집계·조인이 잦다면 샤딩의 비용이 이점보다 커질 수 있습니다.

Q1

해시 샤딩(Hash Sharding)의 특징은?

'해시'라는 이름처럼 계산을 거쳐 흩뿌립니다.
해시 샤딩은 샤드 키를 해시하고 그 값을 샤드 개수로 나눈 나머지로 샤드를 결정해, 데이터를 여러 샤드에 고르게 분산시킵니다. 대신 범위 조회는 여러 샤드를 다 뒤져야 해서 불리합니다.

Q2

레인지 샤딩에서 '핫스팟'이 생기는 대표적인 이유는?

가입일 순으로 샤딩하면 신규 가입자는 전부 어느 샤드로 갈까요?
레인지 샤딩은 값 범위로 샤드를 나누므로, 최신 데이터(예: 신규 가입자)가 몰리는 구간을 담당하는 샤드 하나에 쓰기가 집중되는 핫스팟이 생길 수 있습니다.

Q3

샤딩 환경에서 여러 샤드에 걸친 집계(예: 전체 사용자 수)를 구하는 방법은?

DB가 샤드 간 경계를 넘어 자동으로 합쳐주지 않습니다.
샤딩된 환경에서는 각 샤드에 같은 쿼리를 따로 보내고, 그 결과를 애플리케이션 레벨에서 합산해야 합니다(scatter-gather). 이런 집계·조인이 잦다면 샤딩 비용이 커집니다.