백엔드 데이터 인프라 보강편. DB를 샤딩하는 순간 auto_increment가 깨진다 — 여러 서버가 동시에 겹치지 않는 고유 ID를 만드는 법. UUID의 한계와 Snowflake 64비트 설계(타임스탬프+워커+시퀀스), 시계 역행 같은 운영 함정까지 정리한 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈의 보강편이에요. 68편 Redis Cluster와 133편 일관된 해싱에서 데이터를 여러 서버로 샤딩하는 걸 다뤘죠. 그런데 샤딩을 시작하는 순간 당연하게 쓰던 게 하나 깨져요 — DB의 auto_increment. 여러 샤드가 독립적으로 1·2·3을 발급하면 ID가 겹쳐 버리거든요. 분산 환경에서 겹치지 않는 분산 ID를 만드는 법, 이 글에서 풀어 가요.
auto_increment가 샤딩에서 깨지는 이유
단일 DB에선 auto_increment가 완벽해요. DB가 1, 2, 3… 순서대로, 겹치지 않게, 자동으로 발급하니까요. 그래서 평소엔 ID 생성을 고민할 일이 없어요.
그런데 133편처럼 데이터를 샤드 A·B·C로 나누면 문제가 생겨요. 각 샤드가 자기만의 auto_increment를 돌리거든요. 샤드 A도 1번을 발급하고, 샤드 B도 1번을 발급해요. 같은 ID가 여러 샤드에 존재하게 되죠. 나중에 데이터를 합치거나 옮길 때 ID가 충돌하면서 무너져요.
그래서 분산 환경에선 "어느 서버가 만들든 절대 겹치지 않는 ID" 를 따로 설계해야 해요. 후보가 둘 있어요 — UUID와 Snowflake.
분산 ID 후보 1 — UUID
UUID는 128비트(16바이트) 랜덤 값이에요. 550e8400-e29b-41d4-a716-446655440000 같은 모양이죠. 충돌 확률이 사실상 0이라, 어느 서버가 만들든 겹치지 않아요. 중앙 조율 없이 각자 만들 수 있다는 게 큰 장점이에요.
그런데 여기서 함정이 하나 있어요. UUID는 랜덤이라 시간 순서가 없어요. 이게 왜 문제냐면, 대부분의 DB는 기본 키로 정렬된 인덱스(B-tree)를 만드는데, 랜덤 값이 들어오면 인덱스의 아무 데나 끼어들어요. 그러면 인덱스 페이지가 이리저리 쪼개지며(page split) 인덱스 단편화가 심해지고 쓰기 성능이 떨어져요. 게다가 16바이트라 bigint(8바이트)보다 두 배 무겁고, 인덱스·외래키마다 그 무게가 누적돼요.
정렬 문제를 줄인 UUID v7(시간 기반) 같은 변형도 나와 있어요. 하지만 크기(16바이트)는 그대로라, 더 가볍고 시간순인 분산 ID가 필요하면 다음 방식을 봐요.
분산 ID 후보 2 — Snowflake (64비트 시간 정렬)
Snowflake는 ID를 64비트(8바이트) 정수 하나로 만들되, 그 비트를 의미 있게 쪼개 쓰는 방식이에요. 대략 이렇게 나눠요.
[ 1비트 부호 ] [ 41비트 타임스탬프(ms) ] [ 10비트 워커 ID ] [ 12비트 시퀀스 ]
각 조각의 역할을 풀면 이래요.
- 타임스탬프(41비트) — 밀리초 단위 시각. 맨 앞에 있어서 ID가 시간 순으로 정렬돼요. 약 69년치를 담아요.
- 워커 ID(10비트) — 어느 서버·프로세스가 만들었는지. 최대 1024대가 겹치지 않게 나눠 가져요.
- 시퀀스(12비트) — 같은 밀리초 안에서 한 서버가 여러 개를 만들 때의 일련번호. 1ms에 4096개까지.
이 셋을 합치면 중앙 조율 없이도, 어느 서버가 언제 만들든 겹치지 않는 ID가 나와요. 게다가 맨 앞이 타임스탬프라 시간순으로 정렬 되니 UUID의 인덱스 단편화 문제가 없고, 8바이트라 가볍죠. UUID의 "겹치지 않음"과 auto_increment의 "정렬됨"을 둘 다 가져온 셈이에요.
운영 함정 — 시계와 워커 ID
Snowflake는 시각에 기대다 보니, 시각이 어긋나면 흔들려요.
시계 역행(clock skew) — 서버 시각이 NTP 보정으로 과거로 되돌아가면 이미 발급한 ID보다 작은 ID가 나와 충돌할 수 있어요. 그래서 구현체는 "마지막 발급 시각보다 시계가 뒤로 갔으면 잠깐 대기하거나 예외" 로 막아요. 운영에선 NTP를 안정적으로 두는 게 중요해요.
워커 ID 중복 — 두 서버가 같은 워커 ID를 쓰면 같은 밀리초에 같은 ID를 만들 수 있어요. 그래서 워커 ID를 어떻게 유일하게 배정 하느냐가 운영 포인트예요(설정·ZooKeeper·DB 등록 등).
비트 고갈 — 1ms에 시퀀스(4096개)를 넘게 발급하면 다음 ms까지 기다려야 해요. 보통은 넉넉하지만, 초고빈도 발급이면 비트 배분을 조정하기도 해요.
시험·면접 직전 압축 노트 — 분산 ID
- 샤딩하면 각 샤드의
auto_increment가 독립 발급 → ID 충돌 - 분산 ID 후보 = UUID vs Snowflake
- UUID = 128비트 랜덤, 충돌 0·중앙 조율 불필요 / 단점 = 시간순 아님(인덱스 단편화) + 16바이트로 무거움
- UUID v7 = 시간 기반 변형(정렬 개선)이나 크기는 16바이트 그대로
- Snowflake = 64비트(8바이트) = 부호 1 + 타임스탬프 41 + 워커 10 + 시퀀스 12
- 타임스탬프가 맨 앞 → 시간순 정렬(인덱스 친화) + 8바이트로 가벼움
- 워커 ID 10비트 = 최대 1024대 / 시퀀스 12비트 = 1ms에 4096개
- Snowflake = UUID의 "겹치지 않음" + auto_increment의 "정렬됨"을 둘 다
- 함정 1 = 시계 역행(NTP가 과거로) → 충돌 위험, 대기/예외로 방어
- 함정 2 = 워커 ID 중복 → 유일 배정 체계 필요
- 함정 3 = 시퀀스 비트 고갈(1ms 4096개 초과) → 다음 ms 대기
공식 참고: 분산 ID 설계는 Redis Partitioning 같은 샤딩 맥락과 함께 보면 "왜 필요한가"가 또렷해져요.
시리즈 다른 편
같이 읽으면 좋은 글: