UUID 생성기

데이터베이스 PK로 UUIDv7가 UUIDv4를 이기는 이유

약 10년간의 통념은 "PK는 auto-increment 정수, UUID는 DB 성능을 망친다"였습니다. 맞는 결론이었지만 "UUID는 느리다"는 일반화가 과했습니다. 진짜 범인은 UUIDv4의 완전한 무작위성이었지 UUID 자체가 아니었습니다. UUIDv7(RFC 9562, 2024년 5월에 RFC 4122를 대체)은 상위 비트에 Unix-millisecond 타임스탬프를, 하위에 랜덤 비트를 둡니다. 1ms 간격으로 생성된 두 값은 시간으로 정렬되고, 같은 ms 안에서 생성된 두 값은 그 ms 내에서 무작위로 정렬됩니다. 삽입 패턴이 완전히 달라집니다. 이게 중요한 이유는 B-tree입니다. PostgreSQL, MySQL/InnoDB, SQL Server 모두 PK 순서로 B-tree(B+ tree)에 행을 저장합니다. 기존 모든 키보다 큰 PK 값을 insert하면 항상 가장 오른쪽 리프에 닿습니다. 그 페이지는 buffer pool에 hot 상태이고, 다음 insert도 같은 페이지에 들어가서 디스크 IO가 거의 필요 없습니다. UUIDv4는 매 insert가 무작위 리프에 닿습니다 — 매번 디스크에서 새 페이지를 끌어오고, 더럽히고, 다른 페이지를 evict하고, dirty page를 다시 써내야 합니다. 인덱스가 RAM에 다 들어가지 않을 만큼 큰 테이블에서는 UUIDv4 insert가 auto-increment 대비 10~50배 느릴 수 있고, 무작위 insert가 트리 곳곳에서 page split을 일으켜 B-tree가 심하게 fragment 됩니다. UUIDv7은 이걸 제거합니다. insert가 시간 순서이므로 인덱스의 작은 hot 영역에 들어갑니다 — 항상 가장 오른쪽 리프는 아니지만(랜덤 하위 비트로 약간의 지터), 캐시에 머무르는 작은 워킹셋입니다. PlanetScale, AWS, Percona의 벤치마크 모두 modern InnoDB와 Postgres에서 UUIDv7 INSERT 처리량이 bigint auto-increment 대비 10~20% 이내라는 결과를 냅니다. UUIDv4를 괴롭히던 fragmentation도 시간상 가까운 키들이 항상 모이기 때문에 사라집니다. 실무 주의 한 가지. InnoDB는 UUID를 16바이트 BINARY로 저장하는 게 정석입니다 — 36자 하이픈 hex 문자열 말고 BINARY(16)으로. hex 형태는 36바이트(하이픈 빼면 32)로 2배 이상 저장 공간을 쓰고, 인덱스 footprint도 2배 이상 — 페이지당 키 수가 줄고 IO가 그만큼 늘어납니다. Postgres에는 네이티브 uuid 타입(16바이트)이 있으니 쓰세요. 사람 가독성과 저장 공간의 정답은 binary로 저장하고 API 경계에서만 hex로 렌더링하는 것입니다.
// UUIDv4 layout: 122 random bits, 6 fixed bits
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
                ^^^^      ^^^^
                version 4 variant bits

// UUIDv7 layout: 48-bit Unix ms timestamp + 74 random bits
ttttttttttttt-tttt-7xxx-yxxx-xxxxxxxxxxxx
^^^^^^^^^^^^^ ^^^^      ^^^^
unix_ts_ms    rand-A    rand-B
              + version + variant

// Postgres
CREATE TABLE orders (
  id uuid PRIMARY KEY DEFAULT uuidv7(),  -- PG18+
  ...
);

// MySQL — store as BINARY(16), not VARCHAR(36)
CREATE TABLE orders (
  id BINARY(16) PRIMARY KEY,
  ...
);

// JS — generate UUIDv7 (no deps)
function uuidv7() {
  const ts = Date.now();
  const tsHex = ts.toString(16).padStart(12, '0');
  const rand = crypto.getRandomValues(new Uint8Array(10));
  rand[0] = (rand[0] & 0x0f) | 0x70; // version 7
  rand[2] = (rand[2] & 0x3f) | 0x80; // variant
  const r = [...rand].map(b => b.toString(16).padStart(2, '0')).join('');
  return `${tsHex.slice(0,8)}-${tsHex.slice(8)}-${r.slice(0,4)}-${r.slice(4,8)}-${r.slice(8)}`;
}

UUID 충돌 확률 — 생일 역설을 숫자로

