← Posts

Database · Scaling · Partitioning

파티셔닝과 샤딩 - 나누면 무엇을 잃는가

둘의 경계는 '누가 조각을 아는가'다. 그리고 샤딩하는 순간 지금까지의 트랜잭션 이야기가 대부분 무효가 된다.

목차
  1. 경계는 “누가 조각을 아는가”
  2. 파티셔닝은 조회를 빠르게 하려는 게 아니다
  3. 그럼 왜 나누나
  4. 나누는 축
  5. 샤딩 - 여기서부터 다른 세계다
  6. 잃는 것 하나: ACID
  7. 잃는 것 둘: 조인
  8. 샤드 키 - 되돌리기 가장 어려운 결정
  9. 그래서 순서
  10. 정리

주문 테이블이 10억 건이다. 인덱스가 메모리에 안 들어간 지 오래고, 2년 지난 데이터를 지우려고 DELETE를 돌렸더니 세 시간째 안 끝난다.

한 덩어리로는 감당이 안 되니 나눠야 한다. 그런데 나누는 방법이 둘이고, 둘은 이름만 비슷하지 완전히 다른 결정이다.

경계는 “누가 조각을 아는가”

diagramdiagram

파티셔닝은 DB가 감춰준다. 테이블은 여전히 하나로 보이고, SELECT도 그대로다. 안에서 조각으로 나눠 저장할 뿐이다.

샤딩은 못 감춘다. DB가 여러 대라 서로를 모른다. 어디에 있는지는 애플리케이션(또는 앞단의 프록시)이 알아야 한다.

이 차이가 전부다. 하나는 저장 방식의 변경이고, 다른 하나는 아키텍처의 변경이다.

파티셔닝은 조회를 빠르게 하려는 게 아니다

가장 흔한 오해다. 나누면 각 조각이 작아지니 빨라질 것 같다.

조건이 맞을 때만 그렇다.

partition-pruningpartition-pruning

날짜로 나눈 테이블에 WHERE 주문일 = '2026-07-15'로 물으면, DB는 7월 조각 하나만 열면 된다. 나머지는 열어볼 필요조차 없다. 이걸 파티션 프루닝이라고 한다.

그런데 WHERE 회원ID = 'U7'로 물으면? 회원ID는 나눈 축이 아니다. U7의 주문이 어느 조각에 있는지 알 수 없으니 전부 열어야 한다. 안 나눴을 때보다 오히려 느리다. 조각마다 따로 뒤져야 하니까.

주의

나눈 축이 조회 조건에 없으면 파티셔닝은 손해다. 그리고 이건 실행 계획에 그대로 나온다. 몇 개 조각을 읽었는지가 찍힌다. 나누기 전에 어떤 조건으로 조회하는지부터 세어봐야 한다. 축은 조회가 정하는 것이지 데이터가 정하는 게 아니다.

그럼 왜 나누나

관리 때문이다. 이게 파티셔닝의 진짜 효용이다.

맨 앞의 그 DELETE를 생각해보자. 2년 지난 주문 3억 건을 지운다. DB는 3억 줄을 하나씩 찾아 지우고, 인덱스에서도 빼고, MVCC라면 옛 버전까지 쌓아둔다. 세 시간이 걸리고 그동안 테이블이 부푼다.

날짜로 파티션했다면? 그 조각을 통째로 버린다. 파일을 지우는 것과 같아서 1초면 끝난다. 지울 행을 찾을 필요도, 인덱스를 고칠 필요도 없다.

주기적으로 오래된 데이터를 버리는 시스템이라면 이것만으로 파티셔닝의 값어치가 있다.

나누는 축

어떻게언제
범위날짜·숫자 구간으로가장 흔하다. 로그·주문처럼 시간이 축인 데이터
목록값을 나열해서 (지역, 상태)값의 종류가 적고 잘 안 변할 때
해시키를 해싱해 고르게고르게 흩는 게 목적일 때. 범위 조회는 못 한다
참고

파티션 테이블에는 함정이 하나 있다. 전역 유니크 제약을 걸기 어렵다. 대부분의 DB에서 유니크 제약에 파티션 키가 포함돼야 한다. 각 조각이 자기 인덱스만 갖고 있어서, 다른 조각에 같은 값이 있는지 알 수가 없기 때문이다. 키 설계에서 emailUNIQUE를 걸라고 했는데, 파티션 테이블이면 그게 안 될 수 있다.

샤딩 - 여기서부터 다른 세계다

한 대로 감당이 안 될 때 여러 대로 흩는다. 용량이든 쓰기 처리량이든 한 대의 한계에 닿았을 때다.

그리고 여기서 지금까지의 이야기 대부분이 무효가 된다.

잃는 것 하나: ACID

