UUID 생성기

UUID를 하나씩 또는 쉰 개씩 생성합니다. 무작위 v4와, 앞의 타임스탬프 덕분에 한 묶음이 순서대로 정렬되어 데이터베이스 키에 알맞은 시간순 v7을 고를 수 있습니다.

버전

122비트 랜덤. 언제 만들어졌는지 아무것도 드러내지 않습니다.

개수

생성 중…

UUID란 무엇이고 왜 존재하는가

UUID — Universally Unique Identifier, Microsoft 문서에서는 GUID라고도 합니다 — 는 익숙한 8-4-4-4-12 구분으로 32자리 16진수로 쓰이는 128비트 값입니다. 그 온전한 목적은 별개의 시스템이 서로 조율하지 않고 독립적으로 식별자를 만들면서도 결과가 충돌하지 않으리라 확신할 수 있게 하는 것입니다. 데이터베이스 자동 증가 열과 다른 점이 그것입니다: 두 대의 서버, 비행기 안에서 오프라인인 두 대의 모바일 기기, 그리고 백그라운드 작업이 누구의 허락도 구하지 않고 같은 순간에 레코드를 만들 수 있습니다.

여기서 고유성은 보장이 아니라 확률적입니다. 버전 4는 122비트를 난수에 맡기는데, 이는 수십억 개의 값을 만들어도 반복 확률이 디스크가 조용히 데이터를 손상할 확률보다 훨씬 낮을 만큼 큰 공간입니다. 실무에서는 고유한 것으로 다뤄도 됩니다. 수학이 약한 고리가 되지는 않습니다.

버전 4와 버전 7 — 중요한 선택

버전 4는 끝에서 끝까지 랜덤입니다. 언제 만들어졌는지, 누가, 어떤 순서로 만들었는지 아무 정보도 나르지 않습니다. 그것이 최대 강점인지 핵심 결함인지는 어디에 두느냐에 달렸습니다.

버전 7은 2024년 RFC 9562에서 표준화되었고, 처음 48비트를 밀리초 Unix 타임스탬프로 바꾸고 나머지를 난수로 채웁니다. 시간이 앞에 오고 값이 왼쪽에서 오른쪽으로 읽히므로, v7 식별자를 맨 텍스트로 정렬하면 시간순으로도 정렬됩니다.

이것은 겉모습만의 차이가 아닙니다. 데이터베이스는 기본 키를 정렬된 B-트리 인덱스에 둡니다. v7 키를 삽입하면 새 행이 트리의 오른쪽 끝, 이전 행 옆에 놓입니다 — 쓰는 페이지가 메모리에 남고 인덱스가 깔끔하게 자랍니다. v4 키를 삽입하면 쓸 때마다 무작위 위치에 놓여 데이터베이스가 매번 다른 페이지를 건드리고, 캐시 적중률이 떨어지고, 인덱스가 조각납니다. 크고 바쁜 테이블에서는 삽입 처리량 차이가 크며, v7이 기본 키에 이토록 빨리 채택된 이유입니다.

  • 데이터베이스 기본 키, 이벤트·로그 식별자, 생성 시각으로 정렬하거나 범위 조회하고 싶은 것에는 v7을 고르세요.
  • 식별자가 신뢰할 수 없는 곳에 나타나 아무것도 — 한 레코드가 다른 것보다 조금 먼저 만들어졌다는 사실조차 — 새어서는 안 될 때는 v4를 고르세요.
  • 상관 ID, 멱등 키, 파일 이름처럼 순서가 문제되지 않는 곳에는 어느 쪽이든 괜찮습니다.

대가는 바로 그 새어 나감입니다: v7 식별자는 그것을 보는 이에게 만들어진 시각을 밀리초 단위로 알려 줍니다. 행 ID에는 대개 무해하고 종종 유용합니다. 비밀번호 재설정 토큰이나 공개 공유 링크에는 공개할 뜻이 없던 정보입니다 — 그리고 그것들은 애초에 UUID가 아니라 전용 시크릿 생성기에서 나온 랜덤 토큰이어야 합니다.

같은 밀리초 안의 순서

많은 v7 구현을 잡는 미묘한 점: 현대 하드웨어는 1밀리초에 하나보다 훨씬 많은 식별자를 냅니다. 두 값이 타임스탬프를 공유하면 그 순서는 뒤따르는 난수 비트가 정합니다 — 즉 무작위로. 촘촘한 루프로 쉰 개를 내면 대충만 정렬된 쉰 개의 값이 나와, v7을 고른 바로 그 성질을 잃습니다.

