13 KiB
title, source_type, status, confidence, tags, related_projects, last_reviewed
| title | source_type | status | confidence | tags | related_projects | last_reviewed | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Resource Identifier Format (ULID vs UUIDv7 vs UUIDv4 vs Snowflake) | llm-generated | draft | medium |
|
|
2026-06-04 |
Resource Identifier Format (ULID vs UUIDv7 vs UUIDv4 vs Snowflake)
Layer:
wiki/concepts/— 일반 개념. 특정 프로젝트(ca-tmpl)의 적용 사실은 wiki/projects/ca-tmpl/resource-identifier-format 로 분리.
Summary
Resource identifier format 결정은 API resource 를 가리키는 public ID 의 형식(random vs time-ordered, charset, 길이, prefix)을 고르는 일이다. 후보는 크게 random 계열(UUID v4, NanoID)과 time-ordered 계열(UUID v7, ULID, KSUID, Snowflake, TSID)로 갈린다. 핵심 trade-off 축은 (1) 정렬성/DB index locality, (2) timestamp leak(privacy), (3) URL 길이/charset, (4) 조율 부담, (5) 표준 여부다. ID 는 URL·log·DB PK·cache key·FK 에 한 번 박히면 변경이 breaking 이므로, 형식 선택은 되돌리기 어려운 결정이다.
Standard (공식 정의)
UUID (RFC 9562, 2024)
IETF RFC 9562 는 UUID 의 128-bit 구조와 버전을 정의한다. v4 는 순수 random, v7 은 48-bit Unix millisecond timestamp 를 앞에 두는 time-ordered 변형이며, 같은 timestamp 내 단조성을 위한 monotonicity 메커니즘을 규정한다. RFC 는 새 ID 가 필요할 때 time-ordered 변형(v6/v7)을 SHOULD 로 권고한다. §8 은 timestamp 노출의 attack surface 를 "very small" 로 기술한다.
출처: raw/official-docs/rfc9562-uuid (RFC9562-C1~C5).
ULID (공식 spec)
ULID 는 128-bit 를 26-char Crockford base32 로 인코딩한 형식이다. 앞 48-bit 가 millisecond timestamp(정렬 가능), 뒤 80-bit 가 random. getMonotonicUlid() 류의 monotonic factory 는 동일 ms 내 단조 증가를 보장한다. 128-bit 이므로 UUID 와 binary 호환(상호 변환 가능)이다.
출처: raw/official-docs/ulid-spec.md (ULID-C1~C6).
Crockford base32 / RFC 3986
- Crockford base32: 32-char alphabet 에서 사람이 혼동하는 I / L / O / U 를 제외한다. 디코딩 시
I/L→1,O→0으로 정규화하고 대소문자를 구분하지 않는다(case-insensitive). 출처: raw/official-docs/crockford-base32-spec.md (CROCKFORD-C1~C4). - RFC 3986 (URI generic syntax):
unreservedcharset 은ALPHA / DIGIT / "-" / "." / "_" / "~". path component 는 case-sensitive 로 취급되며 §6.2.2.1 의 case normalization 규칙은 scheme/host 에만 적용된다. ULID 의0-9A-Z는unreserved의 진부분집합이라 percent-encoding 없이 URL path 에 안전하다. 출처: raw/official-docs/rfc3986-uri-generic-syntax (RFC3986-C1/C3/C4).
식별자 관례 (벤더 표준 — best practice 아님)
- Google AIP-148:
name(server-assigned),uid(system-assigned opaque, non-PII),display_name(mutable),parent(계층 resource name) 표준 필드. 출처: raw/official-docs/google-aip-148-standard-fields (AIP148-C1~C5). - Stripe: typed prefix opaque ID(
ch_,cus_,pi_). 단 Stripe 스스로 prefix 변경을 backward-compatible 로 분류 → prefix 영구 불변 보장이 아니므로 prefix 의존 코드는 lock-in 위험. Idempotency-Key 는 client-generated 로 resource ID 와 별개. 출처: raw/official-docs/stripe-resource-id-convention (STRIPE-C1~C5).
AIP-148·Stripe 는
official-vendor-doc/벤더 관례다. RFC 9562·RFC 3986·ULID spec 같은official-standard와 달리 "공식 best practice" 로 일반화하면 안 된다.
한계 / 주의점
후보별 trade-off:
| 형식 | 정렬성(DB index) | timestamp leak | URL 길이 | 조율 부담 | 표준 |
|---|---|---|---|---|---|
| Sequential integer | 최상 | 없음(but enumeration/count leak) | 짧음 | 없음 | — |
| UUID v4 | 나쁨(random → B-tree 단편화) | 없음 | 36자(dashed) | 없음 | RFC 9562 |
| UUID v7 | 좋음(time-ordered) | 48-bit ms 노출 | 36자 | 없음 | RFC 9562 |
| ULID | 좋음(time-ordered) | 48-bit ms 노출 | 26자 | 없음 | ULID spec(비-IETF) |
| NanoID | 나쁨(random) | 없음 | 21자(default) | 없음 | 라이브러리 |
| KSUID | 좋음 | 초 단위 노출 | 27자(base62) | 없음 | 라이브러리 |
| Snowflake | 좋음(k-sorted) | ms 노출 + machine ID | ~19자(64-bit) | worker/datacenter id 조율 | 라이브러리 |
| TSID | 좋음 | ms 노출 | BIGINT fit | 일부 | 라이브러리 |
| CUID2 | 없음(보안 우선) | 없음(저자 주장) | 24자(base36) | 없음 | 라이브러리 |
주요 함정:
- Sequential ID: enumeration attack + count leak + tenant 격리 위반. public ID 로 부적합.
- random UUID v4 의 DB 비용: time-ordered 가 아니라 B-tree index 에 random insert → page split + WAL/디스크 증가. Percona 의 MySQL InnoDB 25M-row 벤치마크에서 random UUID PK 가 ordered UUID 대비 +50% 디스크, ordered UUID ≈ BIGINT 성능. 단 이는 MySQL InnoDB clustered index 기준 — PostgreSQL HEAP/MVCC 등 다른 엔진에는 parallel evidence 로만 적용된다. 출처: raw/company-tech-blogs/percona-uuid-storage-mysql (PERCONA-UUID-C2~C5).
- timestamp leak: UUID v7 / ULID 는 48-bit ms timestamp 가 평문 노출 → 작성 시각·가입 순서·활동 패턴 추론 가능. user-facing ID 에서 실질 문제. 완화책은 수용 / random scramble / CUID2 채택. CUID2 의 timestamp 비노출은 저자 주장이며 독립 감사로 확인된 것은 아니다. 출처: raw/official-docs/cuid2-spec.md (CUID2-C1).
- Snowflake 의 조율 부담: worker_id / datacenter_id 를 노드마다 사전 할당해야 함 → 단일 generator 환경에는 과한 운영 부담. 출처: raw/company-tech-blogs/snowflake-twitter-id (SNOWFLAKE-C1~C5).
- case-insensitive charset 의 함정: Crockford base32(ULID)는 입력이 case-insensitive 라 서버가 URL boundary 에서 canonical uppercase 로 normalize 하지 않으면 cache key miss 가 발생한다.
- typed prefix lock-in: Stripe 자신이 prefix 변경을 backward-compatible 로 본다 → prefix 를 파싱·의존하는 코드는 깨질 수 있다.
- public ID vs internal sequence: external-only(ULID 하나가 public ID = PK, Stripe)는 단순하지만, dual column(internal BIGINT + external ULID, Shopify/Linear/PlanetScale)은 audit/JOIN 성능을 회수한다. 후자는 cache key/FK 를 어느 쪽으로 둘지 추가 결정을 부른다. 출처: raw/company-tech-blogs/planetscale-nanoid-api (PLANETSCALE-NANOID-C4).
Project Application
- ca-tmpl(Clean Architecture skeleton)에서의 실제 ULID 채택 +
adapter-identifier모듈 구현 사실은 wiki/projects/ca-tmpl/resource-identifier-format 참조. (본 개념 문서는 일반론만 다룬다.)
Claim-backed Knowledge
인용된 raw source 의 claim 만. 출처 없는 일반화 금지.
| Knowledge Point | Supporting Claims | Confidence | Notes |
|---|---|---|---|
| RFC 9562 가 UUID v7 = time-ordered(48-bit Unix ms) 를 정의하고 새 ID 에 time-ordered 를 SHOULD 권고 | raw/official-docs/rfc9562-uuid RFC9562-C1/C3 | high | official-standard |
| RFC 9562 §8 이 timestamp 노출 attack surface 를 "very small" 로 기술 | raw/official-docs/rfc9562-uuid RFC9562-C5 | high | official-standard |
| ULID = 26-char Crockford base32, 48-bit ms timestamp + 80-bit random, monotonic 정렬 | raw/official-docs/ulid-spec.md ULID-C1~C5 | high | official-reference(비-IETF spec) |
| Crockford base32 가 I/L/O/U 제외 + 디코딩 시 정규화(case-insensitive) | raw/official-docs/crockford-base32-spec.md CROCKFORD-C1~C3 | high | official-reference |
RFC 3986 unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~", path case-sensitive |
raw/official-docs/rfc3986-uri-generic-syntax RFC3986-C1/C3 | high | official-standard |
| Google AIP-148 의 uid = system-assigned opaque(non-PII), display_name 과 분리 | raw/official-docs/google-aip-148-standard-fields AIP148-C2/C3 | medium | official-vendor-doc (벤더 관례, 공식 표준 아님) |
| Stripe 가 typed prefix 변경을 backward-compatible 로 분류(영구 불변 보장 아님) | raw/official-docs/stripe-resource-id-convention STRIPE-C2 | medium | official-vendor-doc |
| Percona: MySQL InnoDB 에서 random UUID PK 가 ordered UUID 대비 +50% 디스크, ordered UUID ≈ BIGINT (25M-row) | raw/company-tech-blogs/percona-uuid-storage-mysql PERCONA-UUID-C2/C5 | medium | company-case-study (MySQL 5.x, 타 엔진엔 parallel evidence) |
| CUID2 가 timestamp leak 없음 | raw/official-docs/cuid2-spec.md CUID2-C1 | low | official-reference (저자 주장, 독립 감사 미확인) |
| Snowflake 가 worker/datacenter id 사전 조율을 요구 | raw/company-tech-blogs/snowflake-twitter-id SNOWFLAKE-C1 | medium | company-case-study |
NanoID 21자 default + URL-safe alphabet A-Za-z0-9_- + crypto-strong random |
raw/official-docs/nanoid-spec NANOID-C1/C2/C4 | high | official-reference |
| Brandur(전 Stripe): Idempotency-Key 는 client-generated, ~24h TTL, request fingerprint 비교 | raw/company-tech-blogs/brandur-stripe-idempotency-keys BRANDUR-IDEMP-C8~C12 | medium | engineering-blog |
내가 설명할 수 있어야 하는 것
- time-ordered ID(UUID v7 / ULID)가 random UUID v4 대비 DB index locality 에 유리한 원리(B-tree 에 정렬된 키가 append 우세).
- timestamp leak 가 왜 user-facing ID 에서만 실질 문제인지, 완화책(수용 / scramble / CUID2)의 trade-off.
- Crockford base32 가 I/L/O/U 를 제외하는 이유 + 그래서 생기는 canonical uppercase 출력 + case-insensitive 입력 정규화 의무.
- public ID vs internal sequence(external-only vs dual column)의 trade-off.
- Idempotency-Key(client-generated, ephemeral) 와 resource ID(server-assigned, persistent)가 왜 별개 형식인지.
- "Netflix/Stripe 가 X 를 쓰니까 공식이다" 가 아니라, RFC(official-standard) 와 벤더 관례(vendor-doc)·사례(case-study)를 구분해 말하는 것.
Interview Questions
- ULID 와 UUID v7 은 둘 다 time-ordered 인데 왜 ULID 를 고를 수 있는가? (URL 길이 26 vs 36, Crockford base32 의 human-friendliness, Java 21
java.util.UUID의 v7 native 미지원.) - random UUID v4 를 DB PK 로 쓰면 어떤 비용이 있는가? 어느 엔진 기준 벤치마크인가?
- ULID/UUID v7 의 timestamp leak 가 실제로 어떤 정보를 노출하는가? 언제 문제이고 어떻게 완화하나?
- typed prefix(
tk_)를 쓰는 것의 장단점은? Stripe 가 prefix 변경을 어떻게 분류하는가? - public ID 와 internal sequence 를 분리(dual column)하는 동기와 비용은?
Do Not Overclaim
- "ULID 가 UUID 보다 항상 우월하다" → 금지. timestamp leak(privacy), 비-IETF 표준, 라이브러리 의존이라는 trade-off 존재.
- "random UUID 는 PostgreSQL 에서도 느리다" → 단정 금지. 인용 벤치마크는 MySQL InnoDB clustered index 기준 — 다른 엔진에는 parallel evidence 일 뿐.
- "CUID2 는 timestamp 가 절대 안 샌다" → 단정 금지. spec 저자 주장이며 독립 감사로 확인된 것은 아니다.
- "Google AIP / Stripe 관례 = 업계 공식 표준" → 금지. 벤더 관례·사례이지 RFC 같은 official-standard 가 아니다.
- "sequential ID 는 무조건 나쁘다" → 맥락 의존. internal-only(외부 비노출) 라면 합리적일 수 있고, dual column 의 internal PK 가 그 예다.
Sources
- raw/official-docs/rfc9562-uuid — IETF RFC 9562 (UUID v4/v6/v7/v8, monotonicity, §8 attack surface).
- raw/official-docs/ulid-spec.md — ULID 공식 spec (26-char Crockford base32, monotonic).
- raw/official-docs/crockford-base32-spec.md — Crockford base32 (I/L/O/U 제외, case-insensitive 디코딩).
- raw/official-docs/rfc3986-uri-generic-syntax — URI generic syntax (
unreservedcharset, case normalization). - raw/official-docs/cuid2-spec.md — CUID2 (timestamp-leak-free 저자 주장).
- raw/official-docs/nanoid-spec — NanoID (21자 URL-safe, crypto random).
- raw/official-docs/google-aip-148-standard-fields — Google AIP-148 standard fields.
- raw/official-docs/stripe-resource-id-convention — Stripe typed prefix opaque ID 관례.
- raw/company-tech-blogs/percona-uuid-storage-mysql — Percona MySQL InnoDB UUID PK 벤치마크.
- raw/company-tech-blogs/snowflake-twitter-id — Twitter Snowflake (조율 부담).
- raw/company-tech-blogs/planetscale-nanoid-api — PlanetScale NanoID + dual column 사례.
- raw/company-tech-blogs/brandur-stripe-idempotency-keys — Brandur: Idempotency-Key vs resource ID.
- raw/company-tech-blogs/segment-ksuid — Segment KSUID (base62, 초 단위 timestamp).
- raw/company-tech-blogs/github-graphql-global-node-id — GitHub global node ID (base64 type-encoded).
- raw/company-tech-blogs/aws-iam-arn-format — AWS ARN 계층 prefix.