Files
llm-wiki/wiki/concepts/resource-identifier-format.md

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
resource-identifier
ulid
uuid
backend
ca-skeleton
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/L1, O0 으로 정규화하고 대소문자를 구분하지 않는다(case-insensitive). 출처: raw/official-docs/crockford-base32-spec.md (CROCKFORD-C1~C4).
  • RFC 3986 (URI generic syntax): unreserved charset 은 ALPHA / DIGIT / "-" / "." / "_" / "~". path component 는 case-sensitive 로 취급되며 §6.2.2.1 의 case normalization 규칙은 scheme/host 에만 적용된다. ULID 의 0-9A-Zunreserved 의 진부분집합이라 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

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