v4가 맞는 경우
공개 URL에 노출되는 식별자, 비밀 토큰에 가까운 값, 그리고 정렬이 아무 의미 없는 데이터입니다. 예측 불가능성 자체가 요구사항이면 v4가 정답이고 바꿀 이유가 없습니다.
브라우저 내부 처리
"v7이 더 좋다"로 끝나는 글은 많습니다. 여기서는 비트가 어떻게 다른지, 그 차이가 데이터베이스에서 정확히 무엇을 바꾸는지, 그리고 v7이 대신 내주는 것이 무엇인지를 봅니다.
공개 URL에 노출되는 식별자, 비밀 토큰에 가까운 값, 그리고 정렬이 아무 의미 없는 데이터입니다. 예측 불가능성 자체가 요구사항이면 v4가 정답이고 바꿀 이유가 없습니다.
행이 계속 추가되는 대용량 테이블의 기본키, 이벤트 로그, 메시지 큐 레코드입니다. 삽입 순서와 정렬 순서가 같아야 이득이 생기는 워크로드입니다.
테이블이 수십만 행 규모거나, 기본키가 UUID가 아니거나, 삽입이 드물다면 v4를 v7로 바꿔서 체감되는 변화는 거의 없습니다. 이득은 규모에서 나옵니다.
아래에서 이 판단의 근거를 하나씩 봅니다. 값을 직접 뽑아 비교하려면 UUID 변환기의 생성 옵션에서 v4·v7·ULID를 바꿔가며 확인할 수 있습니다.
둘 다 128비트이고 문자열 형태도 같습니다. 다른 건 그 128비트를 무엇으로 채우는지입니다. v7은 RFC 9562로 표준화되면서 앞부분을 시간에 내주었습니다.
버전 4비트와 variant 2비트를 뺀 122비트가 난수입니다. 그래서 같은 값이 두 번 나올 확률을 걱정할 필요가 없고, 값을 보고 언제 만들어졌는지 알 수 없습니다. 문자열 14번째 자리가 항상 4인 것이 버전 표시입니다.
Unix epoch 밀리초가 그대로 앞에 오고, 버전 4비트(0111, 즉 문자열 14번째 자리가 7), variant 2비트, 나머지 74비트가 난수입니다. 앞부분이 시간이라 문자열을 그냥 사전순으로 정렬하면 생성 순서대로 정렬됩니다. 이게 v7의 전부입니다.
01a013ab-09ca-7958-88e4-7f9866134ad6와 01a013ab-09d0-7ec6-ac2e-079d51b83123는 같은 밀리초대에 연속 생성된 v7 두 개입니다. 앞 8자리가 같고 그다음이 조금 늘어난 것이 보입니다. v4를 두 번 뽑으면 앞부터 전혀 다릅니다.
타임스탬프가 같으므로 남은 74비트 난수로 갈립니다. 같은 밀리초 안에서의 순서는 보장되지 않습니다. "완전한 단조 증가"가 필요하면 RFC가 허용하는 카운터 방식을 쓰는 구현을 골라야 하고, 대부분의 애플리케이션에는 밀리초 단위 정렬로 충분합니다.
"v4는 인덱스에 안 좋다"는 말은 자주 들리는데 무엇이 어떻게 안 좋은지는 잘 설명되지 않습니다. B-tree가 어떻게 동작하는지 보면 이유가 하나로 정리됩니다.
B-tree는 정렬된 페이지에 값을 담습니다. 순차적인 키는 항상 맨 오른쪽 페이지 끝에 붙어서 그 페이지가 꽉 차면 새 페이지를 하나 더 만들면 됩니다. 반면 난수 키는 이미 꽉 찬 중간 페이지 아무 데나 꽂히므로 페이지를 반으로 쪼개는 일이 계속 발생합니다. 쪼개진 페이지는 절반만 채워진 채 남아 인덱스가 부풀고 디스크 읽기가 늘어납니다.
순차 키로 삽입하면 방금 쓴 페이지가 메모리에 남아 있어 다음 삽입도 그 페이지를 씁니다. 난수 키는 매번 다른 페이지를 건드리므로 캐시에 없는 페이지를 디스크에서 읽어옵니다. 테이블이 메모리보다 커지는 순간부터 이 차이가 급격히 벌어집니다.
InnoDB는 기본키가 클러스터드 인덱스라서 기본키 순서가 곧 행의 물리적 배치입니다. 난수 기본키는 행 자체를 테이블 전체에 흩뿌립니다. 게다가 모든 세컨더리 인덱스가 기본키를 함께 저장하므로 기본키가 크면 인덱스 전부가 커집니다.
여기서 중요한 건 이 모든 것이 규모의 문제라는 점입니다. 테이블이 메모리에 다 들어가는 동안에는 페이지 분할이든 캐시 미스든 체감되지 않습니다. v4에서 v7로 바꿔서 눈에 보이는 개선이 있었다는 이야기는 대개 수천만 행 이상에서 나옵니다.
정렬 이득에는 대가가 있습니다. 이 부분이 v7 소개 글에서 가장 자주 빠지는 내용입니다.
앞 48비트가 밀리초 타임스탬프이므로 누구든 v7 값에서 생성 시각을 밀리초 단위로 복원할 수 있습니다. 사용자 ID가 v7이고 그것이 공개 프로필 URL에 들어간다면, 그 사람의 가입 시각이 공개되는 것입니다. 주문 ID라면 주문 시각이, 문서 ID라면 작성 시각이 노출됩니다.
v7 값 두 개를 비교하면 어느 것이 먼저 만들어졌는지 알 수 있습니다. 경쟁사가 자사 서비스에 가입해 받은 ID와 다른 ID를 비교해 그 사이에 몇 건이 생성됐는지 추정하는 것도 가능합니다. 난수 74비트가 있어 정확한 카운트는 아니지만 시각 범위는 확실히 드러납니다.
실무에서 흔한 절충은 내부용과 외부용을 분리하는 것입니다. 데이터베이스 기본키는 v7로 두어 인덱스 이득을 챘고, 외부에 노출되는 URL에는 별도의 v4나 짧은 슬러그를 씁니다. 컬럼 하나가 늘지만 정렬 이득과 비노출을 동시에 가질 수 있습니다.
시각이 노출되지 않는다는 점에서는 안전합니다. 다만 v4도 비밀은 아닙니다. 추측하기 어려울 뿐이므로 접근 제어를 대신할 수 없습니다. "URL을 아는 사람만 볼 수 있다"는 설계는 UUID 버전과 무관하게 취약합니다.
UUID를 CHAR(36)으로 저장하면 하이픈까지 36바이트를 씁니다. 같은 값을 BINARY(16)로 저장하면 16바이트입니다. 기본키에서 20바이트 차이는 모든 세컨더리 인덱스에 그대로 곱해집니다. 다만 사람이 눈으로 읽거나 로그에서 grep 하기 어려워지는 대가가 있습니다. UUID 변환기의 hex 변환 탭으로 두 형태를 서로 바꿔볼 수 있습니다.
ULID도 앞 48비트가 타임스탬프이고 나머지 80비트가 난수라 정렬 성질은 v7과 같습니다. 차이는 표현입니다. Crockford base32로 26자 문자열이 되어 UUID의 36자보다 짧고, 대소문자 혼동이 적은 문자만 씁니다. 대신 UUID 타입을 지원하는 데이터베이스 컬럼에 그대로 넣을 수 없습니다.
데이터베이스가 UUID 타입을 지원하고 기존 코드가 UUID를 다루고 있다면 v7이 자연스럽습니다. 반면 URL이나 로그에 사람이 자주 보는 값이고 짧을수록 좋다면 ULID가 낫습니다. 둘을 섞어 쓰는 것은 권하지 않습니다.
이미 저장된 v4 값을 v7로 바꿀 방법은 없습니다. 그 값에는 생성 시각 정보가 애초에 없습니다. 그래서 전환은 항상 "지금부터 새로 만드는 것만 v7"이 되고, 테이블에는 두 버전이 섞여 남습니다.
"기본키로 정렬하면 시간순"이라는 가정으로 쿼리나 페이지네이션을 짜면, v4 시절 행들이 무작위 위치에 끼어 있어 결과가 이상해집니다. 전환 시점을 기록해 두고 그 이전 데이터는 별도의 created_at 컬럼으로 정렬하는 편이 안전합니다. 애초에 시간 정렬이 필요하면 인덱스된 타임스탬프 컬럼을 두는 것이 UUID 버전에 의존하는 것보다 명시적입니다.
v7은 비교적 최근 표준이라 언어와 라이브러리 버전에 따라 지원이 갈립니다. 데이터베이스 드라이버, ORM, 그리고 값을 검증하는 스키마(예: JSON Schema의 format: uuid) 모두가 버전 7을 받아들이는지 봐야 합니다. 브라우저 crypto.randomUUID()는 v4만 만듭니다.
요청 추적 ID, 멱등성 키, 파일명 등에도 UUID가 쓰입니다. 이 자리에서는 정렬이 이득이 아니고 오히려 시각 노출만 생기는 경우가 많습니다. "전부 v7로 통일"보다 자리별로 판단하는 편이 낫습니다.
BINARY(16)이 CHAR(36)보다 작고, 기본키라면 그 차이가 모든 인덱스에 곱해집니다.값을 직접 만들어 비교하려면 UUID 변환기에서 생성 옵션을 v4·v7·ULID로 바꿔보고, hex 변환 탭으로 16바이트 형태를 확인하세요. v7의 앞부분이 실제로 어떤 시각인지 대조할 때는 Unix 타임스탬프 변환기가 편합니다.
v4는 완전 난수라 B-tree 인덱스 곳곳에 삽입되어 페이지 분할이 잦고 캐시 적중률이 떨어집니다. 행이 수백만 건으로 늘어난 뒤에 체감되는 문제라 초기 설계에서 놓치기 쉽습니다. v7은 끝에 순차적으로 붙어 이 비용을 줄입니다.
v7에는 생성 밀리초가 그대로 들어 있어 가입 시점이나 주문 시각을 역산할 수 있습니다. 경쟁사가 가입자 증가 속도를 추정할 수도 있습니다. 외부에 노출되는 식별자는 v4가 맞습니다.
형식이 같아 저장은 문제없지만 정렬 보장이 깨집니다. v7끼리는 시간순이어도 섞인 v4는 무작위 위치에 들어갑니다. 마이그레이션할 때는 정렬에 의존하는 쿼리가 있는지 먼저 확인하세요.
입력
같은 순간에 각각 3개 생성
결과
v4 f47ac10b-58cc-4372-a567-0e02b2c3d479
v4 1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed
v4 7c9e6679-7425-40de-944b-e07fc1f90ae7
v7 01923f8a-1c4d-7000-8a3b-9f2e1d4c5b6a
v7 01923f8a-1c51-7000-8a3b-2c7d9e4f1a3b
v7 01923f8a-1c55-7000-8a3b-6b1a8c2d7e9f
v7은 앞부분이 거의 같습니다. 밀리초 타임스탬프가 들어 있어서 문자열 정렬만으로 생성 순서가 유지됩니다. v4는 앞부분부터 무작위입니다.
입력
01923f8a-1c4d-7000-8a3b-9f2e1d4c5b6a
결과
세 번째 그룹의 첫 글자가 버전
7000 -> v7
4372 -> v4