UUID v4 대 v7 — 차이점과 각각 언제 사용해야 하는지
UUID v4는 10년 넘게 분산 기본 키의 기본 선택이었습니다. 무작위 128비트 값을 생성하고, 8개의 16진수 그룹으로 포맷하고, 완료. 조정이 필요 없고, 실용적으로 충돌 확률이 0이며, 어디서나 작동합니다. 그렇다면 왜 2024년 RFC 9562에 표준화된 UUID v7이 2026년 프로덕션 시스템 전반에 빠르게 확산되고 있을까요?
짧은 답: 데이터베이스 성능. UUID v4는 B-트리 인덱스 지역성을 파괴합니다. UUID v7은 개발자가 UUID에 대해 좋아하는 모든 것을 유지하면서 이를 수정합니다. 이 문서는 각 버전이 실제로 무엇인지, 차이가 규모에서 왜 중요한지, 각각 언제 선택해야 하는지, 그리고 다운타임 없이 마이그레이션하는 방법을 설명합니다.
지금 UUID를 생성해야 합니까? Toova UUID 생성기를 사용해보세요. v4와 v7을 대량으로 지원합니다. 다른 무작위 식별자의 경우 랜덤 문자열 생성기와 비밀번호 생성기가 더 짧은 토큰을 다룹니다.
UUID v4 — 순수 무작위성
UUID 버전 4는 암호학적으로 안전한 의사 난수 생성기(CSPRNG)를 사용하여 128비트 중 122비트를 채웁니다. 나머지 6비트는 고정됩니다: 4비트는 버전(0100)을 인코딩하고 2비트는 변형(10)을 인코딩합니다. 결과는 다음과 같습니다:
f47ac10b-58cc-4372-a567-0e02b2c3d479
^^^^
버전 = 4 (무작위) 무작위성이 기능입니다. 두 독립 시스템은 조정 없이 UUID를 생성할 수 있고 절대 충돌하지 않습니다 — 2.71 quadrillion v4 UUID 세트에서 충돌 확률은 약 50%로, 이는 모든 실용적인 애플리케이션에 대해 위험이 무시할 만하다는 것을 의미합니다. 중앙 집중식 ID 서버, 데이터베이스 시퀀스 또는 분산 잠금이 필요하지 않습니다.
v4가 생성되는 방법
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
// => 'f47ac10b-58cc-4372-a567-0e02b2c3d479' uuid 라이브러리(JavaScript), Python의 uuid.uuid4(), Go의 google/uuid 및 모든 주요 언어 런타임은 OS CSPRNG로 위임합니다 — Linux의 /dev/urandom, Windows의 CryptGenRandom. 생성 비용은 사실상 0입니다.
문제: 무작위 삽입이 B-트리 인덱스를 죽임
B-트리 인덱스는 데이터를 정렬된 상태로 유지합니다. 새 행을 삽입할 때 데이터베이스는 새 키가 정렬 순서에서 어디에 맞는지 찾고 거기에 배치합니다. 모든 새 키가 무작위라면 인덱스에서 무작위 위치에 떨어집니다 — 이는 모든 삽입이 디스크에서 버퍼 풀로 다른 페이지를 로드해야 한다는 것을 의미합니다. 낮은 볼륨에서는 이것이 보이지 않습니다. 높은 볼륨(수백만 행, 높은 INSERT 비율)에서는 인덱스 단편화라는 패턴을 만듭니다: 각 삽입이 다른 위치에 도달하기 때문에 인덱스가 반쯤 비어 있는 페이지로 채워지고, hot 작업 세트가 예측 가능한 최근 슬라이스가 아닌 전체 인덱스에 걸쳐 있기 때문에 버퍼 풀이 끊임없이 churn합니다.
프로덕션의 증상: INSERT 지연 시간이 증가하고, autovacuum 오버헤드가 상승하며(PostgreSQL), 체크포인트 압력이 증가하고, 최근 레코드에 대한 읽기 성능이 저하됩니다. "최근"이 더 이상 인덱스의 어떤 지역성에도 매핑되지 않기 때문입니다. 이는 가설적이지 않습니다 — UUID v4 기본 키가 있는 큰 PostgreSQL과 MySQL 테이블에서 잘 문서화된 고통 지점입니다.
UUID v7 — 시간 순서 무작위성
UUID v7은 B-트리 단편화 문제를 해결하기 위해 명시적으로 설계되었습니다. 가장 중요한 비트에 48비트 Unix 밀리초 타임스탬프를 인코딩하고, 그 뒤에 버전 비트, 12개의 무작위 비트, 변형 비트 및 62개의 추가 무작위 비트가 옵니다. 총 무작위성: 74비트 — 여전히 충돌을 방지하는 데 필요한 것보다 훨씬 더 많습니다.
018f4e6b-a23c-7d45-9abc-0e02b2c3d479
^^^^^^^^^^^^^^
48비트 Unix 타임스탬프 (ms 정밀도)
^
버전 = 7 타임스탬프가 상위 비트를 차지하기 때문에 나중에 생성된 UUID는 이전에 생성된 UUID 뒤에 정렬됩니다. 삽입은 항상 인덱스의 오른쪽 가장자리에 추가됩니다. 데이터베이스는 버퍼 풀에서 전체 인덱스가 아닌 가장 최근의 인덱스 페이지만 hot한 상태로 유지하면 됩니다. 높은 INSERT 비율에서 이것만으로도 쓰기 지연 시간을 30–60% 줄이고 수천만 행이 있는 테이블에서 I/O를 자릿수만큼 줄일 수 있습니다.
v7이 생성되는 방법
import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();
// => '018f4e6b-a23c-7d45-9abc-0e02b2c3d479'
동일한 uuid 라이브러리가 버전 10에서 v7 지원을 추가했습니다. Python의 표준 라이브러리가 3.14에서 추가했습니다. PostgreSQL 17은 uuidv7()을 내장 함수로 포함합니다. 이전 스택을 사용한다면 여러 작은 라이브러리가 종속성 없이 v7 생성을 제공합니다.
밀리초 미만 단조성
두 v7 UUID가 같은 밀리초 내에 생성되면 어떻게 됩니까? RFC 9562는 구현이 같은 밀리초 내에서 순서를 보장하기 위해 무작위 비트에 단조 카운터를 증가시키도록 허용합니다. uuid 라이브러리는 기본적으로 이를 수행합니다. 결과: 10,000개의 ID가 1밀리초에 생성되더라도 여전히 올바르게 정렬됩니다.
B-트리 인덱스 단편화 문제 상세
이것이 왜 중요한지 이해하려면 1억 행이 있는 테이블에서 UUID v4 기본 키로 초당 10,000 삽입 시 무엇이 발생하는지 생각해보세요:
- 각 삽입은 1억 개의 기존 항목 사이에서 무작위 위치에 떨어지는 무작위 128비트 키를 생성합니다.
- 데이터베이스는 그 위치를 포함하는 특정 B-트리 페이지를 버퍼 풀로 로드해야 합니다.
- 1억 행과 8 KB 페이지로 인덱스는 약 100,000 페이지에 걸쳐 있습니다. 매초 10,000개의 다른 페이지가 필요합니다 — 일반적인 8 GB shared_buffers가 이 인덱스만을 위해 보유할 수 있는 것보다 훨씬 많습니다.
- 모든 캐시 미스는 디스크 읽기를 초래합니다. 초당 10,000 삽입 시 이는 초당 수천 개의 무작위 디스크 읽기를 생성할 수 있으며, 큰 테이블에서 SSD조차 포화시킵니다.
UUID v7로는 초당 10,000 삽입 모두가 가장 오른쪽 리프 페이지(또는 최근 페이지의 작은 한 줌)에 떨어집니다. 버퍼 풀은 그 몇 페이지만 hot한 상태로 유지하면 됩니다. 쓰기에 대한 캐시 히트율은 100%에 접근합니다. 쓰기 증폭이 극적으로 감소합니다.
동일한 이점이 범위 스캔에 적용됩니다: v4 테이블의 WHERE created_at BETWEEN x AND y는 전체 인덱스 스캔이나 별도의 타임스탬프 인덱스가 필요합니다. v7 테이블에서 기본 키 자체가 타임스탬프 인덱스입니다 — 쿼리는 올바른 범위로 직접 시크할 수 있습니다.
PostgreSQL에서 UUID v7을 사용하는 방법
-- PostgreSQL 16+에서 UUID 타입과 네이티브로 작동
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- v4 (무작위)
-- 확장 또는 애플리케이션 측 생성을 통해 UUIDv7로 전환
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- UUIDv7로: 타임스탬프가 이미 임베드되어 있으므로
-- 이 별도의 created_at 컬럼은 종종 중복됩니다.
PostgreSQL 17은 uuidv7()을 네이티브로 제공합니다. PostgreSQL 14–16의 경우 pg_uuidv7 확장이 동일한 함수를 제공합니다. UUID 데이터 타입은 v4와 v7을 동일하게 저장합니다 — 16바이트, 오버헤드 없음. 유일한 차이는 정렬 순서를 결정하는 비트 패턴입니다.
유용한 결과 하나: 타임스탬프가 임베드되어 있기 때문에 많은 테이블이 더 이상 순서 지정이나 표시를 위한 별도의 created_at 컬럼이 필요하지 않습니다. 단일 함수 호출로 v7 UUID에서 타임스탬프를 추출할 수 있습니다. 이는 스키마 복잡성을 줄이고 삽입당 쓰기를 제거합니다.
UUID v4 대 v7 — 트레이드오프
| 속성 | UUID v4 | UUID v7 |
|---|---|---|
| 무작위성 비트 | 122 | 74 |
| 정렬 순서 | 무작위 | 시간순 |
| B-트리 INSERT 성능 | 나쁨 (단편화) | 탁월함 (순차적) |
| 타임스탬프 유출 | 없음 | 예 (ms 정밀도) |
| RFC 표준 | RFC 4122 (2005), RFC 9562 (2024) | RFC 9562 (2024) |
| 라이브러리 지원 | 보편적 | 빠르게 성장 중 (2024–2026) |
| 임베드된 created_at | 아니오 | 예 |
마이그레이션 가이드 — v4에서 v7로
기존 테이블을 UUID v4 기본 키에서 v7으로 마이그레이션하는 것은 다단계 작업입니다. 핵심 제약: 모든 참조 테이블도 업데이트하지 않고는 외래 키에 의해 참조되는 기본 키의 값을 변경할 수 없습니다. 유지 보수 창을 계획하거나 이중 쓰기 접근 방식을 사용하세요.
-- 1. 새 컬럼 추가
ALTER TABLE orders ADD COLUMN id_v7 UUID;
-- 2. 기존 행 백필 (원래 created_at를
-- 타임스탬프 소스로 보존; 앱에서 UUIDv7 라이브러리 함수 사용)
UPDATE orders SET id_v7 = generate_uuidv7(created_at);
-- 3. NULL이 남아있지 않은지 확인
SELECT COUNT(*) FROM orders WHERE id_v7 IS NULL;
-- 4. 컬럼 스왑 (짧은 유지 보수 창 필요)
ALTER TABLE orders ALTER COLUMN id_v7 SET NOT NULL;
ALTER TABLE orders ALTER COLUMN id_v7 SET DEFAULT generate_uuidv7(now());
ALTER TABLE orders RENAME COLUMN id TO id_v4_old;
ALTER TABLE orders RENAME COLUMN id_v7 TO id;
ALTER TABLE orders ADD PRIMARY KEY (id); 트래픽이 많은 테이블에서 다운타임 0 마이그레이션의 권장 접근 방식은:
- 앞으로 v7 UUID를 생성하는 서버 기본값으로 새
id_v7컬럼을 추가합니다. - v7 타임스탬프 부분의 시드로 기존
created_at타임스탬프를 사용하여 트래픽이 적은 기간 동안 이전 행을 배치로 백필합니다. - 참조 테이블의 모든 외래 키 컬럼을
id_v7을 가리키도록 업데이트합니다. - 짧은 유지 보수 창 동안 컬럼 이름을 변경하고 이전 기본 키 제약 조건을 삭제합니다.
백필 단계가 가장 긴 단계입니다. 배치당 10,000행과 배치 사이의 50 ms 슬립으로 1억 행 테이블은 약 8시간이 걸립니다. 일찍 시작하세요.
실제 영향
여러 엔지니어링 팀이 프로덕션 크기 데이터셋에서 UUID v4와 v7을 비교한 벤치마크를 게시했습니다. 일관된 결과:
- INSERT 처리량: v4에서 v7으로 전환할 때 50M+ 행이 있는 테이블에서 2–5배 향상되며, 이득은 테이블 크기가 증가함에 따라 증가합니다.
- 쓰기 지연 시간 p99: 동일한 하드웨어에서 수백 밀리초(v4, 부하 하)에서 한 자릿수 밀리초(v7)로 감소합니다.
- 인덱스 크기: 동일한 데이터에 대한 동등한 v4 인덱스보다 v7 컬럼의 B-트리 인덱스가 15–30% 작습니다. 단편화가 반쯤 비어 있는 페이지를 덜 남기기 때문입니다.
- 버퍼 풀 효율성: 기본 키 인덱스에 대한 shared buffers 히트율이 ~40%(v4, 큰 테이블)에서 ~99%(v7)로 갑니다. 최근 페이지만 hot한 상태로 유지하면 되기 때문입니다.
이득은 약 100만 행 미만에서 무시할 만합니다. 테이블이 작게 유지되면 단순성을 위해 v4를 고수하세요. 의미 있는 INSERT 비율이 있는 1천만 행 이상에서 v7이 더 나은 기본값입니다.
각각 언제 사용해야 합니까
UUID v7을 사용할 때:
- 새 스키마를 설계하고 테이블이 크게 성장할 때(10M+ 행).
- 테이블의 INSERT 비율이 높을 때 — 이벤트, 로그, 주문, 메시지, 알림.
- 생성 타임스탬프 역할도 하는 기본 키를 원할 때(컬럼 제거).
- PostgreSQL 17, MySQL 8.0+ 또는 v7 생성을 지원하는 현대 ORM에 있을 때.
- ID로 정렬하는 것이 생성 시간으로 정렬하는 것과 의미적으로 동등할 때 — 대부분의 append-heavy 테이블의 경우 그렇습니다.
UUID v4를 사용할 때:
- 식별자가 사용자에게 노출되고 생성 시간을 드러내서는 안 될 때(초대 코드, 공유 링크, 청구 핸들).
- 테이블이 작고 안정적일 때 — 성능 이점이 마이그레이션 비용을 정당화하지 않습니다.
- 생성 타임스탬프가 민감한 컨텍스트에서 ID를 생성할 때(성장 외관에 경쟁하는 제품의 비공개 사용자 레코드).
- 일회성 자격 증명이나 토큰으로 무언가가 필요할 때 — 어떤 UUID 버전도 아닌 전용 시크릿 생성기를 사용하세요.
요약
UUID v4는 무작위이고, 비공개이며, 보편적으로 지원됩니다. UUID v7은 시간 순서이고, 데이터베이스 친화적이며, 이제 주요 라이브러리와 데이터베이스에서 표준입니다. 크거나 빠르게 성장하는 테이블이 있는 새 스키마의 경우 v7이 더 나은 기본값입니다. 타임스탬프 유출이 우려되는 사용자에게 노출되는 ID의 경우 v4가 여전히 올바른 선택입니다.
Toova UUID 생성기로 두 버전을 즉시 생성하세요 — 계정 필요 없음. 더 짧은 토큰의 경우 랜덤 문자열 생성기가 어떤 길이로든 영숫자 ID를 생성합니다.