식별자 고르기: UUID, 짧은 ID, 그리고 정보를 흘리는 식별자

어떤 UUID 버전을 쓸지, v4 충돌이 왜 실무에서 걱정거리가 아닌지, 순차 정수는 무엇을 노출하는지, 짧은 ID의 알파벳은 무엇을 맞바꾸는지, 무작위 비트를 어디에서 얻어야 하는지 설명합니다.

내부용에는 UUID v4를 쓰고, 식별자가 큰 테이블의 기본 키가 되는 경우에는 v7을 쓰며, 사람이 소리 내어 읽어야 한다면 제한된 알파벳으로 만든 짧은 코드를 쓰십시오. 외부에 노출되는 곳에서 피해야 할 선택지는 순차 정수입니다.

알아둘 만한 UUID 버전

UUID는 128비트입니다. 그중 4비트는 버전, 2비트는 배리언트가 차지하므로 128비트를 온전히 쓰는 버전은 없습니다.

버전담고 있는 것쓰는 경우
v160비트 타임스탬프, 14비트 클럭 시퀀스, 48비트 노드 ID (MAC 주소)레거시 데이터에서만
v3 / v5네임스페이스와 이름의 MD5 또는 SHA-1 해시결정적인 ID가 필요할 때
v4122비트의 무작위 값, 그 외에는 없음기본값이며 아무것도 추론되면 안 될 때
v748비트 밀리초 타임스탬프에 이어 74비트 무작위 값삽입 순서가 중요한 키

주의해야 할 것은 v1입니다. v1은 생성 시각을 담고, 대부분의 구현에서는 그것을 만든 기기의 MAC 주소까지 담습니다. 1999년 멜리사 바이러스 제작자를 추적하는 데 이런 식별자가 도움이 된 것도 그 때문입니다. 2024년 RFC 9562로 표준화된 v7은 생성 시각을 의도적으로 드러냅니다. 그것이 v7의 기능이자 대가입니다.

충돌 문제

v4 UUID에는 122비트의 무작위 값이 있으므로 전체 개수는 2^122, 대략 5.3 x 10^36개입니다. 중요한 것은 공간의 크기가 아니라 생일 문제의 한계입니다. n개의 값 중 어느 둘이라도 일치할 확률은 대략 n의 제곱을 2^123으로 나눈 값입니다. 1조 개의 UUID를 만들어도 중복이 생길 확률은 약 10조분의 1이며, 확률이 동전 던지기 수준이 되려면 약 2.7 x 10^18개가 필요합니다. 초당 10억 개를 만들어도 85년쯤 걸리는 양입니다.

그렇다고 해서 "실무상 문제가 되지 않는다"가 "불가능하다"가 되는 것은 아니며, 문제가 생기는 지점은 결코 산술이 아닙니다. 중복은 생성기에서 나옵니다. 고정된 시드, 엔트로피 풀까지 함께 복제된 가상 머신, 저장된 PRNG 상태를 담아 배포된 컨테이너 이미지 같은 것들입니다. 유일성은 형식이 아니라 난수원에 달려 있으므로, 컬럼의 유일성 제약은 그대로 두십시오.

정렬 순서, 인덱스 지역성, 정보 유출

무작위 식별자는 흩어집니다. B-트리 인덱스에서는 삽입할 때마다 무작위 리프 페이지에 들어가므로 작업 집합이 인덱스의 오른쪽 끝이 아니라 인덱스 전체가 되고, 캐시 적중률이 떨어지며 페이지 분할이 일어납니다. 시간에 따라 커지는 키는 한곳에 이어 붙으므로 소수의 같은 페이지만 뜨거운 상태로 유지됩니다. v7과 ULID가 존재하는 이유가 이것입니다. ULID는 48비트 밀리초 타임스탬프에 80비트 무작위 값을 더한 것이며, 정렬 가능한 Crockford base32 문자 26개로 표기합니다.

그 대가는 예측 가능성입니다. 시간순 ID는 해당 레코드가 언제 만들어졌는지를 알려주므로, 몇 개만 있어도 가입 속도와 한산한 시간대가 드러납니다. 그것이 문제라면 v4를 쓰고 인덱스 비용을 감수하십시오.

순차 정수는 한술 더 뜹니다. /invoices/1041은 청구서를 대략 천 건 발행했다는 사실을 알려주고, 일주일 간격의 ID 두 개면 성장률이 나옵니다. 더 나쁜 것은 공격자가 1040을 요청할 수 있다는 점입니다. 레코드가 존재하고 호출자가 로그인되어 있다는 이유만으로 서버가 소유자를 확인하지 않고 그 레코드를 돌려준다면, 이는 접근 제어 결함이며 안전하지 않은 직접 객체 참조로 분류됩니다. 추측할 수 없는 ID가 인가 검사를 대신하지는 못하지만, 누군가 테이블을 훑고 지나가는 일은 막아줍니다.

슬러그도 식별자이며, 제대로 만들면 규모에 관한 정보를 흘리지 않습니다. 악센트를 풀고, 소문자로 바꾸고, 문장 부호를 하이픈으로 정리하면 되며, 슬러그 생성기가 하는 일이 바로 그것입니다. 게시한 뒤에는 슬러그를 그대로 유지하십시오. 슬러그를 바꾸면 유입 링크가 끊깁니다.

짧은 ID, 알파벳, 길이

무작위 코드의 각 문자는 알파벳 크기의 log2만큼의 값을 갖습니다. base62는 문자당 5.95비트, base32는 5비트, 16진수는 4비트입니다. 길이와 알파벳이 정해지면 주어진 물량에서의 충돌 확률이 정해집니다.