RFC 9562는 이것을 단조 카운터로 다루고, 이 도구는 그것을 구현합니다: 타임스탬프 직후의 12비트가 밀리초 안에서 위로 세므로, 배치가 대충이 아니라 엄격히 증가합니다. 카운터가 차면 — 같은 밀리초에 4096개를 넘으면 — 생성기는 되감아 앞선 것보다 앞에 정렬되는 값을 내는 대신 다음 밀리초를 빌립니다. NTP 보정으로 일어나는, 시계가 뒤로 가는 경우도 같은 방식으로 다룹니다.

v7 값을 왼쪽에서 오른쪽으로 읽으면, 0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b을 예로:

  • 0190a1b2-c3d4 — 48비트 밀리초 Unix 타임스탬프. 앞에 오므로 텍스트 순서가 시간 순서입니다.
  • 7 — 버전 니블로, 이것을 v4가 아니라 v7로 만드는 것입니다.
  • e5f — 12비트 단조 카운터로, 단일 밀리초 안에서 증가합니다.
  • 8 — 변형 비트로, RFC 9562가 모든 현대 UUID에 대해 고정합니다(늘 8, 9, a, b 중 하나).
  • a9b-0c1d2e3f4a5b — 나머지 62비트로, 순전히 랜덤입니다.

UUID 저장과 사용

정규 형태는 소문자에 하이픈이 붙은 것이고, RFC 9562는 생성기가 바로 그것을 내야 한다고 말합니다 — 그래서 이 도구는 서식 옵션을 제공하지 않습니다. 실제로는 다른 표기도 만나게 됩니다: Microsoft 도구에서는 대문자에 중괄호로 감싸이고, 짧은 열을 원한 누군가가 하이픈을 뗀 형태도 있습니다. 모두 같은 128비트이고, 비교는 대소문자를 무시해야 합니다.

중요한 것은 저장입니다. UUID는 16바이트지만 텍스트 형태는 36자입니다 — 그래서 문자열로 저장하면 행에서, 그리고 더 중요하게는 그것을 포함하는 모든 인덱스에서 공간이 두 배 넘게 듭니다. 네이티브 타입이 있다면 그것을 쓰세요:

uuid                      -- PostgreSQL: 네이티브 16바이트 타입
BINARY(16)                -- MySQL: 간결. CHAR(36)은 행마다 20바이트 낭비
uniqueidentifier          -- SQL Server
crypto.randomUUID()       // JavaScript: v4만, 보안 컨텍스트 필요
uuid.uuid4() / uuid7()    # Python: 표준 라이브러리 v4. v7은 라이브러리로

마지막 주의: UUID는 식별하지 인가하지 않습니다. 추측할 수 없다는 이유로, 그것을 담은 미공개 URL을 비공개로 여기고 싶어집니다. 하지만 식별자는 새어 나갑니다 — 로그, 브라우저 기록, 리퍼러 헤더, 스크린샷을 통해 — 그러니 정말로 지켜야 할 것에는 그 뒤에 여전히 진짜 권한 확인이 필요합니다.

이 UUID가 v4인지 v7인지 알아내는 법

식별자는 대개 어디서 왔는지 적힌 쪽지 없이 건네받지만, 쪽지는 필요 없습니다: 버전은 값 자체 안에 쓰여 있습니다. 둘 다 같은 8-4-4-4-12 구분의 32자리 16진수라 차이는 모양에 있지 않고, 정해진 자리에 놓인 두 자리 숫자에 있으며, 나머지는 거기서 따라 나옵니다.

  • 세 번째 그룹의 첫 자리가 버전 니블입니다. 거기가 4면 버전 4, 7이면 버전 7이고, 값의 다른 어느 부분도 여기에 끼어들지 못합니다.
  • 네 번째 그룹의 첫 자리에는 변형 비트가 실리며, 두 버전 모두 8, 9, a, b 중 하나입니다 — 그래서 손에 든 것이 어느 쪽인지는 결코 알려 주지 않습니다. 대신 알려 주는 것은 그 값이 애초에 RFC 9562를 따른다는 사실입니다. 그 자리에 다른 것이 있다면 더 오래된 배치이거나 UUID가 아닙니다.
  • 버전 니블이 7이면 첫 열두 자리가 생성 시각입니다: 1970년 초부터의 밀리초를 16진수로 적은 48비트 수입니다.
  • 4라면 더 읽을 것이 없습니다. v4는 시각도 기기도 순서도 부호화하지 않으며, 그것이 v4를 고른 이유인 성질입니다.

위 절이 풀어 읽는 값 0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b을 보세요. 세 번째 그룹이 7로 열리니 버전 7이고, 첫 열두 자리는 0190a1b2c3d4입니다 — 이 16진수를 10진수로 바꿔 Unix 타임스탬프 변환에 건네면 2024년 7월의 어느 날이 나옵니다. 그 옆의 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d은 세 번째 그룹이 4로 열리고, 그 자신의 첫 열두 자리는 아무것으로도 복호되지 않는 무작위 데이터입니다. 둘이 일치하는 것은 딱 하나입니다: 둘 다 네 번째 그룹을 변형 비트가 허용하는 네 글자 중 하나로 엽니다.

