분산 시스템에서 UUID 대신 Custom ID를 선택했던 이유
분산 시스템에서의 ID 고민에 대한 글이다. 분산 시스템은 보통 DB PK로 사용되는 ID 생성을 어떻게 할지가 중요한데, 관심사와 책임 분리 관점에서 DB 는 인덱싱 + Data Maniqulation 책임만 당담해야한다고 생각하는 내 입장에서는 ID의 생성은 DB에 맡기는 것이 아니라 데이터 조작하는 애플리케이션 레벨에서 책임지면 좋겠다 생각했다.
출발점은 단순했다.
- 여러 인스턴스에서 동시에 ID를 생성해야 하고
- 중앙 집중식 번호 발급기는 피하고 싶고
- DB 인덱싱 성능도 놓치고 싶지 않았다
이 상황에서 가장 먼저 비교한 건 UUID vs Custom ID였다.
1. 처음 문제: 분산 환경에서 “안전하고 빠른 ID”가 필요했다
분산 환경에서 ID 생성기는 보통 세 가지 요구를 동시에 만족해야 한다.
- 유일성: 인스턴스가 여러 대여도 충돌이 없어야 한다
- 성능: 생성이 빠르고 병목이 없어야 한다
- 저장/조회 효율: DB 인덱스와 조회 패턴에 잘 맞아야 한다
초기에는 UUID(v4)를 먼저 고려했다. 표준이고, 구현도 쉽기 때문이다.
하지만 실제 서비스 관점에서 보면 고민 포인트가 남았다.
- PK가 랜덤 분포를 가지면 B-Tree locality가 약해질 수 있음
- 문자열(36자) 형태를 그대로 쓰면 저장/인덱스 비용이 커질 수 있음
- 시간 순 정렬이 필요한 조회에서 별도 컬럼 의존도가 높아짐
그래서 “표준이라 무조건 UUID”가 아니라, 우리 워크로드에 맞는 키 구조를 직접 설계할 필요가 생겼다.
2. 선택지 비교: UUID vs Custom ID
UUID(v4)
장점:
- 표준 포맷이라 연동이 쉽다
- 충돌 확률이 매우 낮다
- 구현 난이도가 낮다
아쉬운 점:
- 랜덤 분포 특성상 인덱스 삽입 locality가 떨어질 수 있다
- 문자열 저장 시 스토리지/인덱스 부담이 커질 수 있다
- 생성 시각/순서 정보를 ID 자체에서 얻기 어렵다
Custom ID (시간 기반 Long)
장점:
- 증가형 성격을 가져 인덱스 locality에 유리
- 8바이트 정수라 저장 효율이 좋다
- ID에서 생성 시각을 유추할 수 있다
아쉬운 점:
- 표준이 아니므로 운영 규약이 필요
- node/sequence/clock 같은 설계 책임이 생긴다
- 구현 품질이 곧 안정성으로 직결된다
결론적으로, 우리 케이스는 범용 표준성보다 인덱싱/쓰기 패턴 최적화 이득이 더 컸다. 그래서 Custom ID를 선택했다.
3. 최종 선택한 구조
최종적으로 채택한 구조는 64비트 Long 기반(실질 63비트 사용)이다.
1
[ timestamp(43) | nodeId(16) | sequence(4) ]
조합식은 다음과 같다.
1
uuid = (timestamp << 20) | (nodeId << 4) | sequence
의도는 명확했다.
- timestamp: 대략적인 시간 순서를 보장
- nodeId: 분산 인스턴스 구분
- sequence: 같은 ms 내 다건 발급 구분
4. 설계하면서 명확해진 것들
(1) 수명은 timestamp 비트가 결정한다
2^43 - 1 ms까지 표현 가능- epoch 기준 약 278.7년 (대략 2248년대)
즉, “언제까지 쓸 수 있나”는 timestamp 비트 문제다.
(2) 처리량은 sequence 비트가 결정한다
- sequence 4비트 => 같은 ms 내 최대 16단계
- 한계 도달 시 다음 ms로 넘겨 생성
즉, “한 순간에 얼마나 찍을 수 있나”는 sequence 비트 문제다.
(3) 분산 충돌 리스크는 nodeId 운영이 핵심이다
비트만 나눴다고 끝이 아니라, nodeId를 어떻게 할당/보장할지 정책이 실제 안정성을 좌우한다.
(4) 처리량 한계와 충돌 확률은 분리해서 봐야 한다
이 부분은 실제로 자주 오해가 생긴다.
- 처리량 한계: sequence 비트가 만든다
- 충돌 확률: nodeId 할당 품질 + clock 상태 + 동시성 제어가 만든다
처리량 계산 (단일 인스턴스 기준)
현재 구조는 sequence 4비트(0~15)이므로 같은 ms에서 최대 16개를 표현할 수 있다.
- 이론치:
16 IDs/ms ≈ 16,000 IDs/s - 코드에서
@Synchronized+ 시퀀스 한계 시 1ms 대기(sleep) 로직이 있으니, 실제 체감은 JVM 스케줄링/워크로드에 따라 더 낮아질 수 있다.
즉, 이 생성기는 “무제한 고속”이 아니라, “예측 가능한 상한을 가진 생성기”라고 보는 게 정확하다.
충돌은 언제 생기나?
이 설계에서 같은 ID가 나오려면 아래 3개가 동시에 같아야 한다.
- timestamp(ms)
- nodeId
- sequence
@Synchronized가 있는 단일 인스턴스 내부에서는 같은 ms에 sequence가 중복되지 않도록 제어한다. 그래서 현실적인 충돌 시나리오는 대부분 인스턴스 간 nodeId 중복에서 시작된다.
예를 들어,
- 서버 A와 서버 B가 우연히 같은 nodeId를 가져버리고
- 같은 ms에
- 동일 sequence 값을 생성하면
ID 충돌이 발생한다.
그래서 확률 논의의 핵심은 UUID가 아니라 nodeId 운영이다
랜덤 fallback만 믿는 경우를 단순화해서 보면, nodeId 공간이 65,536개로 넓어 보여도 인스턴스 수가 늘수록 생각보다 빠르게 중복 가능성이 올라간다.
대략적인 감각치:
- 100대 수준: 중복 확률이 꽤 낮음
- 300대 수준: 중복 위험이 눈에 띄게 증가
- 1,000대 수준: 거의 중복이 발생한다고 보는 게 안전
(정확 값은 1 - exp(-n(n-1)/(2m)), m=65536로 계산 가능)
결론은 단순하다.
- 이 생성기 자체는 나쁘지 않다.
- 하지만 분산 환경에서 충돌 안전성은 “비트 수”보다 nodeId 할당 정책이 좌우한다.
5. DB 인덱싱 관점에서 기대한 효과
시간 기반 Long ID를 선택한 이유 중 하나는 인덱스였다.
기대 효과:
- PK/보조 인덱스 크기 절감 (키 폭 감소)
- 삽입 locality 개선 (append-like 패턴)
- 범위 조회/정렬에서 시간축 활용 용이
물론 트레이드오프도 있다.
- 매우 높은 동시성에서는 hotspot 페이지 경합 가능
- 시간 정보가 노출되는 ID가 부담인 도메인에는 부적합
그래서 “무조건 빠르다”가 아니라, 서비스의 쓰기 패턴과 조회 패턴에 맞는 선택으로 보는 게 맞다.
6. 지금 돌아보면
이 결정의 핵심은 기술 선호가 아니었다.
- UUID가 나빠서도 아니고
- Custom ID가 멋져서도 아니었다.
분산 시스템에서 요구한 조건(유일성/성능/인덱싱)을 놓고 우리 트래픽과 운영 방식에 맞게 선택한 결과였다.
ID 생성기는 작은 유틸처럼 보이지만, 운영 관점에서는 시스템 성격을 결정하는 기반 컴포넌트다.
그래서 중요한 건 하나다.
“어떤 ID가 더 유명하냐”보다 “우리 시스템에서 어떤 실패를 줄여주느냐”
이 기준으로 보면, 당시에는 Custom ID가 더 설득력 있는 선택이었다.