DB 데이터 분산 #파티셔닝, 샤딩, 샤드 키 설계와 데이터 재분배

데이터 분산이 필요한 이유

서비스의 데이터가 증가하면 단일 데이터베이스 서버만으로 모든 요청을 처리하기 어려워진다. 데이터가 많아질수록 저장 공간이 부족해질 수 있고, 특정 테이블에 쿼리가 집중되면 읽기·쓰기 성능이 저하된다. 또한 단일 서버의 CPU, 메모리, 디스크 I/O에는 물리적인 한계가 존재한다.

이러한 문제를 해결하는 방법 중 하나가 데이터 분산(Data Distribution) 이다. 데이터를 논리적 또는 물리적으로 여러 영역에 나누어 저장하고, 필요에 따라 여러 데이터베이스 서버에서 처리하도록 구성한다.

데이터 분산과 관련된 주요 개념은 다음과 같다.

  • 파티셔닝(Partitioning): 하나의 테이블이나 데이터 집합을 여러 부분으로 나누는 기법
  • 샤딩(Sharding): 데이터를 여러 독립적인 데이터베이스 서버에 분산하는 기법
  • 샤드 키(Shard Key): 데이터를 어느 샤드에 저장할지 결정하는 기준
  • 데이터 재분배(Rebalancing): 데이터 증가나 서버 변경에 따라 데이터를 다시 배치하는 작업
  • 분산 데이터베이스(Distributed Database): 여러 노드에 데이터를 분산하여 관리하는 데이터베이스 구조

이 개념들은 서로 연결되어 있지만 동일하지 않다. 특히 파티셔닝과 샤딩은 데이터를 나눈다는 공통점이 있지만, 분산 범위와 운영 방식에서 차이가 있다.

파티셔닝(Partitioning)

파티셔닝은 하나의 큰 테이블을 여러 개의 작은 파티션으로 나누어 관리하는 기법이다.

애플리케이션에서는 하나의 테이블처럼 접근할 수 있지만, 데이터베이스 내부에서는 조건에 따라 데이터가 여러 파티션으로 나뉘어 저장된다.

예를 들어 게임 서버의 접속 기록이 수억 건으로 증가했다고 가정하자.

CREATE TABLE game_logs (
    id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    event_type VARCHAR(50) NOT NULL,
    created_at TIMESTAMP NOT NULL
);

모든 접속 기록이 하나의 테이블에 저장되면 데이터가 증가할수록 인덱스 크기와 유지 관리 비용이 커질 수 있다. 이때 created_at을 기준으로 월별 파티셔닝을 적용하면 데이터를 기간별로 분리할 수 있다.

범위 파티셔닝(Range Partitioning)

특정 값의 범위를 기준으로 데이터를 나누는 방식이다.

예를 들어 생성 날짜를 기준으로 다음과 같이 분리할 수 있다.

  • 2026년 1월 데이터
  • 2026년 2월 데이터
  • 2026년 3월 데이터

날짜나 숫자처럼 순서가 존재하는 값에 적합하다.

장점은 특정 기간의 데이터를 조회하거나 오래된 데이터를 삭제할 때 관련 파티션만 대상으로 작업할 수 있다는 것이다.

단점은 최근 데이터에 쓰기 요청이 집중될 수 있다는 것이다. 특히 시간순으로 데이터가 계속 증가하면 최신 파티션에 부하가 집중될 가능성이 있다.

목록 파티셔닝(List Partitioning)

특정 값의 목록을 기준으로 데이터를 분리한다.

예를 들어 지역 코드나 서비스 유형을 기준으로 나눌 수 있다.

PARTITION BY LIST (region_code)

개념적으로 다음과 같이 구분할 수 있다.

  • KR: 한국 사용자 데이터
  • JP: 일본 사용자 데이터
  • US: 미국 사용자 데이터

분류 기준이 명확하고 값의 종류가 제한적일 때 유용하다. 반면 새로운 지역이 추가될 때 파티션 관리가 필요할 수 있다.

해시 파티셔닝(Hash Partitioning)

특정 컬럼에 해시 함수를 적용하여 데이터를 여러 파티션에 분산하는 방식이다.

partition = hash(user_id) % partition_count

예를 들어 파티션이 4개라면 해시 결과에 따라 데이터를 0부터 3까지의 파티션에 배치할 수 있다.

특정 범위에 데이터가 집중되는 현상을 줄이는 데 유리하지만, 특정 날짜의 데이터만 조회하는 것처럼 범위 조건이 중요한 쿼리에는 적합하지 않을 수 있다.

위 코드는 동작 원리를 설명하기 위한 예시다. 실제 SQL 문법과 지원 방식은 데이터베이스 제품에 따라 달라진다.

파티셔닝의 핵심: Partition Pruning

파티셔닝의 성능상 이점은 단순히 데이터를 나누는 것에서 끝나지 않는다. 쿼리 조건을 바탕으로 불필요한 파티션을 제외할 수 있어야 한다.

예를 들어 2026년 3월의 로그만 조회한다고 가정하자.

SELECT *
FROM game_logs
WHERE created_at >= '2026-03-01'
  AND created_at < '2026-04-01';

데이터베이스가 날짜 조건을 활용해 3월 파티션만 조회한다면 전체 데이터를 탐색하는 비용을 줄일 수 있다. 이를 파티션 프루닝(Partition Pruning)이라고 한다.

반대로 쿼리 조건이 파티션 키와 맞지 않거나 데이터베이스가 조건을 활용할 수 없다면 여러 파티션을 탐색해야 할 수 있다.

또한 파티셔닝은 인덱스를 대체하지 않는다. 파티션 내부에서도 적절한 인덱스 설계가 필요하다.

샤딩(Sharding)

샤딩은 데이터를 여러 데이터베이스 서버에 나누어 저장하는 기법이다.

파티셔닝이 하나의 데이터베이스 내부에서 테이블을 나누는 방식으로도 구현될 수 있다면, 샤딩은 일반적으로 여러 서버에 데이터를 분산하는 수평 확장 전략을 의미한다.

예를 들어 사용자 데이터가 3천만 건이라고 가정하자.

단일 데이터베이스에서는 모든 사용자 데이터를 하나의 서버가 관리한다. 샤딩을 적용하면 사용자 데이터를 여러 서버에 분산할 수 있다.

                  Application
                       |
                  Shard Router
                       |
          +------------+------------+
          |            |            |
       Shard A      Shard B      Shard C
       User 0~N     User N~M     User M~Z

위 구조는 범위 샤딩을 단순화한 예시다. 실제 범위 경계와 샤드 구성은 데이터 분포와 운영 환경에 따라 달라진다.

샤딩의 목적은 다음과 같다.

  • 데이터 저장 공간을 여러 서버로 분산한다.
  • 읽기·쓰기 요청을 여러 서버에서 처리하도록 한다.
  • 특정 서버의 CPU, 메모리, 디스크 I/O 부담을 줄인다.
  • 서버를 추가하여 저장 용량과 처리 능력을 확장할 수 있도록 한다.

다만 샤딩이 적용되었다고 해서 성능이 자동으로 향상되는 것은 아니다. 요청이 특정 샤드에 집중되거나 여러 샤드에 걸친 쿼리가 많다면 오히려 운영 복잡도와 응답 지연이 증가할 수 있다.

수평 확장과 수직 확장

데이터베이스의 확장 방식은 크게 수직 확장과 수평 확장으로 나눌 수 있다.

수직 확장(Scale-up) 은 기존 서버의 CPU, 메모리, 디스크 성능을 높이는 방식이다.

장점은 애플리케이션과 데이터 구조를 크게 변경하지 않고도 성능을 개선할 수 있다는 것이다. 하지만 하드웨어의 상한선과 비용 효율성에 한계가 있다.

수평 확장(Scale-out) 은 여러 서버를 추가하여 처리 부하와 데이터를 분산하는 방식이다.

샤딩은 대표적인 수평 확장 전략이다. 다만 데이터 라우팅, 서버 간 통신, 장애 처리, 데이터 재분배 등 추가적인 설계가 필요하다.

따라서 단일 서버의 자원 증설로 충분한 상황이라면 무조건 샤딩부터 도입할 필요는 없다.

샤드 키 설계(Shard Key Design)

샤드 키는 특정 데이터가 어느 샤드에 저장될지 결정하는 기준이다.

샤드 키 설계는 샤딩 아키텍처에서 가장 중요한 결정 중 하나다. 잘못된 키를 선택하면 데이터가 특정 서버에 집중되거나, 대부분의 요청이 여러 샤드를 동시에 조회해야 하는 문제가 발생한다.

대표적인 샤드 키 후보는 다음과 같다.

  • user_id: 사용자 식별자
  • guild_id: 길드 식별자
  • region_id: 지역 식별자
  • created_at: 생성 시간
  • tenant_id: 테넌트 식별자

어떤 키가 적합한지는 데이터 자체보다 실제 쿼리 패턴과 부하 분포에 따라 결정해야 한다.

카디널리티(Cardinality)

카디널리티는 특정 컬럼이 가질 수 있는 고유 값의 개수와 관련된 개념이다.

예를 들어 region_id가 한국, 일본, 미국의 세 가지 값만 가진다면 카디널리티가 낮다. 반면 user_id가 수백만 개의 고유 값을 가진다면 카디널리티가 높다.

샤드 키의 카디널리티가 지나치게 낮으면 데이터를 충분히 분산하기 어렵다.

예를 들어 지역 코드만으로 10개 샤드를 분배한다고 가정하자. 실제 지역이 세 곳뿐이라면 세 곳의 데이터만 배치되거나, 지역별 데이터량 차이에 따라 일부 샤드에 부하가 집중될 수 있다.

반대로 카디널리티가 높다고 반드시 좋은 것은 아니다. 고유 값이 많더라도 특정 키에 요청이 집중된다면 해당 샤드가 병목이 될 수 있다.

데이터 분포와 핫스팟

핫스팟(Hotspot)은 특정 샤드에 데이터나 요청이 집중되는 현상이다.

예를 들어 게임 서버에서 모든 플레이어가 공통으로 참여하는 대규모 이벤트가 있다고 가정하자. 특정 event_id를 샤드 키로 사용하면 이벤트 관련 데이터와 요청이 하나의 샤드에 집중될 수 있다.

이 문제를 줄이려면 다음과 같은 방식을 검토할 수 있다.

  • 사용자 ID 기반으로 분산한다.
  • 해시 기반 샤딩을 적용한다.
  • 요청이 집중되는 데이터를 별도로 분리한다.
  • 필요한 경우 복합 샤드 키를 사용한다.
  • 특정 데이터의 쓰기 부하를 별도의 처리 구조로 분산한다.

단, 샤드 키를 무작정 복잡하게 만들면 데이터 조회와 운영이 어려워진다. 분산 효과와 조회 효율 사이에서 균형을 잡아야 한다.

쿼리 패턴과 로컬리티(Locality)

데이터 로컬리티는 함께 조회하거나 변경하는 데이터가 가까운 위치에 배치되어 있는 정도를 의미한다.

예를 들어 게임에서 사용자 정보, 인벤토리, 퀘스트 진행 상태를 자주 함께 조회한다고 가정하자.

사용자 ID를 기준으로 관련 데이터를 동일한 샤드에 배치하면 해당 요청을 하나의 샤드에서 처리할 가능성이 높아진다.

Shard A
 ├─ User 1001
 ├─ Inventory 1001
 └─ Quest 1001

Shard B
 ├─ User 2001
 ├─ Inventory 2001
 └─ Quest 2001

이 구조에서는 사용자 단위 조회가 간단해진다. 반면 여러 사용자 간 거래나 전체 사용자 통계처럼 여러 샤드의 데이터가 필요한 작업은 복잡해질 수 있다.

샤드 키를 결정할 때는 다음 질문에 답해야 한다.

  • 가장 빈번하게 실행되는 쿼리는 무엇인가?
  • 어떤 데이터가 함께 조회되거나 변경되는가?
  • 읽기와 쓰기 부하는 어떤 분포를 가지는가?
  • 특정 키에 요청이 집중될 가능성이 있는가?
  • 데이터가 증가했을 때 샤드를 추가하기 쉬운가?

이 질문에 대한 답이 샤드 키 설계의 기준이 된다.

