UUID
고친 사람 github-actions[bot]
UUID 는 각자 혼자 만들어도 서로 겹치지 않게 값을 붙이는 방법입니다. 번호를 내주는 중앙 장부에
물어보지 않고 필요한 쪽이 바로 하나를 지어냅니다. 128비트짜리 값입니다. 글자로는
f81d4fae-7dec-11d0-a765-00a0c91e6bf6 처럼 적습니다.
쉽고 빠른 이해
무슨 일을 하나 — 겹치지 않는 이름표를 혼자 만듭니다. 서버 열 대가 동시에 주문을 받아도 각 서버가 붙인 주문 번호가 서로 부딪히지 않습니다.
왜 필요한가 — 번호를 한 곳에서 받아 오면 값을 만들 때마다 그곳에 다녀와야 합니다. 그곳이 멈추면 새 데이터를 아예 못 만듭니다. 각자 만들면 다녀올 곳이 없어집니다.
어떻게 만드나
- 128비트 중 여섯 비트는 이 값을 어느 방법으로 만들었는지 적는 데 씁니다
- 남는 비트는 만드는 법에 따라 무작위 숫자로 채우거나 지금 시각으로 채웁니다
- 채운 128비트를 16진수 32글자로 바꾸고 다섯 덩이로 끊어 적습니다
대가 — 값이 커지고 뜻이 없습니다. 정수 번호보다 두 배 큽니다. 무작위로 만든 값은 순서가 없어 데이터베이스 인덱스가 새 값을 받을 때마다 여기저기 흩어집니다. 값을 만드는 곳이 하나뿐이면 정수 번호가 낫습니다.
상세
UUID 는 Universally Unique Identifier 의 줄임말입니다. 우리말로는 범용 고유 식별자입니다. 여러 곳에서 각자 값을 지어도 서로 겹치지 않는 것을 노린 식별자입니다. 주문을 받는 서버가 열 대이고 각 서버가 받자마자 주문 번호를 붙여야 할 때 쓰는 값이 이런 것입니다.
겹침을 피하는 방법은 둘 중 하나입니다. 하나는 아주 넓은 값 중에서 무작위로 하나를 뽑는 것이고, 다른 하나는 만든 시각과 만든 기계를 값 안에 섞어 서로 갈라 놓는 것입니다. 어느 쪽이든 남에게 묻지 않고 혼자 끝냅니다.
중앙 장부를 없애서 얻는 것
데이터베이스가 번호를 하나씩 올려 주는 방식은 그 데이터베이스 한 곳이 번호를 쥡니다. 값이 필요하면 먼저 그곳에 다녀와야 합니다. 그곳이 멈추면 새 데이터를 못 만듭니다. 한 곳이 멈추면 전체가 멈추는 단일 장애점입니다.
UUID 는 그 왕복을 없앱니다. 값을 쓰는 쪽이 직접 만들기 때문에 데이터를 저장하기 한참 전에 미리 값을 정해 둘 수 있습니다. 네트워크가 끊긴 휴대폰 앱이 먼저 값을 붙여 두고 나중에 서버에 올려도 번호가 부딪히지 않습니다.
128비트의 구성
128비트는 전부 무작위가 아닙니다. 여섯 비트는 이 값을 읽는 쪽에 규칙을 알려 줍니다. 나머지는 버전마다 다르게 채웁니다. 아래 표는 비트 자리마다 무엇이 들어가는지를 보입니다.
| 비트 구간 | 길이 | 담는 것 |
|---|---|---|
| 0~47 | 48비트 | 시각 또는 무작위 숫자(버전별) |
| 48~51 | 4비트 | 버전 — 채우는 법의 번호 |
| 52~63 | 12비트 | 시각 또는 무작위 숫자(버전별) |
| 64~65 | 2비트 | 변형 — UUID 규칙을 따른다는 표시 |
| 66~127 | 62비트 | 무작위 숫자 |
변형은 이 128비트를 UUID 규칙으로 읽으라는 표시입니다. 버전은 그 안에서 어느 방법으로 채웠는지의 번호입니다. 둘 다 값 안에 박혀 있어서 받은 쪽이 따로 묻지 않고도 읽는 법을 압니다.
글자로 적을 때는 16진수 32글자를 8-4-4-4-12 로 끊고 사이에 붙임표를 넣습니다. 이 두 칸도 글자로
드러납니다. 앞의 예 f81d4fae-7dec-11d0-a765-00a0c91e6bf6 에서 셋째 덩이의 첫 글자 1 이
버전이고, 넷째 덩이의 첫 글자 a 가 변형입니다.
비트 구간과 글자 덩이를 한 그림에 겹쳐 놓으면 이렇습니다.
packet-beta 0-31: "첫째 덩이 8글자 · 시각 또는 무작위" 32-47: "둘째 덩이 4글자" 48-51: "버전 4비트 — 셋째 덩이 첫 글자" 52-63: "셋째 덩이 나머지" 64-65: "변형 2비트 — 넷째 덩이 첫 글자" 66-79: "넷째 덩이 나머지" 80-127: "다섯째 덩이 12글자 · 무작위"
버전별로 채우는 값
버전은 남는 비트를 무엇으로 채울지를 정합니다. 무작위 숫자로 채우면 값에 아무 정보가 안 남습니다. 시각으로 채우면 만든 순서가 값에 남습니다.
| 버전 | 남는 비트를 채우는 것 | 언제 고르나 |
|---|---|---|
| 1 | 만든 시각 + 네트워크 카드마다 붙은 고유 주소 | 만든 기계와 시각을 값에 남길 때 |
| 3 · 5 | 이름을 해시 함수로 눌러 만든 값 | 같은 이름이면 늘 같은 UUID 가 나와야 할 때 |
| 4 | 무작위 숫자 122비트 | 아무 정보도 안 싣고 싶을 때. 가장 흔합니다 |
| 6 | 버전 1 의 시각을 큰 단위부터 놓도록 다시 늘어놓은 값 | 버전 1 을 쓰던 곳이 순서를 얻고 싶을 때 |
| 7 | 밀리초 단위 시각 + 무작위 숫자 | 새로 만드는 곳에서 순서와 무작위를 같이 원할 때 |
| 8 | 직접 정한 규칙 | 위 어느 것도 안 맞아 형식만 빌릴 때 |
여기서 이름은 웹 주소나 도메인 이름처럼 미리 정해 둔 문자열입니다.
버전 2 는 특정 분야 전용으로 따로 정해진 값이라 일반 용도로는 고를 일이 없습니다. 고를 일이 있는 것은 대개 4 와 7 둘입니다. 값에 아무것도 안 남기려면 4 를, 만든 순서가 값에 남아야 하면 7 을 고릅니다.
겹치지 않는다는 말의 뜻
버전 4 는 무작위로 채우는 비트가 122개입니다. 뽑을 수 있는 값이 2의 122제곱 가지입니다. 이 정도로 넓으면 두 번 뽑아 같은 값이 나오는 일을 실무에서는 없는 일로 칩니다.
다만 없는 일로 치는 것이지 못 나온다는 보장은 아닙니다. 값을 많이 뽑을수록 어딘가에서 두 개가 같아질 가능성이 오릅니다. 그 가능성이 생각보다 빨리 오른다는 것이 생일 문제입니다.
그래서 겹침이 절대 없어야 하는 곳은 UUID 하나에 기대지 않습니다. 같은 값이 두 번 들어오면 막는 고유 제약을 데이터베이스 쪽에 따로 겁니다.
무작위 숫자의 품질과 정보 노출
겹침보다 먼저 걸리는 것은 무작위 숫자의 품질입니다. 무작위 숫자를 만드는 부품, 즉 난수 생성기가 예측할 수 있는 값을 내면 같은 값이 두 번 나올 수 있습니다. 남이 다음 값을 알아맞힐 수도 있습니다. 비밀번호 재설정 링크처럼 남이 못 맞혀야 하는 값에 UUID 를 쓸 때 이 대목에서 사고가 납니다.
값 자체가 정보를 흘리기도 합니다. 버전 1 은 만든 시각과 기계 주소를 값 안에 담고 있어서, 받은 쪽이 그 UUID 만 보고 어느 기계가 언제 만들었는지 읽어 냅니다.
데이터베이스에 넣을 때 치르는 값
기본 키를 UUID 로 두면 두 가지를 냅니다. 크기와 순서입니다.
크기부터 봅니다. 128비트는 16바이트입니다. 흔히 쓰는 정수 번호는 8바이트입니다.
기본 키는 모든 인덱스에 같이 실려 다녀서 이 차이가 테이블 하나에서 끝나지 않습니다. 글자 그대로 36글자 문자열로 저장하면 더 커집니다.
순서는 버전이 갈라 놓습니다. 무작위로 만든 값은 정렬 순서가 만든 순서와 아무 관계가 없습니다. B-tree 인덱스는 키 순서대로 값을 끼워 넣으므로, 방금 만든 값 둘이 인덱스의 서로 먼 페이지에 떨어집니다. 시각으로 시작하는 버전 7 은 새 값이 늘 마지막 페이지에 붙습니다.
버전 4 와 버전 7 이 만든 값이 인덱스 페이지에 어떻게 떨어지는지를 그림으로 봅니다.
flowchart TD
subgraph V4["버전 4 · 새 값이 흩어진다"]
N1["새 값"] --> P1["페이지 1"]
P2["페이지 2"]
N2["다음 값"] --> P3["페이지 3"]
end
subgraph V7["버전 7 · 새 값이 뒤에 붙는다"]
Q1["페이지 1"]
Q2["페이지 2"]
M1["새 값"] --> Q3["페이지 3"]
M2["다음 값"] --> Q3
end
흩어지면 새 값을 쓸 때마다 다른 페이지를 디스크에서 읽어 올려야 합니다. 자주 쓰는 페이지만 메모리에 두는 이점도 줄어듭니다. 뒤에 붙으면 마지막 페이지 하나만 메모리에 있으면 됩니다.
이걸 안 고르는 때
값을 만드는 곳이 하나뿐이면 UUID 가 주는 이점이 없습니다. 번호를 하나씩 올려 주는 방식이 더 작습니다. 순서도 그냥 얻습니다.
사람이 읽거나 옮겨 적어야 하는 번호에도 안 맞습니다. 36글자를 전화로 불러 줄 수 없습니다. 주문 번호처럼 고객이 보는 값은 짧은 번호를 따로 두고 UUID 는 안쪽에서만 씁니다.
64비트 안에 순서까지 담아야 하는 곳은 다른 방식으로 갑니다. 스노우플레이크가 그 경우입니다. 시각과 기계 번호와 순번을 64비트에 나눠 담습니다.
고르는 순서를 간추리면 이렇습니다.
flowchart TD
A{"값을 만드는 곳이 여럿인가"} -->|아니오| B["하나씩 올려 주는 번호"]
A -->|예| C{"64비트 안에 담아야 하나"}
C -->|예| D["스노우플레이크"]
C -->|아니오| E{"만든 순서가 값에 남아야 하나"}
E -->|예| F["버전 7"]
E -->|아니오| G["버전 4"]
관련 항목
UUID 를 대신할 수 있는 다른 식별자
스노우플레이크 · ULID · 자동 증가 · 시퀀스 · 일련번호 · ObjectId · 티켓 서버 · 주키퍼 순차 노드
UUID 가 열로 들어가는 데이터베이스 구조
기본 키 · 대리 키 · 자연 키 · 복합 키 · 인덱스 · B-tree · 클러스터형 인덱스 · 페이지 분할 · 샤딩
UUID 를 채울 때 쓰는 재료
난수 생성기 · 엔트로피 · 해시 함수 · MD5 · SHA-1 · 타임스탬프 · 에포크 · MAC 주소
UUID 를 글자로 적어 나르는 표기
16진수 · Base64 · URI · JWT · 빅 엔디언 · 가변 길이 정수
UUID 를 고를 때 따지는 성질
생일 문제 · 멱등성 · 단일 장애점 · 분산 시스템 · 정보 노출 · 열거 공격
UUID 가 이름표로 붙는 대상
다른 이름: Universally Unique Identifier · 범용 고유 식별자 · GUID