v7은 v4만큼 충돌에 강한가

충분히 물을 만합니다. v7이 실제로 난수를 덜 지니기 때문입니다. 버전 4는 128비트 가운데 122비트를 난수에 씁니다. 버전 7은 48비트를 타임스탬프에, 여기서는 12비트를 더 단조 카운터에 쓰므로 난수는 62비트만 남습니다 — 절반을 조금 넘는 양입니다. 맨숫자로만 읽으면 심각한 후퇴처럼 보입니다.

  • 이 도구의 한 배치 안에서 반복은 드문 것이 아니라 불가능합니다. 밀리초를 공유하는 값들은 서로 다른 카운터 값을 받고, 밀리초를 공유하지 않는 값들은 서로 다른 타임스탬프를 지니므로 같아질 수 있는 둘은 없습니다 — 확률이 아니라 산술입니다.
  • 같은 순간에 생성하는 두 기기 사이에서는 확률이 최악이라도 62비트 위의 생일 문제이며, 대략 공간의 제곱근에서 반반이 됩니다: 20억 개쯤의 값이, 그것도 모두 같은 1밀리초 안에서 말입니다.
  • 서로 다른 밀리초 사이에서는 v7의 충돌이 드문 정도가 아니라 불가능합니다. 앞자리 숫자 자체가 다르기 때문입니다.

그러니 정직한 비교는 122비트 대 62비트가 아닙니다. 시스템이 사는 내내 거듭 뽑는 하나의 추첨과, 밀리초마다 따로 열렸다가 그 밀리초가 끝나면 버려지는 훨씬 작은 추첨의 비교입니다. 두 번째 방식이 더 강하고, v4를 고를 이유는 비교 절이 드는 그대로입니다 — 충돌이 아니라, v7이 언제 만들어졌는지를 소리 내어 말한다는 점입니다.

기존 테이블을 v4에서 v7로 바꾸기

v7을 고른 뒤 따라오는 물음은 이미 테이블에 있는 행을 어떻게 할 것인가이고, 안심할 점은 열 자체는 전혀 바꿀 필요가 없다는 것입니다. 두 버전 모두 같은 36자 텍스트 형태의 같은 128비트라, PostgreSQL의 uuid 열도 BINARY(16)도 CHAR(36)도 섞임을 알아채지 못한 채 담아 둡니다. 새 행부터 v7을 생성하기 시작하고 거기서 멈추면 됩니다. 마이그레이션 단계도, 소급 채우기도 없습니다.

  • 곧바로 오는 것: 지금부터 삽입되는 모든 행이 앞에 타임스탬프를 지니므로, 새 키들이 인덱스 전체로 흩어지지 않고 한쪽 끝에서 서로 나란히 놓입니다. 이 이점은 새 쓰기가 어디로 가느냐에 관한 것이고, 첫 삽입부터 있습니다.
  • 결코 오지 않는 것: 이미 있는 행들은 영영 순서를 갖지 못합니다. 받은 적 없는 생성 시각을 값에 넣어 줄 수 있는 것은 없고, 식별자를 전부 다시 발급한다는 것은 그것을 가리키는 모든 외래 키를 다시 쓴다는 뜻입니다 — 생성기를 바꾸는 것보다 훨씬 큰 일이고, 인덱스만을 위해 그럴 값어치가 있는 경우는 드뭅니다.
  • 살펴야 할 것: 열의 정렬이 한 가지를 뜻하기를 그칩니다. 밀리초의 수는 48비트 필드에서는 작은 수라, v7 키들은 범위 아래쪽의 좁은 띠에 모이고 v4 키들은 범위 전체에 퍼지며 이따금 그 띠에 하나씩 끼어듭니다.

무는 것은 바로 이 마지막 점입니다. 키로 정렬하는 질의는 새 데이터 위에서는 옳아 보이고 오래된 데이터에 대해서는 조용히 잘못 보고합니다. 테이블 전체에 하나의 순서가 필요하다면 생성 시각 열을 더해 그것으로 정렬하고, 식별자는 다시 식별자로 돌려 두세요. 그러는 동안에도 섞임은 적어도 읽을 수는 있습니다: 버전 니블이 모든 값에 들어 있으므로, 필요하면 질의가 두 번째 열 없이도 두 시대를 가려낼 수 있습니다.

자주 묻는 질문