길이 (base62)서로 다른 값의 수비트ID 10억 개 안에서의 충돌 확률
82.2 x 10^1443.7사실상 확실
108.4 x 10^1759.5약 45%
123.2 x 10^2171.5약 6,500분의 1
164.8 x 10^2895.3약 1000억분의 1

이는 각 문자가 서로 독립적으로 무작위라는 가정에서 나온 값입니다. inv_ 같은 접두사는 비트를 하나도 더해주지 않습니다.

사람이 읽는 용도라면 base62는 잘못된 알파벳입니다. 0O, 1lI는 옮겨 적을 때 오류가 나기를 기다리는 조합이고, 대소문자를 구분하는 코드는 전화 통화에서 살아남지 못합니다. Crockford base32는 I, L, O, U를 빼고 입력 시 대소문자를 모두 받아들이므로, 문자 하나를 더 쓸 값어치가 있습니다.

무작위 비트는 어디에서 오는가

Math.random()은 암호학적 난수원이 아닙니다. V8은 이를 xorshift128+로 구현하는데, 이 알고리즘의 내부 상태는 짧은 출력 열만 있어도 복원할 수 있고, 그 뒤로는 앞으로 나올 모든 값을 예측할 수 있습니다. 데모용 셔플이나 자리 채우기에는 괜찮습니다. 세션 식별자, API 키, 재설정 링크, 토큰에는 잘못된 선택입니다.

// Browsers, Node 19+, Deno and Bun
const id = crypto.randomUUID();

const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);

그 밖의 환경에서는 Node의 crypto.randomBytes, Python의 secrets.token_urlsafe, Go의 crypto/rand, Java의 SecureRandom, PHP의 random_bytes를 씁니다. UUID 생성기는 암호학적으로 무작위인 v4 값을 만들어내고 비밀번호 생성기도 같은 브라우저 API를 사용합니다. 난수 생성기는 표본 추출용이지 비밀값용이 아닙니다.

직접 구현할 때의 함정이 하나 있습니다. 무작위 바이트를 62개 문자 알파벳에 % 62로 대응시키면 앞쪽 여덟 문자로 분포가 치우칩니다. 기각 표본 추출을 쓰십시오.

자리 채우기 데이터와 샘플 데이터

Lorem ipsum은 망가뜨린 키케로 문장이며, 읽을 수 없다는 이유로 살아남았습니다. 언어처럼 보이지만 언어가 아닌 텍스트는 레이아웃을 문장 내용이 아니라 형태와 행 길이로 판단하게 해줍니다. Lorem Ipsum 생성기가 만들어내는 것이 바로 그런 텍스트입니다. 다만 최종 승인 전에는 실제 문구로 교체하십시오. 자리 채우기용 라틴어는 진짜 제목이 일으킬 넘침을 가려버리기 때문입니다.

지어낸 데이터에는 규칙이 하나 필요합니다. 절대로 진짜와 헷갈릴 수 있어서는 안 된다는 것입니다.

  • 카드 번호는 결제 사업자가 공개한 테스트 범위에서 가져오거나, 아예 Luhn 검사를 통과하지 못하게 만듭니다. Luhn 검사를 통과하는 무작위 16자리 숫자는 누군가의 것입니다.
  • 국가 식별번호는 예약된 범위를 씁니다. 000, 666, 900부터 999로 시작하는 미국 사회보장번호는 발급되지 않습니다.
  • RFC 2606이 예약한 example.com과 그 형제 도메인, 그리고 문서화용 대역인 192.0.2.0/24를 사용하십시오.

테스트 데이터가 운영 환경까지 흘러가는 일은 생각보다 자주 일어납니다. 엉뚱한 데이터베이스를 가리킨 시드 스크립트, 전체 수신자에게 발송된 이메일 템플릿에 남아 있던 자리 채우기 문구 같은 것들입니다. 가짜 레코드는 누가 봐도 가짜로 보여야 하며, 그래야 지원 조직이 대응하기 전에 사람이 알아챕니다.

QR 코드 요약

QR 코드는 모듈로 이루어진 격자이며, 버전 1부터 40까지 21x21에서 177x177까지 있습니다. 페이로드 길이가 버전을 결정하고, 인쇄 크기가 고정되어 있다면 버전이 높을수록 모듈이 작아져 더 좋은 인쇄 품질과 더 가까운 스캔이 필요해집니다. QR 코드 생성기에 넣기 전에 URL을 줄이십시오. 그리고 영숫자 모드는 대문자만 지원하므로 HTTPS://EXAMPLE.COM이 소문자 형태보다 더 작게 인코딩된다는 점도 알아두십시오.

오류 정정에는 네 단계가 있으며, 손상된 코드의 대략 7%(L), 15%(M), 25%(Q), 30%(H)까지 복구합니다. 중복성은 용량을 잡아먹으므로, 단계가 높아지면 같은 페이로드가 더 촘촘한 버전으로 밀려납니다. 보통은 M이 기본값이고, 작게 인쇄하거나 곡면에 붙이거나 가운데에 로고를 얹을 때는 Q나 H를 고릅니다. 로고를 얹어도 동작하는 이유는 중복성이 그것을 흡수하기 때문입니다.

코드가 스캔되는지를 결정하는 물리적 조건은 두 가지입니다. 첫째는 쿼이엇 존, 즉 사방으로 모듈 네 개 폭의 여백입니다. 디코더가 경계를 찾는 데 쓰이므로 테두리나 사진에 바짝 붙은 코드는 실패하는 경우가 많습니다. 둘째는 대비입니다. 확실히 어두운 색이 확실히 밝은 색 위에 있어야 하며, 흰 바탕의 옅은 파랑은 흔한 실패 사례이고 많은 디코더는 반전된 코드를 아예 거부합니다.