
PK: 1, 2, 3...의 시대는 끝났는가?
과거 모놀리식(Monolithic) 아키텍처 시절, 데이터베이스의 Primary Key(PK)는 고민할 필요 없이 Auto Increment(Sequence)였습니다. 하지만 마이크로서비스 아키텍처(MSA)와 분산 데이터베이스 환경이 표준이 되면서 이야기는 달라졌습니다.
서로 다른 서버에서 동시에 데이터를 생성해야 하는 환경에서 순차적인 ID 발급은 병목 구간(Bottleneck)이자 단일 실패 지점(SPOF)이 될 수 있습니다. 이때 등장한 구세주가 바로 UUID(Universally Unique Identifier) 입니다.
하지만 UUID를 무턱대고 RDB의 PK로 사용하는 순간, 여러분의 데이터베이스는 비명을 지를 수도 있습니다. 오늘은 UUID의 강력함과 치명적인 단점, 그리고 이를 해결하기 위한 현대적인 대안까지 심도 있게 다뤄보겠습니다.
1. UUID란 무엇인가?
UUID는 128-bit(16바이트)의 숫자로, 보통 32개의 16진수 문자와 4개의 하이픈으로 구성된 문자열로 표현됩니다.
Format: 550e8400-e29b-41d4-a716-446655440000
이론적으로 충돌 확률이 0은 아니지만, 사실상 0에 수렴합니다.
수학적으로 UUID v4(완전 랜덤)에서 충돌이 발생할 확률은 다음과 같습니다.

만약 매초 10억 개의 UUID를 100년 동안 생성하더라도, 단 하나의 충돌이 발생할 확률은 약 50% 정도입니다. 즉, 지구상의 시스템에서는 "중복 걱정은 접어둬도 된다"는 뜻입니다.
Python 예시 코드 (UUID v4 생성)
import uuid
def generate_user_id():
# 랜덤 기반의 UUID v4 생성
user_id = uuid.uuid4()
print(f"Generated UUID: {user_id}")
print(f"Hex Value: {user_id.hex}")
print(f"Variant: {user_id.variant}, Version: {user_id.version}")
return str(user_id)
# 실행 결과 예시: f47ac10b-58cc-4372-a567-0e02b2c3d479
generate_user_id()
2. UUID의 치명적인 매력 (장점)
1) 중앙 통제가 필요 없는 독립성
별도의 ID 발급 서버(Sequence generator)와 통신할 필요가 없습니다. 클라이언트(앱/웹)나 여러 대의 WAS에서 각자 ID를 생성하고 DB에 밀어넣기만 하면 됩니다. 이는 네트워크 왕복 비용(Round Trip)을 줄여줍니다.
2) 보안성 (예측 불가능성)
users/100, users/101 같은 순차적 ID는 경쟁사의 규모를 추정하거나, URL 파라미터 조작을 통해 다른 유저의 데이터에 접근하려는 시도(Enumeration Attack)에 취약합니다. UUID는 다음 ID를 예측할 수 없어 이러한 공격을 원천 차단합니다.
3) 데이터 병합의 용이성
서로 다른 DB(예: 인수합병, 샤딩된 DB 통합)를 합칠 때, Auto Increment를 썼다면 ID 충돌 대재앙이 일어납니다. UUID는 그냥 합치면 됩니다.
3. UUID가 감추고 있는 어두운 면 (단점)
많은 개발자가 간과하는 부분이 바로 "성능"입니다.
1) 스토리지 비용 증가
BIGINT(8바이트)에 비해 UUID(16바이트)는 2배의 공간을 차지합니다. 문자열(CHAR(36))로 저장한다면 4배 이상 커집니다. 수억 건의 행이 있는 테이블에서는 인덱스 크기 자체가 수 기가바이트씩 차이 나게 됩니다.
2) DB 성능 저하 (Clustered Index Fragmentation)
가장 치명적인 단점입니다. MySQL(InnoDB) 같은 RDBMS는 PK를 기준으로 데이터를 정렬하여 저장(Clustered Index)합니다.
- Sequential ID: 항상 끝에 추가되므로 페이지 분할(Page Split)이 적고 캐시 효율이 좋습니다.
- UUID v4 (Random): ID가 뒤죽박죽이므로, DB는 데이터를 중간중간 끼워 넣기 위해 디스크의 데이터 페이지를 끊임없이 재정렬하고 분할해야 합니다. 이를 **인덱스 파편화(Fragmentation)**라고 하며, INSERT 성능을 급격히 떨어뜨립니다.
3) 가독성과 디버깅
WHERE id = 105와 WHERE id = 'f47ac10b-...'는 로그를 볼 때 개발자의 피로도 차이가 큽니다.
4. 개선 가능성: UUID v7과 TSID (Game Changer)
"고유성은 유지하되, 정렬 가능하게(Sortable) 만들면 어떨까?"라는 아이디어에서 최신 스펙이 등장했습니다.
UUID v7 (Timestamp-based)
2024년 공식 표준(RFC 9562)으로 승인된 UUID v7은 앞부분 48비트에 Unix Timestamp를 포함합니다.
- 구조: [Timestamp(48bit)] - [Ver] - [Random]
- 특징: 생성된 순서대로 정렬됩니다. 즉, DB 인덱스 파편화 문제를 해결하면서 UUID의 장점을 그대로 가집니다.
Python 예시 (UUID v7 라이브러리 활용)
import uuid6 # pip install uuid6 (표준 라이브러리 업데이트 전까지 사용)
def generate_sorted_uuid():
# 시간 순서가 보장되는 UUID v7 생성
sorted_id = uuid6.uuid7()
print(f"UUID v7: {sorted_id}")
print(f"Timestamp 포함 여부 확인: {sorted_id.time}")
return sorted_id
# 실행 결과: 018d4e5f-7b1a-7000-8d9e-123456789abc
# 잠시 후 실행하면 앞부분 숫자가 증가해 있음 (정렬 가능)
5. 실제 활용 전략 및 결론
그렇다면 언제 무엇을 써야 할까요?
| 상황 | 추천 전략 | 이유 |
| 소규모/내부 시스템 | Auto Increment | 성능 최고, 디버깅 용이, 스토리지 절약 |
| 대규모 분산 시스템 | UUID v7 (or TSID) | 충돌 방지 + DB 인덱스 성능 최적화 |
| 보안이 중요한 공개 URL | UUID v4 | 유추 불가능성 (단, 내부 PK와 별도로 매핑하여 사용 권장) |
| 로그/추적 ID | UUID v4 | 생성 속도가 가장 빠르고 정렬이 크게 중요하지 않음 |
결론:
UUID는 강력하지만, RDBMS의 PK로 무작정(특히 v4를) 사용하는 것은 기술 부채가 될 수 있습니다. "정렬 가능한 고유 식별자(Sortable Unique Identifier)"인 UUID v7이나 ULID, TSID 등을 도입하는 것이 현대적인 백엔드 시스템의 모범 답안이 되어가고 있습니다.
지금 여러분의 DB 스키마를 확인해 보세요. 무작위 UUID가 인덱스를 파편화시키고 있지는 않나요?
댓글