"UUID 충돌 확률은 얼마나 되나요?"는 모든 팀이 한 번은 묻는 질문입니다. 답의 근거는 생일 역설입니다. UUIDv4는 122 랜덤 비트(나머지 6 비트는 version·variant로 고정)를 가지므로 네임스페이스는 2^122 ≈ 5.3 × 10^36 가지. 직관은 "이 정도 슬롯이면 충돌은 불가능"이라고 말하지만, 생일 역설에 따르면 충돌 위험은 N이 아니라 sqrt(N)에 비례합니다 — 첫 충돌 50% 확률에 도달하려면 약 sqrt(2^122) ≈ 2^61 ≈ 2.3 × 10^18개를 생성해야 합니다. 구체적으로 — 초당 10억 개의 UUIDv4를 100년간 생성해도 충돌 확률은 10^-9 자릿수입니다. 총 1조 개를 만들었을 때 그중 어떤 두 개가 같을 확률은 약 10^-15 — 올해 운석에 맞을 확률보다 100만 배 낮습니다. 단일 애플리케이션에서는 사실상 0입니다. 수십 년 동안 극단적 규모로 생성하는 여러 독립 시스템에서 나온 UUID들을 합쳤을 때만 수학이 흥미로워지고, 그 경우에도 실무 답은 "전략 하나 정하고 절대 걱정하지 마라"입니다. 현대 UUID 계열의 버전 표는 대략 이렇습니다 — v1 (1988): 타임스탬프 + MAC 주소. 호스트별 고유하지만 MAC과 시각이 노출됨. 거의 사장. v3 / v5: namespace + name의 결정적 해시. 같은 입력 → 같은 UUID. 파생 ID에 유용. v4: 122 랜덤 비트. 2010~2024년 지배적 선택. v6: v1과 비슷하나 정렬 가능한 바이트 순서. 틈새. v7 (RFC 9562, 2024): 48비트 Unix ms + 74 랜덤 비트. 신규 시스템 강력 추천. v8: 커스텀. v7로 안 되는 경우에만. 실무 교훈: UUID 포맷을 직접 깎지 마세요. 6개의 reserved bit(version + variant) 덕분에 도구와 DB가 UUID를 식별합니다. v4를 만들면서 version 비트를 잘못 세팅하면 다른 버전으로 해석되어 비교·정렬·드라이버 수준 최적화가 깨집니다. 항상 검증된 라이브러리 또는 위의 표준 공식을 쓰세요.
// Birthday paradox: probability of ANY collision among n values
// drawn from a space of size N is ≈ 1 - exp(-n^2 / (2N))

// For UUIDv4: N = 2^122
// n = 1 billion → P ≈ 4.7 × 10^-20
// n = 1 trillion (10^12) → P ≈ 4.7 × 10^-14
// 50% collision threshold: n ≈ 2.3 × 10^18

// Detect the version of a UUID
function uuidVersion(uuid) {
  // version is the first hex digit of the third group
  return parseInt(uuid[14], 16);
}
uuidVersion('018fb1c2-...-7abc-...');  // 7
uuidVersion('a1b2c3d4-...-4abc-...');  // 4

// Deterministic UUIDv5 (SHA-1 of namespace + name)
// Same name → same UUID, useful for migration mapping
import { v5 as uuidv5 } from 'uuid';
const NS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // DNS namespace
uuidv5('example.com', NS); // always "cfbff0d1-9375-5685-968a-48ce8b50e3e0"

ULID vs UUIDv7 — 하나 골라서 끝내자

