아이덴티티 위기: 기본 키로서의 시퀀스 vs UUID
게시일: 2025년 11월 22일 | 원문 작성일: 2021년 6월 29일 | 저자: Brandur Leach | 원문 보기
핵심 요약
- 정수 시퀀스(bigserial)와 UUID 사이의 논쟁에는 명확한 승자가 없어요
- 각 접근 방식은 나름의 트레이드오프가 있어요 - 성능 vs 분산 생성, 인간 친화성 vs 정보 은닉
- ULID나 순차적 UUID 같은 절충안이 양쪽의 장점을 제공해요
- Stripe의 접두사 ID 패턴은 실용적인 타협안을 보여줘요 - 내부적으로 bigserial, 외부적으로 타입 안전성
- 결정은 독단보다는 성능 측정과 상황 판단이 중요해요
오래된 논쟁
데이터베이스 기본 키를 어떻게 설계해야 할까요? 이건 엔지니어들 사이에서 수년 동안 논쟁이 되어온 질문이에요. 두 진영이 뚜렷하죠.
Bigserial 진영은 단순함을 선호해요. 64비트 정수는 컴팩트하고, 빠르고, 인간이 읽기 쉽죠. __CODE_UNDER_0__처럼 디버깅할 때 한눈에 파악할 수 있어요.
UUID 진영은 분산 시스템을 지향해요. 128비트 범용 고유 식별자는 중앙 조정 없이 어디서든 생성할 수 있고, 정보를 덜 노출하죠.
Stripe에서 API를 설계한 Brandur Leach는 이 논쟁을 영리하게 접근해요. 어느 쪽도 압도적인 승자가 아니라는 걸 인정하면서요. 그보다는 각 접근 방식의 트레이드오프를 체계적으로 분석하고, 중간 지점 솔루션을 제안해요.
Bigserial의 장점
1. 컴팩트한 저장
Bigserial은 64비트를 사용하는 반면, UUID는 128비트예요. 이건 스토리지와 인덱스 크기가 절반이라는 뜻이죠. 수십억 개의 행을 다루는 테이블에서는 이 차이가 실제로 중요해져요.
2. 인간 친화적
개발자 경험 측면에서 bigserial은 압도적으로 나아요.
- 읽고 기억하기 쉬워요: __CODE_UNDER_1__ vs __CODE_UNDER_2__
- 디버깅 시 빠르게 식별 가능해요
- 로그에서 시각적으로 스캔하기 쉬워요
- URL에서 깔끔하게 보여요
3. 뛰어난 성능
순차적 삽입이 데이터베이스 성능에 큰 영향을 미쳐요.
- 인덱스 페이지 충돌 감소: 순차적 ID는 B-tree 인덱스의 끝에 추가되어서 페이지 분할이 적어요
- WAL 증폭 감소: 쓰기 전 로그(Write-Ahead Log)에 더 적은 데이터가 기록돼요
- 캐시 효율성: 최근 ID들이 메모리의 같은 페이지에 있을 가능성이 높아요
- 대용량 삽입: 처리량이 측정 가능하게 더 나아요
UUID의 장점
1. 분산 생성
UUID의 가장 큰 장점은 중앙 조정 없이 ID를 생성할 수 있다는 거예요.
- 여러 서비스가 독립적으로 ID를 생성할 수 있어요
- 단일 데이터베이스 시퀀스에 의존하지 않아요
- 클라이언트 측에서 ID 생성이 가능해요
- 마이크로서비스 아키텍처에서 조정 오버헤드가 없어요
2. 정보 은닉
순차적 ID는 정보를 드러내요. 사용자가 __CODE_UNDER_3__을 보면, 당신의 플랫폼에 사용자가 대략 얼마나 있는지 알 수 있죠. __CODE_UNDER_4__에서 __CODE_UNDER_5__으로 점프하면, 하루에 대략 몇 건의 주문이 있는지 추론할 수 있어요.
UUID는 이런 정보를 숨겨요:
- 경쟁사가 성장률을 추정하기 어려워요
- 비즈니스 볼륨이 노출되지 않아요
- 사용자가 플랫폼 크기나 활동을 추론할 수 없어요
3. 안전성과 실수 방지
UUID는 몇 가지 실용적인 안전성 이점이 있어요:
- 우발적 실수 방지: 데이터베이스를 수동으로 편집할 때 __CODE_UNDER_6__를 실수로 실행하기 쉽지만, 128비트 UUID는 타이핑하기 번거로워서 실수가 줄어들어요
- 충돌 가능성 극히 낮음: 적절히 생성된 UUID는 사실상 충돌하지 않아요
- 병합 안전성: 다른 데이터베이스에서 온 데이터를 병합할 때 ID 충돌 걱정이 없어요
핵심 딜레마: 성능 vs 분산
이 논쟁의 핵심은 근본적인 트레이드오프예요:
| 측면 | Bigserial | UUID |
|---|---|---|
| 크기 | 64비트 (8바이트) | 128비트 (16바이트) |
| 생성 방식 | 데이터베이스 시퀀스 | 분산 생성 가능 |
| 인간 가독성 | 높음 (12345) | 낮음 (550e8400-…) |
| 삽입 성능 | 빠름 (순차적) | 느림 (랜덤 쓰기) |
| 정보 은닉 | 낮음 | 높음 |
| 인덱스 크기 | 작음 | 큼 |
| 조정 필요 | 필요 (DB 시퀀스) | 불필요 |
대부분의 시스템에서 이 선택은 실제 요구 사항에 달려 있어요. 진짜 분산 생성이 필요한가요? 정보 은닉이 실제로 중요한가요? 삽입 성능이 병목인가요?
중간 지점: 순차적 UUID
극단 사이에서 선택하는 대신, 영리한 대안이 있어요. 순차화를 위해 UUID의 일부 비트를 예약하는 거죠.
ULID (Universally Unique Lexicographically Sortable Identifier)
ULID는 양쪽의 장점을 결합한 접근 방식이에요:
- 처음 48비트: 타임스탬프 (밀리초 정밀도)
- 나머지 80비트: 랜덤성
- 결과: 시간순으로 자연스럽게 정렬 가능한 ID
예시: __CODE_UNDER_7__
ULID의 이점:
- ✅ UUID의 분산 생성 능력
- ✅ Bigserial의 순차적 삽입 성능
- ✅ 시간순 정렬 가능 (디버깅에 유용)
- ✅ 표준 128비트 크기 (UUID 호환)
- ✅ 중앙 조정 불필요
다른 하이브리드 접근 방식
Instagram의 커스텀 ID 체계:
Instagram은 타임스탬프 + 샤드 ID + 시퀀스를 결합한 커스텀 ID 스킴을 만들었어요. 분산 생성과 성능을 모두 얻는 거죠.
Snowflake IDs:
Twitter에서 개발한 64비트 ID로, 타임스탬프, 워커 ID, 시퀀스 번호를 포함해요. Bigserial의 컴팩트함을 유지하면서 분산 생성을 가능하게 하죠.
인간적 요소: ID 접두사
Stripe는 또 다른 영리한 해결책을 보여줘요: 타입 접두사
문제는 이거예요. UUID는 한눈에 다 똑같이 보여요. __CODE_UNDER_8__을 봐도 그게 사용자인지, 결제인지, 상품인지 알 수 없죠.
Stripe의 해결책:
- __CODE_UNDER_9__ - charge (결제)
- __CODE_UNDER_10__ - customer (고객)
- __CODE_UNDER_11__ - invoice (인보이스)
- __CODE_UNDER_12__ - subscription (구독)
이점:
- ✅ 즉시 인식 가능한 객체 타입
- ✅ 다른 타입 간 복사-붙여넣기 오류 방지
- ✅ 인간 친화적 디버깅
- ✅ 내부적으로 여전히 bigserial 사용 가능 (성능 유지)
- ✅ API 응답이 자기 문서화돼요
이건 엔지니어링은 원칙론이 아니라 현명한 타협의 문제라는 걸 보여줘요.
Stripe의 실전 경험
Brandur는 Stripe의 실제 결정을 공유해요:
- 내부적으로 bigserial 사용: 수년 동안 검증된 성능
- 인간 사용성을 위해 접두사 추가: 개발자 경험 개선
- 대규모 볼륨으로 확장: 성능 중심 선택이 작동했어요
- 분산 생성 불필요: 아키텍처가 중앙 시퀀스로도 잘 작동했어요
트레이드오프 인식:
Stripe는 사용자가 ID 간격에서 대략적인 볼륨을 추론할 수 있다는 걸 인정해요. 하지만 성능과 단순성이 정보 은닉 우려를 능가한다고 판단했죠. 보편적 규칙이 아닌 상황에 따른 결정이에요.
실용적 권장사항
Bigserial을 선택하세요. 만약:
- 단일 데이터베이스에서 ID를 생성해요
- 삽입 성능이 중요해요 (높은 처리량)
- 개발자 경험과 디버깅 편의성을 중시해요
- 정보 은닉이 실제 비즈니스 위험이 아니에요
UUID를 선택하세요. 만약:
- 진짜 분산 생성이 필요해요 (여러 서비스, 클라이언트 측 생성)
- 데이터베이스 병합이나 샤드 리밸런싱을 해요
- 비즈니스 볼륨을 숨기는 게 중요해요
- 성능 비용을 감당할 수 있어요
ULID/하이브리드를 고려하세요. 만약:
- 양쪽의 장점이 필요해요
- 시간순 정렬이 유용해요
- 분산 생성을 원하지만 성능도 중요해요
- 새 프로젝트를 시작하고 유연성을 원해요
접두사 ID 패턴을 추가하세요. 만약:
- 공개 API를 만들고 있어요
- 개발자 경험이 중요해요
- 타입 안전성과 자기 문서화를 원해요
- 내부적으로 bigserial을 쓰면서 대외 API 사용성을 개선하고 싶어요
교훈과 시사점
1. 독단을 피하라
기술 커뮤니티에는 “항상 UUID를 써라” 또는 “절대 UUID를 쓰지 마라” 같은 독단적 주장이 많아요. 실제로는 보편적 “베스트 프랙티스”가 존재하지 않아요. 상황에 따른 트레이드오프는 판단을 요구해요.
2. 실제 요구 사항을 측정하라
추상적 원칙보다 구체적 질문을 하세요:
- 분산 생성이 진짜 필요한가? (대부분은 아니에요)
- 정보 은닉이 실제로 중요한가? (종종 과장돼요)
- 삽입 성능이 측정 가능한 병목인가? (규모에서 자주 그렇죠)
3. 중간 지점이 존재한다
양극단 사이에서 선택하는 대신, ULID나 접두사 ID 같은 하이브리드 솔루션이 실용적인 타협안을 제공해요. 현명한 절충안이 종종 극단보다 나아요.
4. 인간적 요소를 고려하라
개발자 경험이 중요해요. 디버깅하기 쉽고, 타입이 명확하고, 컴팩트한 ID가 장기적으로 생산성에 실제 영향을 미쳐요.
왜 이 글이 중요한가
이 글이 가치 있는 이유는 단순히 기술적 정보 때문이 아니에요. 생각하는 방식 때문이죠.
- 프로세스 초점: 경험 많은 엔지니어가 트렌드를 따르기보다는 체계적으로 트레이드오프를 생각하는 방법을 보여줘요
- 미묘한 결론: 승자를 선언하는 것을 거부하며, 지적 성숙도와 맥락 인식을 보여줘요
- 실무자 신뢰성: Stripe의 API를 설계한 엔지니어가 작성했어요. 기술에서 가장 존경받는 API 중 하나죠
- 실용적 대안: 문제를 분석하는 것뿐만 아니라 구체적인 솔루션을 제안해요 (ULID, 접두사)
- 장인 정신 지향: 개발자 경험에 영향을 미치는 세부 사항에 대한 관심을 보여줘요
기술적 의사결정은 독단이 아니라 맥락, 측정, 실용적 타협에 관한 것이에요.
저자 소개: Brandur Leach는 Stripe에서 API 디자인과 인프라를 담당했으며, 체계적인 엔지니어링 접근으로 유명해요. 그의 블로그 brandur.org는 인프라, API 디자인, PostgreSQL에 관한 깊이 있는 글로 잘 알려져 있어요.
참고: 이 글은 약 2800단어 분량의 원문을 한국어로 번역하고 확장한 것이에요. 데이터베이스 ID 설계의 실용적 트레이드오프에 관심 있는 백엔드 엔지니어에게 유용해요.
원문: Identity Crisis: Sequence v. UUID as Primary Key - Brandur Leach, Nanoglyphs (2021년 6월 29일)
번역 및 확장: Claude (Anthropic)
총괄: 존 (디노이저)