샤딩 전략

범위 기반 샤딩(Range-based Sharding)

샤드 키의 값 범위에 따라 데이터를 분리한다.

Shard A: user_id 1 ~ 1,000,000
Shard B: user_id 1,000,001 ~ 2,000,000
Shard C: user_id 2,000,001 ~ 3,000,000

장점은 범위 조회가 비교적 쉽고 특정 데이터가 어느 샤드에 있는지 판단하기 편하다는 것이다.

단점은 값이 증가하는 방향으로 쓰기가 집중되거나 특정 범위에 데이터가 몰리면 핫스팟이 발생할 수 있다는 것이다.

해시 기반 샤딩(Hash-based Sharding)

샤드 키에 해시 함수를 적용해 샤드를 결정한다.

shard_index = hash(user_id) % shard_count

사용자 ID가 고르게 분포한다면 데이터와 요청을 여러 샤드에 분산하기 유리하다.

하지만 샤드 개수가 변경되면 기존 데이터의 매핑이 크게 달라질 수 있다. 예를 들어 샤드가 4개일 때 사용하던 나머지 연산을 5개로 변경하면 상당수 데이터가 새로운 위치로 이동할 수 있다.

이 문제를 완화하기 위한 방법 중 하나가 일관된 해싱(Consistent Hashing)이다.

디렉터리 기반 샤딩(Directory-based Sharding)

별도의 매핑 정보로 데이터가 위치한 샤드를 결정한다.

user_id 1001 -> Shard A
user_id 2001 -> Shard C
user_id 3001 -> Shard B

이 방식은 데이터 위치를 유연하게 변경할 수 있다는 장점이 있다.

다만 샤드 매핑 정보를 관리하는 시스템이 필요하다. 매핑 정보의 가용성, 캐싱, 동기화, 장애 복구도 함께 고려해야 한다.

데이터 재분배(Rebalancing)

데이터 재분배는 기존 샤드에 저장된 데이터를 새로운 샤드 구성에 맞게 이동하거나 재배치하는 작업이다.

초기에는 두 개의 샤드로 충분했더라도 사용자와 데이터가 증가하면 서버를 추가해야 할 수 있다.

Before

Shard A: 50%
Shard B: 50%

        ↓ 데이터 증가 및 서버 추가

After

Shard A: 약 33%
Shard B: 약 33%
Shard C: 약 34%

위 수치는 균등 분배를 가정한 개념적 예시다. 실제 데이터량과 요청량은 반드시 균등해지는 것이 아니다.

단순 해시 방식의 재분배 문제

다음과 같은 샤드 선택 로직을 사용한다고 가정하자.

function getShard(userId: number, shardCount: number): number {
  return userId % shardCount;
}

샤드가 3개일 때와 4개일 때를 비교하면 다음과 같다.

user_id = 10

10 % 3 = 1
10 % 4 = 2

샤드 개수가 변경되면 동일한 사용자 데이터가 다른 샤드에 위치해야 한다. 기존 데이터를 실제로 이동하지 않았다면 애플리케이션은 새로운 위치에서 데이터를 찾지 못할 수 있다.

따라서 샤드 개수만 변경하는 것으로는 충분하지 않다. 데이터 이동과 라우팅 변경을 함께 처리해야 한다.

일관된 해싱(Consistent Hashing)

일관된 해싱은 샤드와 데이터를 해시 공간에 배치하고, 데이터가 해당하는 구간의 샤드에 연결하는 방식이다.

샤드를 추가하거나 제거할 때 일반적인 모듈로 해싱보다 이동해야 하는 데이터의 범위를 줄일 수 있다.

다만 일관된 해싱이 데이터 재분배를 완전히 제거하는 것은 아니다. 가상 노드, 데이터 균형, 샤드별 용량 차이 등을 고려해야 하며, 실제 데이터 이동은 여전히 필요할 수 있다.

가상 노드(Virtual Nodes)

가상 노드는 하나의 물리적 샤드를 해시 공간의 여러 위치에 배치하는 기법이다.