UUIDv7가 표준화되기 전에도 커뮤니티는 시간 정렬되는 ID 포맷을 여러 개 만들어왔습니다. 가장 유명한 게 2016년에 나온 ULID(Universally Unique Lexicographically Sortable Identifier)입니다. ULID도 128비트입니다 — 48비트 ms 타임스탬프 + 80비트 랜덤 — 다만 hex 하이픈이 아니라 Crockford Base32로 인코딩합니다. 시각적 형식은 01HX1Y9ZQM7TWBKE6JR5VN8YGB 같은 26자 문자열. 기능적으로 ULID와 UUIDv7은 매우 비슷합니다. 둘 다 시간 정렬되고, ms 타임스탬프 prefix가 있고, 랜덤 비트가 충분하고, 텍스트로 저장하면 lexicographic 정렬이 됩니다. 차이는 미학과 생태계 — 인코딩: ULID는 Crockford Base32(26자). UUIDv7은 hex + 하이픈(36자). ULID 문자열이 더 짧고 헷갈리는 문자(I, L, O, U)를 일부러 뺍니다. URL과 사람 가독성은 ULID가 우세. DB 지원: UUIDv7은 UUID이므로 uuid 타입을 가진 모든 DB가 16바이트 binary로 효율 저장합니다. ULID는 네이티브 타입이 없어 CHAR(26)이나 BINARY(16)으로 저장하고 경계에서 변환해야 합니다. 툴링: UUID는 표준어입니다. 모든 언어, 모든 ORM, 모든 DB에 수십 년치 UUID 도구가 있습니다. ULID는 라이브러리는 좋지만 도구 통합이 들쑥날쑥합니다. 스펙 성숙도: UUIDv7은 RFC 9562(2024년 5월, IETF 표준). ULID는 GitHub의 커뮤니티 스펙. 2026년 기본 선택은 UUIDv7입니다 — URL 미학상 ULID를 꼭 써야 할 이유가 없는 한. UUIDv7은 진짜 RFC라는 제도적 모멘텀, 점차 네이티브 DB 지원(Postgres 18+, MySQL 9+, 주요 드라이버 전부), 그리고 "UUID 그냥 써"라는 모든 코드 리뷰어와 DBA가 이미 이해하는 언어를 갖고 있습니다. 최종 사용자 노출용으로 URL-friendly한 짧은 ID(YouTube /watch?v=, Stripe cus_)가 필요하다면 UUIDv7도 ULID도 이상적이지 않습니다 — 그건 NanoID 스타일의 21자 순수 랜덤 alphanumeric 토큰이 답입니다. 다른 도구. 정렬 가능한 ID는 내부 PK로, NanoID는 외부 노출용으로 — 역할 분리하세요.
// Side-by-side
UUIDv7: 018fb1c2-7abc-7d3e-8f4a-1b2c3d4e5f60   (36 chars, hex)
ULID:   01HX1Y9ZQM7TWBKE6JR5VN8YGB              (26 chars, base32)
NanoID: V1StGXR8_Z5jdHi6B-myT                    (21 chars, alphanumeric)

// Both UUIDv7 and ULID sort by time when stored as strings:
SELECT id FROM orders ORDER BY id; -- chronological without an extra index!

// UUIDv7 Postgres setup
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- for older versions
CREATE TABLE orders (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- v4
  -- or in PG18+:  DEFAULT uuidv7()
  user_id uuid REFERENCES users(id),
  created_at timestamptz NOT NULL DEFAULT now()
);

// Generating ULID
import { ulid } from 'ulid';
ulid();                       // "01HX1Y9ZQM7TWBKE6JR5VN8YGB"
ulid(1714560000000);          // ULID for a specific timestamp
최종 수정:

도구 소개

UUID 생성기는 표준 8-4-4-4-12 hex 형식의 128비트 식별자를 만들어냅니다. v4는 완전 무작위, v1은 타임스탬프와 MAC 주소를 포함, v7(신규)은 앞부분에 Unix 밀리초 타임스탬프를 박아 ID가 시간순으로 정렬됩니다 — 인서트가 B-트리 가장 오른쪽 페이지에 몰려 데이터베이스가 좋아하는 속성입니다.

사용 방법

  1. 버전을 선택합니다: 일반 ID는 v4, DB 기본키는 v7, MAC + 시각이 필요하면 v1.
  2. 한 번에 생성할 개수를 선택합니다.
  3. Generate 버튼을 누르면 아래에 복사하기 쉬운 목록으로 표시됩니다.
  4. 단일 ID는 항목을 클릭, 일괄 복사는 Copy All을 사용합니다.
  5. 생성된 값을 마이그레이션 파일, 픽스처, 환경 변수, 테스트에 사용합니다.

주요 사용 사례

  • 증분 정수가 비즈니스 규모를 누설할 수 있는 새 DB 테이블의 기본키 생성
  • 분산 트레이싱 로그용 상관 관계 ID 생성
  • 안정적이고 유일한 ID로 테스트 픽스처 시딩
  • 안전한 API 재시도를 위한 멱등성 키 생성
  • 두 클라이언트가 충돌하지 않도록 오브젝트 스토리지 파일명 부여
  • 공유 링크의 순차 URL을 추측 어려운 식별자로 대체

자주 묻는 질문

Q. v4 대신 v7을 언제 쓰나요?

A. DB 기본키로 UUID를 저장할 때입니다. v7은 시간 순으로 정렬되어 B-트리 인서트가 한쪽에 모이고, v4가 일으키는 무작위 쓰기 비용을 피할 수 있습니다.

Q. UUID v4는 정말 유일한가요?

A. 수학적으로 수십억 개를 생성해도 충돌 확률은 사실상 0입니다 — 단, 난수원이 암호학적이어야 하며 crypto.getRandomValues가 이를 보장합니다.

Q. UUID v1은 왜 MAC 주소를 노출하나요?

A. 개인정보 개념이 정착되기 전 설계되었기 때문입니다. 최신 v1 구현은 node 필드를 무작위화하지만 프라이버시가 중요하면 v4나 v7을 권장합니다.

Q. UUID는 대소문자를 구분하나요?

A. 아닙니다. a-f와 A-F는 같은 의미입니다. 대부분 구현은 소문자로 출력하며 비교 시 동일하게 처리됩니다.