트랜잭션 글에서 트랜잭션이 뭘 약속하는지 봤다. 격리 수준도, 도, MVCC도 다뤘다.

그게 전부 한 DB 안에서의 약속이었다.

A가 1번 샤드에 있고 B가 2번 샤드에 있는데 A에서 B로 이체한다면? 두 DB는 서로를 모른다. 각자 자기 트랜잭션만 안다. 한쪽은 커밋되고 한쪽이 실패하면 원자성이 깨진다. 트랜잭션 글에 나왔던 그 “만 원이 증발한” 상황이 그대로 돌아온다. 트랜잭션으로 막았다고 생각했던 바로 그 문제다.

분산 트랜잭션(2단계 커밋)이라는 게 있긴 하다. 여러 DB에게 “준비됐냐”를 물어보고 다 됐다고 하면 커밋하는 방식이다. 그런데 느리고, 조율자가 죽으면 참가자들이 락을 쥔 채 멈춘다. 한 대 안에서 공짜로 받던 걸 아주 비싸게 사는 셈이다.

그래서 실무에서는 대개 분산 트랜잭션을 피하도록 설계한다. “같이 바뀌어야 하는 것들은 같은 샤드에 두자”는 원칙이 나온다. 그게 안 되면 트랜잭션을 포기하고 다른 방법(보상 트랜잭션 같은)으로 푼다.

잃는 것 둘: 조인

조인 알고리즘에서 본 셋은 한 DB 안에서 두 테이블을 붙이는 이야기였다. 옵티마이저가 통계를 보고 알아서 골라줬다.

샤드를 넘으면 그런 게 없다. 회원은 1번 샤드에, 주문은 2번 샤드에 있으면 DB가 붙여줄 방법이 없다. 애플리케이션이 양쪽에서 각각 읽어와 메모리에서 붙여야 한다. 옵티마이저도, 해시 조인도, 통계도 없다.

그래서 샤드 키를 고를 때 같이 조회되는 것들이 같은 샤드에 모이게 해야 한다. 회원과 그 회원의 주문을 user_id로 같이 샤딩하면 한 샤드 안에서 조인된다.

샤드 키 - 되돌리기 가장 어려운 결정

키 설계에서 기본키는 안 바뀌어야 한다고 했다. 샤드 키는 그보다 더하다. 바꾸려면 데이터를 전부 옮겨야 한다.

고를 때 세 가지를 본다.

고르게 흩어지나. 한쪽에 쏠리면(핫스팟) 나눈 의미가 없다. 시간을 샤드 키로 쓰면 최신 데이터가 전부 한 샤드로 몰린다. 나머지 샤드는 놀고 한 대만 죽어난다.

대부분의 조회에 그 키가 들어가나. 안 들어가면 모든 샤드에 물어봐야 한다. 파티션 프루닝이 실패한 것과 같은데, 이번엔 네트워크를 타고 나간다.

나중에 늘릴 수 있나. 샤드를 3대에서 4대로 늘릴 때 user_id % 3이었다면 거의 모든 데이터가 자리를 옮긴다. 이걸 줄이려고 일관 해싱 같은 걸 쓴다.

그래서 순서

주의

샤딩은 마지막 수단이다. 위에서 본 것들을 다 포기하고 시작하는 것이다. 그 전에 인덱스·쿼리·통계를 다 손봤는지, 읽기가 문제면 복제로 풀리는지, 오래된 데이터를 파티셔닝으로 덜어낼 수 있는지부터 본다. 샤딩은 되돌리기가 가장 어렵다.

  • 파티셔닝은 저장 방식만 바꾼다. 애플리케이션이 몰라도 된다. 되돌리기도 상대적으로 쉽다.
  • 샤딩은 아키텍처를 바꾼다. ACID도, 조인도, 옵티마이저도 잃는다.

정리

  • 둘의 경계는 누가 조각을 아는가다. 파티셔닝은 DB가 감춰주고, 샤딩은 못 감춘다.
  • 파티셔닝은 조회를 빠르게 하려는 게 아니다. 나눈 축이 조회 조건에 없으면 오히려 느리다. 진짜 효용은 오래된 조각을 통째로 버리는 것이다.
  • 샤딩하면 지금까지의 트랜잭션 이야기가 대부분 무효가 된다. ACID는 한 DB 안의 약속이었다.
  • 샤드를 넘는 조인은 DB가 안 해준다. 같이 조회되는 건 같은 샤드에.
  • 샤드 키는 되돌리기 가장 어려운 결정이다. 고르게 흩어지고, 조회에 들어가고, 늘릴 수 있어야 한다.
참고

다음 글은 복제다. 쓰기가 아니라 읽기가 문제라면 나누기 전에 복제로 풀 수 있다. 대신 “방금 쓴 걸 바로 못 읽는” 새로운 문제가 생긴다.

관련 글