물리적 샤드가 하나의 위치만 차지하면 데이터 분포가 불균등해질 수 있다. 여러 가상 노드를 사용하면 분포를 개선하고 서버 추가·제거 시 이동할 데이터의 범위를 세밀하게 조정할 수 있다.

다만 가상 노드 수를 늘리는 것만으로 실제 데이터량과 요청량이 완벽하게 균등해지는 것은 아니다.

안전한 데이터 재분배 절차

운영 중인 서비스에서 데이터를 이동할 때는 데이터 손실과 불일치를 방지해야 한다.

일반적인 절차는 다음과 같다.

  • 대상 샤드 준비: 새로운 샤드와 스키마, 인덱스를 준비한다.
  • 초기 데이터 복사: 기존 샤드의 데이터를 신규 샤드로 복사한다.
  • 변경 사항 동기화: 복사 중 발생한 쓰기를 추적하여 신규 샤드에 반영한다.
  • 검증: 데이터 건수, 체크섬, 핵심 레코드 및 제약 조건을 확인한다.
  • 라우팅 전환: 읽기·쓰기 요청을 새로운 위치로 전환한다.
  • 안정성 확인: 오류율, 지연 시간, 데이터 불일치를 감시한다.
  • 기존 데이터 정리: 롤백 가능성과 보존 정책을 고려해 이전 데이터를 제거한다.

특히 데이터 복사와 라우팅 전환 사이에서 발생하는 동시 쓰기를 어떻게 처리할 것인지가 중요하다. 이 부분이 설계되지 않으면 복사 도중 변경된 데이터가 누락되거나 중복 반영될 수 있다.

데이터 재분배는 단순한 데이터 복사 작업이 아니다. 데이터 일관성, 서비스 가용성, 장애 복구를 포함하는 운영 작업이다.

분산 데이터베이스(Distributed Database)

분산 데이터베이스는 데이터를 여러 노드에 분산하여 저장하고 처리하는 데이터베이스 구조다.

분산 데이터베이스는 데이터를 분할하는 샤딩뿐 아니라, 복제(Replication), 분산 트랜잭션, 장애 복구, 노드 간 통신과 같은 여러 기술을 조합할 수 있다.

여기서 샤딩과 복제는 구분해야 한다.

  • 샤딩: 서로 다른 데이터의 일부를 여러 노드에 저장한다.
  • 복제: 동일한 데이터의 사본을 여러 노드에 저장한다.

샤딩은 저장 용량과 처리 부하를 분산하는 데 초점을 맞추고, 복제는 읽기 처리 능력이나 가용성, 장애 대응을 개선하는 데 활용된다. 실제 효과는 복제 방식과 시스템 구성에 따라 달라진다.

예를 들어 다음과 같은 구조를 구성할 수 있다.

                   Application
                        |
                    Shard Router
                        |
             +----------+----------+
             |                     |
          Shard A               Shard B
             |                     |
        +----+----+           +----+----+
        |         |           |         |
     Primary   Replica     Primary   Replica

각 샤드는 서로 다른 데이터를 담당하고, 각 샤드 내부에서는 Primary와 Replica를 구성해 복제할 수 있다.

이 구조는 데이터 분산과 복제를 함께 활용하는 예시다. 모든 분산 데이터베이스가 동일한 구성을 사용하는 것은 아니다.

분산 데이터베이스의 주요 과제

분산 트랜잭션

하나의 트랜잭션이 여러 샤드의 데이터를 변경해야 한다면 원자성을 보장하기 어려워진다.

예를 들어 플레이어 A가 플레이어 B에게 게임 재화를 전달하는 작업이 서로 다른 샤드에 위치한다면, A의 잔액 감소와 B의 잔액 증가를 일관되게 처리해야 한다.

이때 분산 트랜잭션 프로토콜을 사용하거나, 비동기 메시지와 보상 작업을 이용하는 구조를 검토할 수 있다. 어떤 방식이 적합한지는 데이터 정합성 요구사항과 장애 상황에서 허용 가능한 동작에 따라 결정된다.

분산 조인

여러 샤드에 분산된 테이블을 조인하면 네트워크 통신과 데이터 이동이 발생할 수 있다. 대량의 데이터를 여러 노드에서 수집해야 한다면 단일 데이터베이스의 조인보다 비용이 커질 수 있다.

