분산 환경에서 식별자를 만들 때 선택지는 의외로 많다. UUID v4·v7, ULID, Snowflake, 짧은 nanoid 같은 변형들. 각자 "정렬되는가", "DB 에 친화적인가", "URL 에 박을 수 있는가", "추측 가능한가" 라는 축 에서 트레이드오프를 가진다. 이 가이드는 실무에서 자주 부딪히는 다섯 선택지를 비교하고, 어느 경우에 어떤 것이 맞는지 정리한다.
왜 식별자 선택이 중요한가
식별자는 한 번 박으면 마이그레이션 비용이 크다 — DB row 만 바꾸는 게 아니라 외부에 노출된 URL, 로그, 분석 데이터, 백업까지 추적해야 한다. 그리고 식별자의 성격(정렬성·길이·생성 위치)이 다음 항목에 직접 영향을 준다.
- DB 인덱스 효율 — B-tree 인덱스는 정렬된 키에 친화적 이다. 랜덤 UUID v4 를 PK 로 박으면 insert 마다 인덱스 페이지가 흩어져 쓰기 성능과 캐시 효율이 떨어진다.
- URL 보안 — Auto-increment 정수 id 는 enumeration 공격 (다음 ID 추측) 에 취약. 충분히 큰 임의값이 필요한 경우가 많다.
- 로깅·디버깅 — 시간순으로 정렬되는 ID 는 "최근 만든 것" 을 한 눈에 본다. 랜덤 ID 는 로그 분석에 timestamp 가 추가로 필요.
5 가지 후보
1. UUID v4 (랜덤)
f47ac10b-58cc-4372-a567-0e02b2c3d479- 구조 — 122 비트 임의 + 6 비트 버전/variant. 128 비트.
- 형식 — 32 hex 글자 + 4 하이픈 = 36 자.
- 정렬 — 불가능 (완전 랜덤).
- 충돌 확률 — 사실상 0 (122 비트, 1 조개 생성해도 충돌 기대값 0.0000003).
- 장점 — 어디서나 안전. 클라이언트·서버·DB 어디서 만들든 충돌 X. 라이브러리 보편적.
- 단점 — DB PK 로 박으면 인덱스 fragmentation. 36 자 길이.
2. UUID v7 (timestamp + 랜덤)
018f5b86-7c4d-7abc-9def-0123456789ab- 구조 — 48 비트 Unix epoch 밀리초 + 12 비트 임의 + 62 비트 임의. RFC 9562 (2024).
- 형식 — UUID v4 와 동일한 36 자.
- 정렬 — 시간순 단조 증가 (밀리초 단위). 같은 ms 내에서는 랜덤.
- 충돌 확률 — 같은 ms 안에서 74 비트 임의 — 사실상 0.
- 장점 — UUID v4 의 호환성 + 정렬 가능. DB 인덱스 친화적. 신규 시스템 기본값으로 권장.
- 단점 — 발급 시각이 노출됨 (보안상 ID 가 언제 만들어졌는지 알리고 싶지 않다면 v4 또는 별도 칼럼).
3. ULID
01HQXM5K3D7Z9YW0123456789AB- 구조 — 48 비트 Unix epoch 밀리초 + 80 비트 임의 = 128 비트.
- 형식 — Crockford Base32, 26 자. 하이픈 없음.
- 정렬 — 시간순 사전식 정렬 (lexicographic) — 문자열 정렬만으로 시간 순서.
- 장점 — URL 친화 (영숫자만). UUID v4 보다 28% 짧음. ULID 와 UUID 사이 1:1 변환 가능 (둘 다 128 비트).
- 단점 — DB native 타입 없음 (대부분 VARCHAR 26 또는 BINARY 16). UUID v7 의 표준화 이후 ULID 의 상대 가치 약간 감소.
4. Snowflake (Twitter)
1768956123987456000- 구조 — 41 비트 timestamp + 10 비트 머신 ID + 12 비트 시퀀스 = 63 비트 (signed 64-bit 정수).
- 형식 — 정수 — 보통 19 자 문자열.
- 정렬 — 시간순 단조 증가.
- 장점 — 정수 인덱스가 가장 빠름. BIGINT 8 바이트로 저장. URL 짧음.
- 단점 — 머신 ID 할당 인프라 필요. 분산 환경에서 시계 역행 대응 코드 필요. 클라이언트 측에서 생성 불가 (머신 ID 충돌).
5. nanoid (짧은 랜덤)
V1StGXR8_Z5jdHi6B-myT- 구조 — 기본 21 자, 126 비트 임의. URL-safe 알파벳.
- 정렬 — 불가능.
- 장점 — UUID v4 보다 짧으면서 보안 강도 동일.
- 단점 — DB 표준 타입 없음. 디코딩 도구 적음.
한눈에 비교
| UUID v4 | UUID v7 | ULID | Snowflake | |
|---|---|---|---|---|
| 길이 | 36 자 | 36 자 | 26 자 | ~19 자 |
| 비트 | 122 | ~74 + ms | 80 + ms | ~12 + ms |
| 정렬 | X | O (ms) | O (ms) | O |
| 시각 노출 | X | O | O | O |
| 분산 생성 | O | O | O | 인프라 필요 |
| DB 타입 | UUID | UUID | BINARY/VARCHAR | BIGINT |
실무 선택 가이드
새 시스템의 PK
UUID v7 권장. UUID 표준 타입을 그대로 쓰면서 시간순 정렬이 되어 인덱스 fragmentation 이 거의 없다. 라이브러리는 Node 의 uuid@9+, Python 의 uuid_utils, Go 의 google/uuid@1.6+ 가 v7 을 지원. UUID / ULID 생성기 도 v7 생성 지원.
외부 노출 URL
시각 노출이 보안 문제이면 (예: 가입한 시간이 알려지면 안 됨) UUID v4 또는 nanoid. 그렇지 않으면 UUID v7 또는 ULID. auto-increment 정수는 외부 노출 금지 — 다음 ID 추측 + 데이터 양 추정 가능.
로그 / 이벤트 ID
ULID 추천. 짧고 (26 자), 시간 정렬, URL 친화. 로그 조회에 timestamp 칼럼 따로 안 둬도 ID 자체로 시간 정렬.
고성능 DB PK
Snowflake 또는 BIGINT auto-increment. UUID v7 도 충분히 빠르지만, PostgreSQL BIGINT (8 바이트) vs UUID (16 바이트) 인덱스 크기 차이는 대규모에서 의미가 있다. 클라이언트 생성이 필요 없는 백엔드 시스템 한정.
클라이언트 생성 ID (offline-first)
UUID v4 또는 v7. 머신 ID 할당 인프라 없이도 충돌 0. 모바일/PWA 가 오프 라인에서 만든 ID 를 서버로 동기화하는 패턴에 표준.
자주 빠지는 함정
1. UUID v1 (MAC + timestamp)
오래된 시스템에서 보이지만 권장하지 않는다. MAC 주소가 노출돼 프라이버 시 이슈. 같은 머신에서 빠르게 생성하면 충돌 가능. v7 이 사실상 후속.
2. UUID 문자열로 저장
PostgreSQL/MySQL 의 UUID 타입은 16 바이트 binary 저장. 문자열 (36 자) 로 저장하면 인덱스 크기 2.25 배, 쿼리도 느려진다. 항상 native UUID 타입 사용.
3. ULID 의 monotonic 보장 오해
같은 ms 안에서 ULID 가 단조 증가하려면 라이브러리의 monotonic 모드를 명시 활성화해야 한다. 그렇지 않으면 같은 ms 내에서 랜덤 — 정렬은 ms 단위까지만 보장.
4. 충돌 가능성 과대평가
UUID v4 의 122 비트 임의는 1 초당 100 만 개 생성을 100 년 해도 50% 충돌 확률이 안 된다. "혹시 충돌하면" 걱정으로 PK 에 UNIQUE 제약 + 재시도 로직을 박는 건 대개 과한 방어. 단, 외부 시드를 받아 생성하는 경우 (예: deterministic UUID v5) 는 별개.
도구로 직접 보기
- UUID / ULID 생성기 — UUID v4 / v7 / ULID 생성 + 여러 개 일괄.
- Unix 타임스탬프 변환 — UUID v7 / ULID 에서 추출한 timestamp 검증.
요약
- 새 PK 의 기본값은 UUID v7. UUID 호환성 + DB 인덱스 친화 + 분산 생성.
- 외부 노출 URL — 시각 노출 OK 면 v7/ULID, 보안 우선이면 v4 또는 nanoid.
- 로그/이벤트 — ULID 가 짧고 정렬되어 편함.
- UUID 는 항상 native binary 타입으로 저장 — 문자열 저장은 2.25 배 낭비.
- auto-increment 정수 ID 를 외부 URL 에 박지 마라 — enumeration 공격.