데이터가 많아지면 단순히 서버를 업그레이드하는 것만으로는 한계가 온다.
크게 두 가지 방향으로 접근할 수 있다.
비슷해 보이지만 해결하려는 문제의 레벨이 다르다. 하나씩 살펴보자.
같은 서버 안에서 하나의 큰 테이블을 논리적으로 분할하는 방식이다. 쿼리 성능 향상과 데이터 관리 편의가 목적이다.
파티셔닝의 핵심 이점이다. 쿼리 조건에 맞는 파티션만 스캔하고 나머지는 아예 건드리지 않는다.
인덱스도 빠른 탐색을 제공하지만, 인덱스는 "어디 있는지 알려주는 포인터"일 뿐이다. 위치를 찾은 뒤에도 실제 데이터는 디스크에서 읽어와야 한다. 데이터가 테이블 전체에 흩어져 있으면 디스크를 여기저기 왔다갔다 하는 I/O 비용이 쌓인다.
파티셔닝은 데이터 자체를 조건 기준으로 묶어두기 때문에, 해당 파티션 외의 디스크 I/O를 원천 차단한다.
날짜나 숫자 범위 기준으로 분할한다.
2024-01 파티션 → 1월 주문 데이터
2024-02 파티션 → 2월 주문 데이터
2024-03 파티션 → 3월 주문 데이터
WHERE order_date BETWEEN '2024-01-01' AND '2024-01-31' 조회가 들어오면 1월 파티션만 스캔한다. 나머지는 건드리지 않는다.
오래된 데이터를 파티션 단위로 DROP할 수 있어 유지보수도 편하다. DELETE로 수억 건을 지우는 것보다 파티션 하나를 통째로 날리는 게 훨씬 빠르다.
hash(key) % n으로 데이터를 균등하게 분산한다. 특정 파티션에 쏠리는 현상 없이 고르게 나뉜다.
단, hash 값은 연속적이지 않기 때문에 범위 쿼리(WHERE date BETWEEN ...)에는 partition pruning이 적용되지 않는다. 범위 조건이 자주 쓰이는 데이터라면 Range 파티셔닝이 더 적합하다.
특정 값 목록 기준으로 분할한다.
서울 파티션 → region = '서울'
부산 파티션 → region = '부산'
대구 파티션 → region = '대구'
지역이나 상태값(ACTIVE/INACTIVE/DELETED)처럼 값이 정해진 카테고리 분할에 적합하다. 해당 값으로 조회하면 그 파티션만 스캔한다.
단점은 새로운 값이 생기면 파티션을 따로 추가해야 한다는 것이다. 사전에 가능한 값의 범위를 알고 있을 때 유용하다.
데이터를 물리적으로 다른 서버에 분산하는 방식이다. 파티셔닝이 한 서버 안의 최적화라면, 샤딩은 서버 자체를 늘려 수평 확장(scale-out)하는 것이다.
샤딩 설계에서 가장 중요한 선택은 Shard Key다. 어느 서버에 저장할지 결정하는 기준 컬럼으로, 자주 쓰는 조회 조건이 shard key가 돼야 불필요한 전체 샤드 쿼리를 피할 수 있다.
범위 기준으로 서버를 나눈다.
user_id 1 ~ 100만 → 서버 A
user_id 100만 ~ 200만 → 서버 B
user_id 200만 ~ 300만 → 서버 C
구조가 단순하지만 핫스팟 문제가 생긴다. 신규 가입자 대부분이 최근 user_id에 몰리면, 서버 C에만 요청이 집중된다. 나머지 서버는 놀고 있는데 하나만 과부하 상태가 되는 것이다.
데이터 양이 아니라 접근 빈도가 폭발하는 문제다. BTS가 사진을 올리면 수백만 명이 동시에 그 계정을 조회한다. 해당 user_id가 있는 샤드 하나만 읽기 요청이 집중되어 과부하 상태가 된다.
핫스팟이 데이터 분배 불균등 문제라면, 유명인사 문제는 특정 키에 대한 접근 빈도 불균등 문제다. 샤딩 방식과 무관하게 어디서든 발생할 수 있다. 해결책은 캐싱이나 해당 계정 전용 서버 분리 같은 방식이다.
hash(shard_key) % 서버수로 균등 분산한다. 핫스팟 없이 데이터를 고르게 나눌 수 있다.
문제는 서버 수(n)가 바뀔 때 생긴다. 서버 10대에서 11대로 늘리면 % 10이 % 11로 바뀌어 거의 모든 데이터의 담당 서버가 달라진다. 90% 넘는 데이터를 다시 옮겨야 한다는 뜻이다.
Hash 샤딩의 재할당 문제를 해결한 방식이다.
해시값을 원형(ring)으로 배치하고 서버를 그 위에 올린다.
데이터는 자기 해시값에서 시계방향으로 가장 가까운 서버에 저장된다.
[서버A] ---- [서버B] ---- [서버C] ---- (ring)
서버B가 추가되면 → 서버B 앞 구간만 서버B로 이동
서버C가 죽으면 → 서버C 구간만 서버A로 이동
서버가 추가되거나 죽어도 인접 구간만 재할당되기 때문에, 전체 데이터의 1/n만 이동한다. Hash 샤딩이 서버 변경마다 전체를 흔드는 것과 대조된다.
어느 데이터가 어느 서버에 있는지 lookup table을 관리하는 라우팅 서버를 별도로 둔다. 유연하게 서버를 관리할 수 있지만, 라우팅 서버가 단일 장애점(SPOF)이 될 수 있다.
shard key가 아닌 컬럼으로 조회할 때 발생한다.
shard key = user_id
WHERE created_at = '2024-01-01' 조회 → 어느 서버에 있는지 알 수 없음
→ 모든 샤드에 동시에 쿼리 → 결과를 모아서 합침
샤드가 10개면 쿼리도 10개, 100개면 100개가 날아간다. shard key 설계가 중요한 이유다.
샤드를 넘나드는 JOIN이나 트랜잭션은 비용이 크다.
서버 A와 서버 B에 걸친 트랜잭션을 처리하려면 두 서버가 동시에 합의해야 하는 2PC(2 Phase Commit)가 필요하다. 샤드가 늘수록 조율에 참여하는 서버도 늘어 비용이 기하급수적으로 커진다. NoSQL이 분산 환경에서 ACID를 포기한 이유와 같은 맥락이다.
이를 피하기 위해 반정규화(denormalization)를 선택하기도 한다. 참조 대신 실제 값을 같은 샤드에 복사해두면 cross-shard JOIN 없이 한 번에 조회할 수 있다. 대신 원본 데이터가 바뀌면 복사본을 모두 업데이트해야 한다. 조회 성능과 쓰기 비용, 둘 다 잡을 수 없다. 하나를 얻으면 하나를 포기해야 한다.
| 파티셔닝 | 샤딩 | |
|---|---|---|
| 위치 | 같은 서버 내 | 다른 서버 |
| 목적 | 쿼리 성능, 관리 편의 | 수평 확장 |
| 분할 단위 | 논리적 | 물리적 |
파티셔닝은 한 서버 안에서 더 잘 쓰기 위한 최적화다. 샤딩은 서버 한 대의 한계를 넘기 위한 확장이다. 데이터 규모와 조회 패턴에 따라 둘을 함께 쓰기도 한다.