이를 줄이기 위해 함께 조회되는 데이터를 같은 샤드에 배치하거나, 읽기 전용 집계 테이블과 별도의 분석 시스템을 활용할 수 있다.

장애 처리

한 노드가 중단되더라도 서비스가 지속될 수 있도록 복제, 장애 감지, 리더 선출, 복구 절차를 설계해야 한다.

그러나 복제본이 존재한다고 해서 모든 장애에서 데이터 손실이 방지되는 것은 아니다. 복제 지연, 동기식·비동기식 복제 방식, 장애 조치 시점에 따라 결과가 달라질 수 있다.

일관성과 가용성

분산 환경에서는 노드 간 통신이 지연되거나 끊길 수 있다. 이때 시스템이 최신 데이터의 일관성을 우선할지, 일부 요청을 계속 처리하는 가용성을 우선할지 정책을 정해야 한다.

모든 요청에서 항상 최신 데이터를 보장하면서 네트워크 분할 상황에서도 모든 노드가 계속 정상 처리하도록 만드는 것은 일반적으로 불가능하다. 따라서 데이터 종류별로 필요한 일관성 수준을 정의해야 한다.

게임 서버에서의 데이터 분산 설계

게임 서버는 사용자 데이터, 인벤토리, 게임 기록, 매치 정보, 랭킹 등 서로 다른 특성을 가진 데이터를 처리한다. 모든 데이터를 동일한 샤드 키로 분산하는 것이 항상 최선은 아니다.

예를 들어 다음과 같이 설계할 수 있다.

데이터고려할 분산 기준설계상 주의점
사용자 정보user_id사용자별 접근 집중도
인벤토리user_id재화 및 아이템 정합성
게임 로그created_at 또는 사용자 ID기간별 조회와 데이터 증가
길드 정보guild_id길드 단위 요청 집중
매치 기록match_id 또는 사용자 ID매치 조회와 사용자별 조회의 균형
랭킹별도 랭킹 저장소 또는 분산 집계전체 정렬과 상위 순위 조회 비용

이 표는 가능한 설계 방향을 제시한 것이다. 실제 샤드 키는 데이터 접근 패턴, 트래픽 규모, 정합성 요구사항을 측정한 뒤 결정해야 한다.

특히 게임 재화와 인벤토리는 단순한 조회 성능보다 데이터 정합성이 중요할 수 있다. 여러 샤드에 걸친 거래가 빈번하다면 샤딩으로 얻는 성능상 이점보다 분산 트랜잭션 비용이 커질 수도 있다.

반면 로그나 분석 데이터처럼 시간 범위별 조회와 대량 저장이 중요한 데이터는 시간 기반 파티셔닝 또는 별도의 분석 저장소가 더 적합할 수 있다.

따라서 데이터 종류별로 분산 전략을 다르게 적용하는 것이 합리적이다.

언제 데이터 분산을 도입해야 하는가?

데이터 분산은 모든 데이터베이스 문제를 해결하는 범용적인 최적화 기법이 아니다. 분산 구조는 성능과 확장성을 개선할 수 있지만, 그 대가로 운영 복잡도와 장애 대응 비용이 증가한다.

도입 전에는 다음 순서로 검토하는 것이 좋다.

  • 인덱스와 쿼리 실행 계획을 점검한다.
  • 불필요한 쿼리, N+1 조회, 과도한 데이터 반환을 줄인다.
  • 커넥션 풀과 서버 자원 사용량을 확인한다.
  • 읽기 부하가 문제라면 읽기 전용 복제본을 검토한다.
  • 테이블이 지나치게 커졌다면 파티셔닝을 검토한다.
  • 단일 서버의 용량과 처리 능력이 한계에 도달했다면 샤딩을 검토한다.
  • 샤딩을 도입한다면 샤드 키, 재분배, 장애 복구 전략을 함께 설계한다.

중요한 것은 현재 병목이 어디에 있는지 확인하는 것이다. 단일 서버에서 발생하는 느린 쿼리 문제를 해결하지 않은 채 샤딩부터 도입하면 복잡한 분산 구조 위에서 동일한 비효율을 유지하게 될 수 있다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다