550e8400-e29b-41d4-a716-446655440000 와 01a0b72d-62fb-7a3f-9c21-5e8b0d4f6a17. 둘 다 UUID인데 성격이 꽤 다릅니다. 앞의 것은 v4라서 전부 랜덤이고, 뒤의 것은 v7이라 앞 12자리만 봐도 "2026년 9월 19일 오전 10시 0분 0.123초(KST)에 만들어졌다"는 걸 알 수 있어요. 결론부터 말하면 v4는 122비트 랜덤이라 아무 정보도 드러나지 않는 범용 ID, v7은 48비트 밀리초 타임스탬프 + 74비트 랜덤이라 생성 순서대로 정렬되는 ID예요. 충돌 확률은 둘 다 현실적으로 0이고, 고를 때의 기준은 "정렬이 필요한가, 생성 시각이 드러나도 되는가" 두 가지입니다.
이 글은 2024년에 RFC 4122를 대체한 표준 RFC 9562 기준으로, UUID 128비트가 어떻게 나뉘는지, 문자열에서 버전과 variant를 읽는 법, 충돌 확률을 직접 계산하는 법, v7의 시각을 꺼내는 법을 정리했어요. DB 기본키로 쓸 때의 성능 이야기는 분량이 많아서 UUID vs 자동증가 기본키 글로 나눴습니다.
핵심 요약
UUID는 128비트이고, 16진수 32자를 8-4-4-4-12로 끊어 씁니다. 세 번째 묶음 첫 글자가 버전, 네 번째 묶음 첫 글자(8·9·a·b)가 variant예요.
v4는 버전·variant 6비트를 뺀 122비트가 전부 랜덤이고, v7은 Unix 밀리초 48비트 뒤에 랜덤 74비트가 붙어요.
v4를 10억 개 만들어도 충돌 확률은 약 9.4×10⁻²⁰이고, 50% 확률이 되려면 약 2.7×10¹⁸개가 필요해요.
v7은 앞 12자리가 생성 시각이라 정렬에 유리하지만, 외부에 공개하면 생성 시점이 노출돼요.
UUID 128비트 구조: 8-4-4-4-12 읽는 법
UUID는 128비트(16바이트) 숫자예요. 이걸 16진수로 쓰면 32자가 되고, 사람이 읽기 쉽게 하이픈으로 8-4-4-4-12자로 나눠 총 36자로 표기합니다. 하이픈은 표기일 뿐이라 빼도 같은 값이에요.
이 중에서 버전과 관계없이 위치가 고정된 정보가 두 개 있어요.
- 버전(version): 세 번째 묶음의 첫 글자(전체 13번째 자리).
41d4의 4,7a3f의 7이 버전입니다. - variant: 네 번째 묶음의 첫 글자(17번째 자리). 표준 UUID라면 이 글자가 8, 9, a, b 중 하나예요. 이진수로 10xx라서 그렇습니다. c~d면 마이크로소프트 예약, 0~7이면 옛 NCS 예약이에요.
그래서 아무 UUID나 받았을 때 13번째 글자와 17번째 글자만 보면 "표준 UUID인지, 몇 버전인지"를 바로 판단할 수 있어요. 예를 들어 xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx 패턴에 맞으면 표준 v4입니다.
UUID 버전 한눈에 보기 (v1~v8)
RFC 9562에는 버전이 여러 개 정의돼 있지만 실제로 새로 쓸 일이 있는 건 v4와 v7 정도예요. 나머지는 왜 밀려났는지 알면 v7이 왜 나왔는지도 이해됩니다.
| 버전 | 구성 | 특징 | 지금 쓸 일 |
|---|---|---|---|
| v1 | 100나노초 타임스탬프 + 노드(MAC 주소) | 시간 비트가 뒤섞여 있어 정렬 안 됨, MAC 노출 | 레거시 (MySQL UUID()가 v1) |
| v3 / v5 | 이름 + 네임스페이스의 MD5 / SHA-1 해시 | 같은 입력이면 항상 같은 UUID | 이름에서 결정적 ID가 필요할 때 v5 |
| v4 | 122비트 랜덤 | 정보 노출 없음, 정렬 안 됨 | 범용 ID, 공개 ID, 토큰 보조 |
| v6 | v1의 시간 비트를 정렬 가능하게 재배치 | v1 호환용 | v1을 쓰던 시스템 이전용 |
| v7 | Unix 밀리초 48비트 + 랜덤 | 시간순 정렬 | DB 기본키, 로그·이벤트 ID |
| v8 | 사용자 정의 | 버전·variant만 지키면 자유 | 특수 목적 |
v1은 시간 기반이라 정렬될 것 같지만, 시간의 하위 비트가 맨 앞에 오도록 배치돼 있어서 문자열로 정렬하면 순서가 뒤죽박죽이에요. 거기에 MAC 주소까지 드러나서 개인정보 문제가 있었죠. v6가 시간 비트 순서를 고쳤고, v7은 아예 널리 쓰는 Unix 밀리초를 그대로 앞에 넣고 MAC 대신 랜덤을 붙이는 방식으로 정리한 거예요.
UUID v4: 122비트 랜덤의 의미
v4는 단순해요. 128비트를 전부 난수로 채운 다음, 버전 자리에 0100(4), variant 자리에 10을 덮어씁니다. 그래서 진짜 랜덤 비트는 128 − 4 − 2 = 122비트예요. 가능한 값의 수는 2¹²² ≈ 5.3×10³⁶개입니다.
중요한 건 그 난수가 암호학적으로 안전한 난수 생성기(CSPRNG)에서 나와야 한다는 점이에요. 브라우저와 Node.js의 crypto.randomUUID(), 파이썬의 uuid.uuid4()(내부적으로 os.urandom), PostgreSQL의 gen_random_uuid()가 다 여기에 해당해요. 반대로 Math.random()으로 직접 조립한 UUID는 시드나 내부 상태가 예측될 수 있어서 충돌 확률 계산이 성립하지 않습니다.
// 브라우저·Node.js
crypto.randomUUID(); // 'xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx'
import uuid
uuid.uuid4() # UUID('30a6ace3-9b06-445e-a4a9-207ec0f602ba')
uuid.uuid4().version # 4
UUID v7: 앞 48비트가 밀리초 시각
v7의 레이아웃은 이렇게 나뉘어요.
| 구간 | 비트 수 | 내용 |
|---|---|---|
| unix_ts_ms | 48 | 1970-01-01 UTC부터 지난 밀리초 |
| ver | 4 | 0111 (7) |
| rand_a | 12 | 랜덤 또는 같은 밀리초 안의 순번 카운터 |
| var | 2 | 10 |
| rand_b | 62 | 랜덤 |
v7은 랜덤 48비트를 시각 정보로 바꾼 대신 정렬 가능해졌어요
표로 보기
| 구분 | 타임스탬프 | 랜덤 | 버전·variant 고정 |
|---|---|---|---|
| UUID v4 | 0 | 122 | 6 |
| UUID v7 | 48 | 74 | 6 |
48비트 밀리초면 얼마나 버틸까요? 2⁴⁸밀리초는 약 8,900년이라 서기 10889년까지 표현할 수 있어요. 32비트 초 단위 Unix 시간이 2038년에 넘치는 문제와는 다른 이야기예요. 그 문제는 Unix 타임스탬프 2038년 문제에 정리했습니다.
v7에서 생성 시각 꺼내기
앞 12자리(하이픈 제외)를 16진수로 읽으면 Unix 밀리초가 나와요. 위 예시 01a0b72d-62fb-7a3f-...라면 01a0b72d62fb가 그 값입니다.
const u = '01a0b72d-62fb-7a3f-9c21-5e8b0d4f6a17';
const ms = parseInt(u.replace(/-/g, '').slice(0, 12), 16); // 1789779600123
new Date(ms).toISOString(); // '2026-09-19T01:00:00.123Z' (KST 10:00:00.123)
UUID 생성기 하단의 "UUID 분석" 칸에 붙여 넣으면 버전과 함께 이 시각을 KST로 바로 보여 줘요. v1·v6도 시간 필드를 재조립해서 생성 시각을 계산합니다. 밀리초를 사람이 읽는 날짜로 바꾸는 건 타임스탬프 변환기로도 할 수 있는데, 초와 밀리초를 헷갈리면 1970년이나 5만 년 후 날짜가 나오니 초와 밀리초 단위 버그도 함께 참고하세요.
같은 밀리초에 여러 개 만들면?
1밀리초에 UUID를 여러 개 만들면 앞 48비트가 같아져서, 뒤의 랜덤 부분 때문에 그 밀리초 안에서는 순서가 섞일 수 있어요. RFC 9562는 이걸 해결하는 방법 몇 가지를 제시하는데, 가장 흔한 게 rand_a 12비트를 카운터로 쓰는 방식이에요. 밀리초가 바뀌면 카운터를 랜덤 값으로 새로 시작하고, 같은 밀리초 안에서는 1씩 올립니다. 이 사이트의 UUID 생성기도 이 방식이라 1,000개를 한 번에 만들어도 목록 순서와 정렬 순서가 같아요.
다만 카운터 방식은 한 프로세스 안에서만 순서를 보장해요. 서버 여러 대가 같은 밀리초에 만든 v7끼리는 밀리초 단위까지만 정렬되고, 그 안에서는 순서가 랜덤입니다. 엄격한 전역 순서가 필요하면 UUID가 아니라 별도의 시퀀스를 써야 해요.
UUID 충돌 확률, 직접 계산해 보기
"UUID가 겹칠 수 있나요?"의 답은 생일 문제로 계산해요. 가능한 값이 N개일 때 n개를 뽑으면, 적어도 한 쌍이 같을 확률은 대략 p ≈ 1 − e^(−n²/2N)이고, p가 작을 때는 n²/2N으로 근사됩니다. v4는 N = 2¹²²이에요.
| 생성 개수 | 충돌 확률 (v4, 근사) |
|---|---|
| 100만 개 (10⁶) | 약 9.4×10⁻²⁶ |
| 10억 개 (10⁹) | 약 9.4×10⁻²⁰ |
| 1조 개 (10¹²) | 약 9.4×10⁻¹⁴ |
| 1,000조 개 (10¹⁵) | 약 9.4×10⁻⁸ |
| 100경 개 (10¹⁸) | 약 9% |
| 약 271경 개 (2.71×10¹⁸) | 약 50% |
50% 확률로 충돌하려면 약 2.7×10¹⁸개가 필요해요. 1초에 10억 개씩 쉬지 않고 만들어도 86년이 걸리는 양입니다. 충돌 확률을 10억분의 1(10⁻⁹) 이하로 유지하고 싶다면 약 103조 개까지 만들어도 돼요. 웬만한 서비스의 전체 데이터보다 수만 배 많은 숫자죠.
v7은 랜덤 비트가 74비트로 줄어들지만, 충돌은 같은 밀리초에 만든 것끼리만 일어날 수 있어요. 시각이 다르면 앞 48비트가 다르니까요. 같은 밀리초에 100만 개를 만든다고 해도 n²/2N = 10¹²/2⁷⁵ ≈ 2.6×10⁻¹¹ 수준입니다. 카운터를 쓰는 구현은 랜덤 비트가 조금 더 줄지만, 현실적인 생성 속도에서는 여전히 무시할 수 있는 확률이에요.
충돌보다 현실적인 위험
실제 사고는 수학적 확률이 아니라 구현에서 나요.
- 약한 난수:
Math.random()기반 직접 구현, 오래된 라이브러리. 같은 시드로 시작한 프로세스가 같은 UUID를 만들어요. - VM·컨테이너 복제: 난수 생성기 상태까지 복제된 스냅샷에서 동시에 시작하면 같은 값이 나올 수 있어요. 표준 CSPRNG는 운영체제 엔트로피를 쓰니 이 위험이 매우 작습니다.
- 잘라 쓰기: UUID를 앞 8자리만 잘라 "짧은 ID"로 쓰면 32비트라 약 7만 7천 개에서 이미 50% 확률로 충돌해요. v7은 앞부분이 시각이라 잘라 쓰면 같은 밀리초에 만든 것끼리 무조건 같아집니다.
v4와 v7, 언제 무엇을 쓸까
| 기준 | v4 | v7 |
|---|---|---|
| 정렬 | 무작위 | 생성 시간순 |
| 생성 시각 노출 | 없음 | 앞 12자리로 알 수 있음 |
| DB 기본키 인덱스 | 삽입 위치가 흩어짐 | 항상 끝에 추가, 유리 |
| 추측 가능성 | 매우 낮음 | 시각 부분은 추측 가능, 랜덤 74비트 |
| 대표 용도 | 공개 ID, 파일명, 멱등키 | 기본키, 로그·이벤트·메시지 ID |
제 기준은 이래요. 내부 저장용 기본키나 시간순으로 쌓이는 이벤트는 v7, 외부에 노출되는 ID나 생성 시점이 알려지면 곤란한 값(가입 순서, 주문 시각)은 v4. 둘 다 필요하면 내부 PK는 v7, 외부 공개 ID는 v4로 컬럼을 나누는 것도 흔한 설계예요.
그리고 한 가지 분명히 할 점이 있어요. UUID는 추측하기 어려운 식별자이지, 비밀번호나 인증 토큰을 대신하도록 설계된 값이 아니에요. 비밀 링크나 세션 토큰이 필요하면 전용 토큰 생성 방식을 쓰고, UUID에 권한 확인을 맡기지 마세요. 인코딩과 암호화를 혼동하는 실수는 Base64는 암호화가 아니다 글에서도 다뤘어요.
언어·DB별 UUID 생성 방법
| 환경 | v4 | v7 |
|---|---|---|
| 브라우저 / Node.js | crypto.randomUUID() | 라이브러리(예: uuid 패키지) 사용 |
| Python | uuid.uuid4() | 3.14부터 uuid.uuid7(), 그 전엔 라이브러리 |
| PostgreSQL | gen_random_uuid() (13 이상 기본) | 18부터 uuidv7() 기본 제공 |
| MySQL | 기본 UUID()는 v1 | 애플리케이션에서 생성해 BINARY(16)로 저장 |
| Java | UUID.randomUUID() | 외부 라이브러리 사용이 일반적 |
MySQL의 UUID() 함수가 v4가 아니라 v1이라는 점은 의외로 잘 모르는 부분이에요. 그래서 MySQL은 UUID_TO_BIN(UUID(), 1)처럼 시간 비트를 재배치하는 옵션을 따로 제공합니다. 표준 라이브러리 지원 현황은 버전마다 바뀌니, 쓰는 런타임 버전의 공식 문서를 한 번 확인하세요.
자주 묻는 질문
UUID 문자열에서 버전은 어떻게 확인하나요?
하이픈으로 나눈 세 번째 묶음의 첫 글자가 버전이에요. 550e8400-e29b-41d4-...이면 v4, 01a0b72d-62fb-7a3f-...이면 v7입니다. 네 번째 묶음 첫 글자가 8·9·a·b면 표준(RFC 9562) variant예요.
UUID v4는 정말 겹치지 않나요?
수학적으로 0은 아니지만, 10억 개를 만들어도 충돌 확률이 약 10⁻¹⁹ 수준이라 현실적으로 0으로 봐도 돼요. 다만 암호학적으로 안전한 난수 생성기를 쓰는 표준 구현이어야 이 계산이 성립합니다.
UUID v7은 보안상 문제가 없나요?
앞 48비트가 생성 시각이라 언제 만들어졌는지가 드러나요. 가입 시각이나 주문 순서가 노출되면 안 되는 곳에서는 외부 공개용으로 v4를 쓰는 게 안전합니다. 나머지 74비트는 랜덤이라 시각을 알아도 전체 값을 맞히긴 어려워요.
UUID와 GUID는 같은 건가요?
같은 128비트 식별자예요. 마이크로소프트 생태계에서 GUID라고 부르고, 중괄호로 감싼 대문자 표기({…})를 자주 씁니다. 대소문자와 중괄호는 표기 차이일 뿐 값은 같아요.
v4를 v7로 바꾸면 기존 데이터는 어떻게 하나요?
기존 v4 값은 그대로 두고 새로 만드는 값만 v7로 바꿔도 돼요. 두 버전 모두 같은 128비트 UUID 타입에 저장되고 서로 충돌하지 않아요. 다만 기존 구간은 여전히 정렬되지 않습니다.
정리
UUID는 128비트이고, 13번째 글자가 버전, 17번째 글자가 variant예요. v4는 122비트 랜덤이라 아무 정보도 담지 않고, v7은 앞 48비트에 밀리초 시각을 넣어 시간순으로 정렬됩니다. 충돌 확률은 둘 다 실무에서 걱정할 수준이 아니고, 진짜 선택 기준은 정렬 필요 여부와 생성 시각 노출 여부예요.
UUID 생성기에서 v4와 v7을 최대 1,000개까지 만들어 보고, 분석 칸에 넣어 버전과 생성 시각을 확인해 보세요. DB 기본키로 쓸지 고민 중이라면 UUID vs 자동증가 정수 기본키로 이어서 보시면 됩니다.