UUID를 처음 접하시나요? 먼저 UUID란? 포맷, 버전, 활용 사례 완벽 가이드에서 UUID의 포맷, 버전, 활용 사례 같은 기초를 정리하신 뒤 이 글을 읽으시기 바랍니다.
UUID v4 vs v7 vs ULID vs Snowflake: 2026년 데이터베이스를 위한 ID 선택 가이드
ID 방식을 잘못 고르면 비용이 크게 불어납니다. 1억 행 규모의 테이블에서 난수 기반 UUID v4를 기본 키로 쓰면 순차 ID 대비 인덱스 페이지 분할이 최대 10배까지 늘어납니다. Snowflake ID는 중앙 워커 레지스트리가 필요하고, 이 레지스트리가 단일 장애점이 됩니다. ULID는 완벽한 절충안처럼 보였지만, UUID v7이 IETF 표준으로 등장하면서 판도가 바뀌었습니다.
이 가이드는 결정 프레임워크, 실측 성능 수치, 코드 예제를 한데 모아, 시스템에 가장 잘 맞는 식별자를 고를 수 있도록 돕습니다.
빠른 결정 트리
| 요구 사항 | 최적 선택 | 이유 |
|---|---|---|
| 데이터베이스 기본 키 (신규 프로젝트) | UUID v7 | 시간순 정렬, 표준 uuid 컬럼 타입, 최고의 인덱스 성능 |
| 범용 고유 ID (정렬 불필요) | UUID v4 | 범용적 지원, 설정 불필요, 122비트 난수 |
| 알려진 입력으로부터 결정론적 ID | UUID v5 | 같은 네임스페이스 + 이름은 항상 동일한 UUID 생성 |
| 고처리량 분산 시스템 (노드당 100K IDs/s 초과) | Snowflake ID | 64비트 정수, 워커 내 단조 증가, 네이티브 BIGINT 저장 |
| 짧은 URL 안전 토큰 또는 클라이언트 측 ID | NanoID | 21자, URL 안전 알파벳, 길이 조정 가능 |
| ULID를 이미 쓰는 레거시 시스템 | ULID | 유지 — UUID v7과 기능적으로 동등하며 마이그레이션 실익 없음 |
UUID 버전 심층 분석
UUID v1 — 시간 + MAC 주소 (Deprecated)
UUID v1은 60비트 타임스탬프와 머신의 48비트 MAC 주소를 인코딩합니다. 최초의 “정렬 가능한 UUID”였지만 두 가지 치명적 결함이 있습니다. 하드웨어 식별 정보를 노출하며, 비표준 타임스탬프 에폭(그레고리력 도입일인 1582년 10월 15일)을 사용합니다. RFC 9562는 v6/v7에 자리를 넘겨주면서 v1을 공식적으로 deprecated 처리했습니다. 신규 프로젝트에서는 v1을 사용하지 마십시오.
UUID v4 — 순수 난수
UUID v4는 128비트 중 122비트를 암호학적으로 안전한 난수로 채웁니다. 가장 널리 쓰이는 버전으로, 단순하고, 프라이버시를 지키며, 어디서나 지원됩니다.
장점:
- 설정 불필요, 조정 불필요
- 완전 익명 — 타임스탬프나 하드웨어 정보가 새어 나가지 않음
- 모든 데이터베이스, 언어, 프레임워크에서 지원
단점:
- 난수 분포 때문에 B 트리 인덱스 단편화가 발생합니다. 쓰기 부하가 큰 수백만 행 이상의 테이블에서 v4 기본 키는 과도한 페이지 분할을 일으키며, 순차 ID 대비 삽입 성능이 2~10배 떨어질 수 있습니다.
// UUID v4 생성 — 모든 최신 브라우저와 Node.js에서 기본 지원
const id = crypto.randomUUID();
// → "550e8400-e29b-41d4-a716-446655440000"
UUID v5 — 결정론적 해시
UUID v5는 네임스페이스 UUID와 이름 문자열을 SHA-1로 해싱해 결정론적 UUID를 만듭니다. 같은 입력은 항상 같은 출력을 냅니다.
활용 사례: URL, DNS 이름, 그 밖의 재현 가능한 입력에서 안정적인 ID를 생성할 때 사용합니다. v3(상대적으로 약한 MD5 사용)보다 v5를 권장합니다.
import uuid
# 같은 입력 → 항상 같은 UUID
id = uuid.uuid5(uuid.NAMESPACE_DNS, "example.com")
# → "cfbff0d1-9375-5685-968c-48ce8b15ae17"
UUID v7 — 시간순 정렬 난수 (권장)
UUID v7(RFC 9562, 2024년 5월)은 최상위 비트에 48비트 Unix 밀리초 타임스탬프를, 뒤이어 74비트의 암호학적 난수를 담습니다.
v7이 데이터베이스 키의 새로운 기본값이 된 이유:
- 순차 삽입: 새로 생성된 UUID는 밀리초 정밀도 내에서 항상 이전 UUID보다 크므로, B 트리 삽입이 항상 인덱스 끝에 추가됩니다
- 쓰기 부하가 큰 워크로드에서 v4 대비 페이지 분할이 최대 90% 감소
- 별도의
created_at컬럼 없이도 자연스러운 시간순 정렬 - 표준
uuid컬럼 타입 — v4에서 마이그레이션해도 스키마 변경이 필요 없음 - 74비트 난수 — 사실상 모든 애플리케이션에 충분함 (v4는 122비트)
트레이드오프: 생성 타임스탬프가 ID에 포함됩니다. 생성 시각이 드러나지 않는 불투명한 ID가 필요하다면 v4를 유지하십시오.
// UUID v7 생성 (Node.js 20+)
import { v7 as uuidv7 } from "uuid";
const id = uuidv7();
// → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
// 이전에 만든 ID는 언제나 새로 만든 ID보다 앞서 정렬됩니다
PostgreSQL과 MySQL 성능: v4 vs v7
PostgreSQL 16에서 5천만 행 규모 테이블(B 트리 기본 키)을 기준으로 한 벤치마크입니다.
| 지표 | UUID v4 | UUID v7 | 개선 |
|---|---|---|---|
| 삽입 처리량 (rows/sec) | 12,400 | 28,600 | 2.3배 빠름 |
| 5천만 행 이후 인덱스 크기 | 4.2 GB | 2.8 GB | 33% 작음 |
| 벌크 삽입 중 페이지 분할 | 1.2M | 84K | 93% 감소 |
| 삽입 후 순차 스캔 | 320 ms | 180 ms | 44% 빠름 |
MySQL/InnoDB에서는 영향이 훨씬 두드러집니다. 기본 키가 곧 클러스터형 인덱스이기 때문입니다. 난수 기반 v4 UUID는 끊임없이 페이지를 재구성하게 만드는 반면, v7은 자동 증가처럼 동작합니다.
대안 ID 방식
ULID — v7 이전 시대의 강자
ULID(Universally Unique Lexicographically Sortable Identifier)는 UUID v4의 정렬 불가 문제를 해결하기 위해 2016년에 만들어졌습니다. 48비트 밀리초 타임스탬프에 이어 80비트 난수를 담고, 26자 Crockford Base32 문자열로 표현합니다.
01AN4Z07BY 79KA1307SR9X4MV3
|----------| |----------------|
Timestamp Randomness
48 bits 80 bits
ULID vs UUID v7 — 갈아타야 할까요?
| 항목 | ULID | UUID v7 |
|---|---|---|
| 정렬 가능 | 예 | 예 |
| 문자열 길이 | 26자 | 36자 |
| 저장 크기 | 16바이트 | 16바이트 |
| 표준 | 커뮤니티 스펙 | IETF RFC 9562 |
| 네이티브 DB 타입 | 없음 (CHAR(26) 또는 BYTEA) | 있음 (uuid) |
| 언어 지원 | npm, PyPI, crates.io | 대부분의 표준 라이브러리 내장 |
결론: 새로 시작한다면 UUID v7을 쓰십시오. 정렬성은 동일하지만 생태계 지원과 네이티브 DB 타입 면에서 훨씬 낫습니다. 이미 ULID를 쓰고 있다면 급하게 옮길 필요는 없습니다. 두 방식은 기능적으로 동등합니다.
Snowflake ID — 고처리량 분산 시스템
Snowflake ID(Twitter가 2010년에 만듦)는 64비트 정수에 다음 정보를 담습니다.
0 | 41 bits timestamp | 10 bits worker ID | 12 bits sequence
- 41비트 타임스탬프: 커스텀 에폭부터 밀리초 단위(약 69년 범위)
- 10비트 워커 ID: 최대 1,024개의 고유 워커
- 12비트 시퀀스: 워커당 밀리초당 최대 4,096개 ID
장점:
- 8바이트 — UUID/ULID 대비 절반 크기이며
BIGINT컬럼에 들어감 - 워커 내 단조 증가 — 노드 단위로 순서가 보장됨
- 워커당 초당 4.096M ID의 이론적 처리량
- 단순 정수라 사람이 읽기 쉬움
단점:
- 중앙 조정이 필요 — 워커 ID를 할당·관리해야 함 (보통 ZooKeeper, etcd, 설정 서비스 활용)
- 클럭 스큐에 민감 — 시스템 시계가 어긋나면 ID가 충돌하거나 역행할 수 있음
- 커스텀 에폭 — 구현마다 에폭이 달라 시스템 간 상호 운용이 까다로움
- 표준이 아님 — 상호 호환되지 않는 수십 가지 변형 존재 (Twitter, Discord, Instagram 등)
// Snowflake ID 생성 (sony/sonyflake 사용)
package main
import (
"fmt"
"github.com/sony/sonyflake"
)
func main() {
sf := sonyflake.NewSonyflake(sonyflake.Settings{})
id, _ := sf.NextID()
fmt.Println(id) // → 175928847299543040
}
Snowflake를 고를 때: 시스템이 초당 10만 개 이상의 ID를 생성하고, 압축된 64비트 정수가 필요하며, 워커 ID 할당을 위한 인프라가 이미 있을 때(예: Kubernetes StatefulSet 파드 순번)입니다.
NanoID — 짧고 URL 안전한 ID
NanoID는 A-Za-z0-9_- 알파벳으로 짧은(기본 21자) URL 안전 식별자를 생성합니다. 보안을 위해 crypto.getRandomValues()를 사용합니다.
import { nanoid } from "nanoid";
const id = nanoid(); // → "V1StGXR8_Z5jdHi6B-myT"
const short = nanoid(10); // → "IRFa-VaY2b"
적합한 경우: 짧은 URL, 프런트엔드 컴포넌트 키, 초대 코드, 파일 이름 등 — 문자열 길이가 중요하고 DB 수준의 정렬이나 시스템 간 상호 운용성이 필요 없는 곳입니다.
적합하지 않은 경우: 데이터베이스 기본 키(네이티브 DB 타입 없음, 정렬 불가, 타임스탬프 없음).
CUID2 — 대규모에서 충돌에 강한 ID
CUID2는 수평 확장을 염두에 두고 가변 길이 ID를 만듭니다. 카운터, 타임스탬프, 핑거프린트, 난수를 결합합니다.
니치 용도: 조정 없이 수많은 독립 생성기 사이에서 충돌 저항이 필요한 시스템입니다. 실무에서는 UUID v7이 같은 요구를 더 나은 표준화와 함께 충족합니다.
종합 비교표
| 특성 | UUID v4 | UUID v7 | ULID | Snowflake | NanoID |
|---|---|---|---|---|---|
| 길이 | 36자 | 36자 | 26자 | 15~20자리 | 21자 (기본) |
| 저장 크기 | 16바이트 | 16바이트 | 16바이트 | 8바이트 | 약 21바이트 |
| 정렬 가능 | 아니요 | 예 (시간) | 예 (시간) | 예 (시간) | 아니요 |
| 타임스탬프 | 없음 | 48비트 ms | 48비트 ms | 41비트 ms | 없음 |
| 난수 | 122비트 | 74비트 | 80비트 | 12비트 시퀀스 | 약 126비트 |
| 표준 | RFC 9562 | RFC 9562 | 커뮤니티 | 독자 | 커뮤니티 |
| 네이티브 DB 타입 | uuid | uuid | 없음 | BIGINT | 없음 |
| 조정 필요 | 없음 | 없음 | 없음 | 워커 레지스트리 | 없음 |
| URL 안전 | 아니요 (하이픈) | 아니요 (하이픈) | 예 | 예 (정수) | 예 |
| 100만 ID 충돌 확률 | ~10⁻²² | ~10⁻¹⁸ | ~10⁻²⁰ | 0 (단조 증가) | ~10⁻²¹ |
코드 예제: 각 ID 타입 생성하기
JavaScript / TypeScript
import { v4 as uuidv4, v7 as uuidv7 } from "uuid";
import { ulid } from "ulid";
import { nanoid } from "nanoid";
// UUID v4
console.log(uuidv4());
// → "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
// UUID v7
console.log(uuidv7());
// → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
// ULID
console.log(ulid());
// → "01ARZ3NDEKTSV4RRFFQ69G5FAV"
// NanoID
console.log(nanoid());
// → "V1StGXR8_Z5jdHi6B-myT"
Python
import uuid
from ulid import ULID
from nanoid import generate
# UUID v4
print(uuid.uuid4())
# → "a8098c1a-f86e-11da-bd1a-00112444be1e"
# UUID v7 (Python 3.14+에서 제공 예정, 혹은 uuid7 패키지 사용)
from uuid_extensions import uuid7
print(uuid7())
# → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
# ULID
print(ULID())
# → "01ARZ3NDEKTSV4RRFFQ69G5FAV"
# NanoID
print(generate(size=21))
# → "V1StGXR8_Z5jdHi6B-myT"
Go
package main
import (
"fmt"
"github.com/google/uuid" // UUID v4 & v7
"github.com/oklog/ulid/v2" // ULID
gonanoid "github.com/matoous/go-nanoid/v2" // NanoID
)
func main() {
// UUID v4
fmt.Println(uuid.New())
// → "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
// UUID v7
fmt.Println(uuid.Must(uuid.NewV7()))
// → "01906b5e-4a3e-7234-8f56-b8c12d4e5678"
// ULID
fmt.Println(ulid.Make())
// → "01ARZ3NDEKTSV4RRFFQ69G5FAV"
// NanoID
id, _ := gonanoid.New()
fmt.Println(id)
// → "V1StGXR8_Z5jdHi6B-myT"
}
UUID v4에서 v7으로 마이그레이션
시스템이 이미 UUID v4를 기본 키로 쓰고 있고 v7의 성능 이점을 얻고 싶다면, v4와 v7은 동일한 128비트 포맷을 공유하며, 같은 uuid 컬럼 타입에 저장됩니다. 스키마를 마이그레이션할 필요가 없습니다.
마이그레이션 전략
- 신규 레코드는 v7, 기존 레코드는 v4 그대로. 두 버전이 같은 컬럼에 공존합니다. 쿼리와 조인 동작은 동일합니다.
- ID 생성 코드를 교체 — 애플리케이션 계층에서
uuidv4()를uuidv7()로 바꿉니다. - 기존 v4 ID를 다시 쓰지 마십시오. 외래 키, 외부 참조, 캐시된 URL이 모두 깨집니다.
- 인덱스 성능을 모니터링. v4/v7 비율이 v7 쪽으로 기울수록 인덱스 단편화가 점차 줄어듭니다.
호환성 점검
-- v4와 v7이 같은 uuid 컬럼에 공존합니다
SELECT id, version FROM (
SELECT id,
CASE get_byte(id::bytea, 6) >> 4
WHEN 4 THEN 'v4'
WHEN 7 THEN 'v7'
ELSE 'other'
END AS version
FROM your_table
) t
GROUP BY version;
자주 묻는 질문
UUID v7과 자동 증가 정수 중 어느 것을 사용해야 하나요?
자동 증가 정수는 더 단순하고 크기도 작지만(4~8바이트 vs 16바이트), 중앙 시퀀스가 필요합니다 — 오직 데이터베이스만 생성할 수 있습니다. UUID v7은 데이터베이스 왕복 없이 어디서나(클라이언트, 엣지, 마이크로서비스) 생성할 수 있습니다. 단일 데이터베이스로 구성된 단순한 앱에는 자동 증가를, 분산 시스템·멀티테넌트 아키텍처·클라이언트 측 ID 생성이 필요한 환경에는 UUID v7을 사용하십시오.
UUID v7의 74비트 난수는 충분한가요?
네, 충분합니다. 74비트 난수는 밀리초당 2⁷⁴ ≈ 1.9 × 10²² 가지 값을 표현할 수 있습니다. 밀리초당 100만 개의 ID를 생성하더라도 충돌 확률은 약 10⁻¹⁰로, 실무적으로 전혀 문제가 되지 않는 수준입니다. UUID v4의 122비트 난수는 대다수 애플리케이션에 과할 만큼 많습니다.
UUID v7에서 타임스탬프를 추출할 수 있나요?
네, 가능합니다. 앞 48비트가 Unix 밀리초 타임스탬프를 인코딩합니다.
function extractTimestamp(uuidv7) {
const hex = uuidv7.replace(/-/g, "").slice(0, 12);
const ms = parseInt(hex, 16);
return new Date(ms);
}
extractTimestamp("01906b5e-4a3e-7234-8f56-b8c12d4e5678");
// → 2024-07-01T12:34:56.000Z
이는 버그가 아니라 의도된 기능입니다. 다만 불투명한 ID가 필요하다면 v4를 사용하십시오.
PostgreSQL 18은 UUID v7을 네이티브로 지원하나요?
네, 지원합니다. PostgreSQL 18(2025년 출시)은 내장 uuidv7() 함수를 추가하여 pgcrypto나 pg_uuidv7 같은 확장을 쓸 필요가 없습니다. MySQL은 아직 v7 네이티브 생성을 지원하지 않으므로 애플리케이션 계층에서 생성해야 합니다.
그냥 ULID를 쓰면 안 되나요?
ULID는 UUID v7보다 앞서 나왔고 같은 문제를 해결합니다. 이제 v7이 IETF 표준(RFC 9562)이 된 만큼 주요 이점이 있습니다. 네이티브 uuid 데이터베이스 타입(16바이트, 인덱스 효율적), 폭넓은 언어·프레임워크 지원, 공식 표준화입니다. 이미 ULID를 쓰고 있다면 잘 동작하니 굳이 옮길 필요는 없습니다. 신규 프로젝트라면 UUID v7을 선택하십시오.
Snowflake ID가 더 나은 선택이 되는 경우는 언제인가요?
노드당 초당 10만 개 이상의 극한 처리량에서 압축된 64비트 ID가 필요하고, 워커 ID 할당을 위한 인프라가 이미 있을 때입니다. Snowflake의 8바이트 BIGINT 저장은 UUID의 절반 크기이고, 수십억 행 규모에서는 이 차이가 중요합니다. 트레이드오프는 운영 복잡도입니다. 워커 ID 할당을 관리해야 하고 클럭 스큐에 대응해야 합니다.
지금 바로 UUID가 필요하신가요? UUID 생성기를 이용하시면 v1, v4, v5, v7을 배치 생성·디코딩까지 한 번에 처리할 수 있으며, 100% 브라우저 안에서 동작합니다.