v4와 v7 중 무엇을 써야 하나요?
데이터베이스 기본 키와 생성 시각으로 정렬하고 싶은 것에는 v7을 쓰세요: 앞의 타임스탬프가 삽입을 인덱스 전체로 흩지 않고 끝에 모아 둡니다. 식별자가 언제 만들어졌는지를 포함해 아무것도 드러내면 안 될 때는 v4를 쓰세요.
두 UUID가 같아지는 일이 있나요?
가능하지만 사라질 듯 드뭅니다. 버전 4는 122비트 난수를 지니므로, 수십억 개의 값을 만든 뒤에도 충돌 확률이 저장소가 조용히 그것들을 손상할 확률보다 훨씬 낮게 남습니다.
버전 1, 3, 5는 어떻게 됐나요?
버전 1은 타임스탬프와 머신의 MAC 주소를 부호화해 하드웨어 신원을 새게 하고 브라우저에서는 아예 만들 수 없습니다. 버전 3과 5는 네임스페이스와 이름에서 MD5나 SHA-1로 UUID를 결정론적으로 이끌어 냅니다 — 같은 입력이 늘 같은 식별자를 내야 할 때 유용하지만, 새로 생성하는 것과는 다른 일입니다.
UUID가 시크릿 토큰으로 쓸 만큼 안전한가요?
v4 UUID는 추측할 수 없지만 v7은 생성 시각을 공공연히 부호화하고, 어느 쪽도 자격 증명으로 의도되지 않았습니다. 비밀번호 재설정, 세션 토큰, 공유 링크에는 전용 랜덤 시크릿을 생성하고, 식별자가 추측하기 어렵다는 데 기대지 말고 서버에서 권한을 확인하세요.
제 v7 배치가 다른 도구에서 완벽히 정렬되지 않는 이유는 무엇인가요?
많은 구현이 단조 카운터를 빼기 때문입니다. 여러 값이 밀리초를 공유하면 그 순서가 뒤따르는 난수 비트에 맡겨집니다. 이 도구는 RFC 9562의 카운터를 구현하므로 여기서 생성한 배치는 엄격히 증가합니다.
UUID를 데이터베이스에 어떻게 저장해야 하나요?
네이티브 16바이트 타입이 있다면 그것으로 — PostgreSQL의 uuid, SQL Server의 uniqueidentifier, MySQL의 BINARY(16)입니다. 대신 36자 텍스트 형태로 저장하면 행에서, 그리고 그 열을 포함하는 모든 인덱스에서 쓰는 공간이 두 배 넘게 듭니다.
이것들이 당신의 서버에서 생성되나요?
아니요. 플랫폼의 암호학적 난수원인 crypto.getRandomValues를 써서 브라우저 안에서 생성됩니다 — crypto.randomUUID가 쓰는 것과 같습니다. 어느 값도 어디로 보내지지 않고, 페이지를 새로 고치면 완전히 새로운 한 벌이 나옵니다.
v7 UUID가 언제 만들어졌는지 알 수 있나요?
네, 값 말고는 아무것도 필요 없습니다. 첫 열두 자리 16진수가 1970년 초부터의 밀리초 수입니다: 10진수로 바꿔 그 결과를 Unix 타임스탬프 변환에 건네세요. v4에는 그런 필드가 없어서 거기의 같은 열두 자리는 무작위이고 아무것으로도 복호되지 않습니다.
v7은 v4보다 난수 비트가 적나요?
네 — 여기서는 62비트로, v4의 122비트에 견줍니다. 타임스탬프와 카운터가 자리를 차지하기 때문입니다. 그렇다고 실제로 충돌이 더 잘 나지는 않습니다: 두 v7 값은 같은 밀리초에 만들어졌을 때만 충돌할 수 있으므로 그 62비트는 시스템이 사는 내내가 아니라 한 밀리초 안에서 쓰이고, 이 도구의 한 배치 안에서는 카운터가 반복을 드문 정도가 아니라 불가능하게 만듭니다.
v4와 v7 UUID가 같은 열에 있어도 되나요?
네. 같은 텍스트 형태의 같은 128비트라 스키마는 아무것도 바뀌지 않고 마이그레이션도 없습니다 — 지금부터 v7을 생성하고 오래된 행은 그대로 두면 됩니다. 살필 것은 정렬 하나뿐입니다: 새 행들끼리는 생성 순서로 놓이지만 오래된 무작위 행들이 그 주위에 흩어져 있어서, 키로 정렬하는 것이 테이블 전체의 시간 순서는 아닙니다.
UUID 버전 6과 8은 무엇인가요?
RFC 9562가 v7과 나란히 둘 다 정의합니다. 버전 6은 버전 1의 타임스탬프 필드를 다시 배열해 값이 시간순으로 정렬되게 한 것으로, 이미 v1에 매인 시스템을 위한 것입니다 — 그 밖의 경우에는 v7을 쓰라고 표준이 말합니다. 버전 8은 맞춤 배치를 위해 일부러 비워 둔 자리로, 고정되는 것은 버전과 변형 비트뿐이고 나머지 122비트는 직접 정합니다. 이 도구는 둘 다 생성하지 않습니다.

관련 도구