1. 프롤로그 - "도서관이 꽉 찼어요"
동네 도서관에 책이 너무 많아서 건물이 무너지기 직전입니다. 해결책은 두 가지입니다.
- Scale-Up: 건물을 100층으로 증축한다. (비싸고, 엘리베이터가 터져나감).
- Scale-Out (Sharding): 옆 동네에 '제2 도서관', '제3 도서관'을 짓고 책을 나눠서 보관한다.
샤딩(Sharding)은 거대한 데이터베이스를 조각(Shard)내어 여러 대의 서버에 나누어 저장하는 기술입니다. 데이터가 PB(페타바이트) 단위로 넘어가면 선택이 아닌 필수가 됩니다.
왜 샤딩을 공부하게 되었나
처음에는 "DB가 느리면 인덱스 걸면 되는 거 아닌가?"라고 생각했습니다. 실제로 인덱스를 추가하니 쿼리가 빨라졌고, 거기서 만족했습니다.
그런데 테이블에 행이 1억 개를 넘어가기 시작하자 상황이 달라졌습니다.
인덱스를 걸어도 느리고, EXPLAIN을 찍어보면 풀 테이블 스캔이 돌고 있었습니다.
Redis 캐시를 앞에 둬도 캐시 미스가 나면 결국 DB로 떨어지고, 그때마다 응답이 3초씩 걸렸습니다.
그때 관련 문서를 읽다가 "샤딩을 검토해볼 시간이다"라는 조언을 접했습니다. 처음엔 "데이터를 쪼갠다고? DB를 여러 대 쓴다고?"가 와닿지 않았지만, 공부하다 보니 왜 마지막 수단인지 이해할 수 있었습니다.
2. 파티셔닝의 종류 - 쪼개는 방법
1) 수직 파티셔닝 (Vertical Partitioning)
- 개념: 테이블을 컬럼(Column) 단위로 쪼갭니다.
- 예시: 사용자 테이블(
User)이 너무 비대할 때.- 자주 쓰는 컬럼:
ID,Name,Email→ 고성능 SSD 서버에. - 가끔 쓰는 컬럼:
Biography,ProfilePicBlob→ 저렴한 HDD 서버에.
- 자주 쓰는 컬럼:
- 비유: 도서관에서 책을 '과학관', '문학관'으로 나누는 것이 아니라, 자주 빌리는 인기 도서만 1층 카운터 앞에 두고, 나머지는 지하 서고에 넣는 것과 비슷합니다.
- 장점: 자주 쓰는 데이터에 대한 I/O가 줄어들어 캐시 효율이 올라갑니다.
- 한계: 결국 한 서버 안에서 행(Row) 자체가 너무 많으면 해결이 안 됩니다.
2) 수평 샤딩 (Horizontal Sharding) — 진짜 "Sharding"
- 개념: 테이블을 행(Row) 단위로 쪼갭니다. 스키마(Schema)는 똑같은데 데이터만 나눕니다.
- 예시:
- Shard A: 1번 ~ 100만 번 유저.
- Shard B: 101만 번 ~ 200만 번 유저.
- Shard C: 201만 번 ~ 300만 번 유저.
- 비유: 도서관 건물을 여러 채 지어서, A
G로 시작하는 저자의 책은 제1도서관, HN은 제2도서관에 보관하는 것입니다.
3. 샤딩 전략 (Sharding Strategies)
"어떤 기준으로 데이터를 나눌 것인가?" — 이것이 Shard Key 선택의 문제이고, 샤딩에서 가장 중요한 결정입니다.
1) Range Sharding (범위 기반)
- 방식:
UserID11000은 A서버, 10012000은 B서버. - 장점: 구현이 쉽습니다. 범위 쿼리(
WHERE date BETWEEN ...)에 유리합니다. - 단점: 데이터 쏠림(Data Skew). 최근 가입자가 폭주하면 '최근 번호대'를 맡은 서버만 불타고, 옛날 번호대 서버는 놉니다. 새벽에는 한산한데 출퇴근 시간에만 특정 샤드가 녹아내리는 상황이 벌어집니다.
2) Hash Sharding (해시 기반)
- 방식:
UserID % 3연산 결과가 0이면 A, 1이면 B, 2면 C. - 장점: 데이터가 아주 균등하게 분산됩니다. 핫스팟 문제가 거의 없습니다.
- 단점: 서버 개수를 늘리거나 줄일 때(Resharding), 해시 함수가 바뀌므로 거의 모든 데이터를 이동해야 합니다. 서버 3대에서 4대로 늘리면
% 3이% 4로 바뀌니까, 기존에 잘 있던 데이터들이 전부 다른 서버로 가야 합니다. - 해결책: Consistent Hashing (아래에서 설명).
3) Geo Sharding (지역 기반)
- 방식: US 유저 → US DB, EU 유저 → EU DB, Asia 유저 → Asia DB.
- 장점: 물리적으로 가까운 서버에서 응답하니 지연 시간(latency)이 줄어듭니다. GDPR 같은 데이터 규정 준수에도 유리합니다.
- 단점: 지역별 트래픽 편차가 크면 특정 샤드에 부하가 몰립니다.
4) Directory Sharding (룩업 테이블)
- 방식: 별도의 "매핑 테이블"을 둡니다. (
UserID 1→Shard A,UserID 55→Shard B). - 장점: 샤드 키를 내 마음대로 바꿀 수 있어 유연합니다.
- 단점: 매핑 테이블 자체가 병목(SPOF)이 될 수 있습니다. 매번 거기를 조회해야 하니까.
4. Consistent Hashing: 재할당 문제의 해결사
해시 기반 샤딩의 재할당 문제를 해결하기 위해 고안된 알고리즘입니다. DynamoDB, Cassandra, Discord 등에서 사용합니다.
개념 - 해시 링(Hash Ring)
- 0 ~ $2^-1$ 까지의 숫자를 원으로 둥글게 맙니다 (Ring).
- 서버를 해싱해서 링 위에 점을 찍습니다. (S1, S2, S3).
- 데이터(Key)도 해싱해서 링 위에 점을 찍습니다. (K1, K2...).
- 데이터는 시계 방향으로 돌다가 가장 먼저 만나는 서버에 저장됩니다.
마법 같은 효과
서버 S4가 새로 추가되어도, S4 근처에 있는 일부 데이터만 S4로 이동하면 됩니다. 다른 서버(S1, S2, S3)에 있던 데이터는 건드릴 필요가 없습니다. 데이터 이동량을 획기적으로 줄여, 무중단 확장이 가능해집니다.
일반 해시 샤딩에서는 서버 추가 시 데이터의 거의 100%를 재배치해야 하지만,
Consistent Hashing에서는 평균적으로 1/N(N은 서버 수)만 이동하면 됩니다.
Virtual Node (가상 노드)
실제로는 물리 서버 하나당 여러 개의 "가상 노드"를 링 위에 배치합니다. 서버가 3대인데 링 위에 점이 3개뿐이면 데이터가 고르게 분산되지 않을 수 있습니다. 가상 노드를 100~200개 찍어두면 훨씬 균등한 분산이 가능합니다.
5. 샤딩의 3대 난제 (The Pain Points)
샤딩은 만병통치약이 아닙니다. "샤딩 도입 = 개발 난이도 10배 상승"입니다.
-
조인 불가 (No Cross-Shard Joins)
- Shard A에 있는
User와 Shard B에 있는Order를 JOIN 하려면? - DB 레벨에서는 불가능합니다. 애플리케이션 레벨에서 각각 조회해서 코드에서 합쳐야 합니다.
- 이게 생각보다 고통스럽습니다. 정렬, 페이지네이션까지 애플리케이션에서 처리해야 하니까요.
- Shard A에 있는
-
분산 트랜잭션 (Distributed Transaction)
- Shard A에서 돈을 빼고, Shard B에 돈을 넣어야 한다면? 트랜잭션(
ACID) 보장이 매우 어렵습니다. - 2-Phase Commit(2PC) 같은 복잡하고 느린 프로토콜을 써야 합니다.
- 현실적으로는 Eventual Consistency(최종 일관성)를 받아들이고, 보상 트랜잭션(Saga Pattern)을 쓰는 경우가 많습니다.
- Shard A에서 돈을 빼고, Shard B에 돈을 넣어야 한다면? 트랜잭션(
-
오토 인크리먼트 불가 (No Auto-Increment)
- 각 샤드에서
ID를 1씩 증가시키면, Shard A에도ID=100, Shard B에도ID=100이 생깁니다. 중복 키 발생! - 해결: Twitter Snowflake 같은 전역 UUID 생성기를 따로 둬야 합니다.
- Snowflake ID는 타임스탬프 + 머신ID + 시퀀스 번호를 조합해서 전역적으로 유일하면서도 시간 순서로 정렬 가능한 ID를 생성합니다.
- 각 샤드에서
6. 실제 Lab - 샤딩 라우터 구현하기
간단한 해시 샤딩 라우터를 만들어보겠습니다.
class ShardingRouter {
constructor(shardMap) {
this.shards = shardMap; // { 0: 'DB_A', 1: 'DB_B', 2: 'DB_C' }
this.totalShards = Object.keys(shardMap).length;
}
getShard(key) {
const hash = this.simpleHash(key);
const shardIndex = hash % this.totalShards;
return this.shards[shardIndex];
}
simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash += str.charCodeAt(i);
}
return hash;
}
}
const router = new ShardingRouter({ 0: 'Server 1', 1: 'Server 2', 2: 'Server 3' });
console.log(router.getShard("user_100")); // Server 2
console.log(router.getShard("user_101")); // Server 1
실제 프로덕션에서는 simpleHash 대신 CRC32나 MurmurHash 같은 검증된 해시 함수를 사용합니다.
단순 아스키코드 합은 충돌이 많아 데이터가 고르게 분산되지 않기 때문입니다.
7. 실제 아키텍처 예시 (Router Pattern)
애플리케이션은 샤딩된 걸 몰라야 좋습니다. 중간에 Router(Proxy)를 둡니다.
- App:
SELECT * FROM User WHERE id=123(그냥 보냄). - Router (예: Mongos, Vitess, ProxySQL):
- "id가 123이네? 이건 해시해보니 Shard-2로 가야 해."
- Shard-2에 쿼리 전송 → 결과 받음 → App에 전달.
- App 개발자는 DB가 1대인 것처럼 코딩할 수 있습니다.
이 패턴의 장점은 샤딩 로직을 애플리케이션 코드에서 분리할 수 있다는 것입니다. 나중에 샤드 수를 늘리거나 재배치해도 애플리케이션 코드를 수정할 필요가 없습니다.
8. Celebrity Problem (연예인 문제)
특정 샤드 키에 트래픽이 몰리는 문제입니다.
- 상황: 인스타그램 DB를
UserID기준으로 샤딩했습니다. - 문제:
Justin Bieber나BTS의 데이터가 있는 샤드 서버는 수백만 명의 조회 요청을 받습니다. (Hotspot). 반면, 제 계정이 있는 샤드는 파리만 날립니다. - 해결 방법들:
- 핫 키를 감지해서 별도 캐시(Redis)에 저장.
- 해당 데이터만 더 잘게 쪼개서 여러 샤드에 복제.
- 읽기 전용 복제본(Read Replica)을 셀러브리티 샤드에만 추가.
9. 샤딩을 도입하기 전에 먼저 할 것들
샤딩은 최후의 수단입니다. 도입 전에 시도해볼 것들:
- 인덱스 최적화:
EXPLAIN찍어보고, 빠진 인덱스 추가. - 쿼리 최적화: N+1 문제 해결, 불필요한 JOIN 제거.
- 캐싱(Redis): 읽기 부하를 캐시로 분산.
- Read Replica: 읽기 전용 복제본으로 읽기 트래픽 분산.
- Scale-Up: CPU/RAM 업그레이드. (돈으로 해결)
- 테이블 파티셔닝: DB 내장 파티셔닝 기능으로 큰 테이블을 논리적으로 분할.
이 모든 것을 시도하고도 감당이 안 될 때 (보통 데이터 수 TB 이상), 그때 샤딩을 도입합니다. 너무 일찍 도입하면 복잡도 지옥에 빠집니다.
10. 용어 사전 (Glossary)
- Sharding: 거대 데이터를 수평으로 분할하여 여러 노드에 분산 저장하는 기술.
- Shard Key (Partition Key): 데이터를 나누는 기준이 되는 컬럼. (가장 중요).
- Horizontal Scaling (Scale-Out): 서버 대수를 늘려 성능을 확장하는 방식.
- Vertical Scaling (Scale-Up): CPU/RAM을 업그레이드하는 방식.
- Hotshard (Hotspot): 데이터가 균등하지 않아 특정 샤드에만 부하가 몰리는 현상.
- Snowflake ID: 분산 환경에서 시간 순서대로 정렬 가능한 유니크 ID 생성 알고리즘 (Twitter 개발).
- Consistent Hashing: 노드 변경 시 데이터 재배치를 최소화하는 해싱 알고리즘.
11. FAQ & Common Questions
- Q: 샤딩은 언제 도입해야 하나요?
- A: 최후의 수단(Last Resort)입니다. 인덱스 최적화, 캐싱(Redis), Read Replica 도입, Scale-Up을 다 해보고도 감당이 안 될 때 도입합니다.
- Q: 샤딩 후 JOIN은 어떻게 하나요?
- A: 되도록 JOIN이 필요 없게 비정규화(Denormalization)를 하거나, 데이터를 중복 저장합니다. 꼭 필요하다면 애플리케이션 단에서 두 번 쿼리해서 합칩니다.
- Q: 클라이언트 사이드 샤딩 vs 서버 사이드 샤딩?
- A: 클라이언트(App 코드)에서 직접 라우팅하면 구조가 복잡해집니다. 보통 중간에 프록시(Router)를 두는 방식을 선호합니다.