TypeID - 스트라이프 ID에서 영감을 받은 타입 안전한, K-정렬 가능한, 전역 고유 식별자
(github.com/jetpack-io)- TypeID는 UUIDv7을 확장한 타입 안전 식별자로, Stripe API의 prefix 사용 방식에서 영감을 받은 전역 고유 식별자 형식
- 정식 문자열은 소문자 문자열이며, 최대 63자의 소문자 snake_case ASCII type prefix,
_구분자, 수정된 base32로 인코딩된 26자 UUIDv7 suffix의 세 부분으로 구성 - 타입 prefix로
userID를postID가 필요한 곳에 실수로 사용하는 일을 막고, 디버깅 시 식별자가 어떤 엔티티 타입을 가리키는지 바로 파악 가능 - TypeID는 UUID의 상위 집합이며, 타입 정보를 제거하고 디코딩하면 유효한 UUIDv7을 얻는 구조
- K-정렬 가능해 데이터베이스 기본 키로 사용할 수 있으며, 완전 무작위 전역 ID인 UUIDv4와 비교해 데이터베이스 locality 저하 문제를 줄이는 목적
- base32 인코딩은 URL 안전성, 대소문자 비구분, 모호한 문자 회피, 더블클릭 선택 가능성, UUID의 전통적 hex 인코딩보다 짧은 길이 특성을 가짐
- 공식 구현은 Go, SQL, TypeScript로 제공되며, 최신 spec 버전은 v0.3.0
- 명령줄 도구로 TypeID 생성, 기존 TypeID의 UUID 디코딩, UUID의 TypeID 인코딩을 수행 가능
typeid new prefix로 새 TypeID 생성typeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41로 type과 uuid 출력typeid encode prefix 0188bac7-4afa-78aa-bc3b-bd1eef28d881로 TypeID 생성
- Jetify의 TypeID Converter에서 TypeID 문자열을 UUID로 변환하거나
prefix:UUID형식으로 TypeID 변환 가능
댓글과 토론
Hacker News 의견들
-
몇 가지 제안이 있음: prefix 문자열은 늦기 전에 고정하고 문서화하는 게 좋겠음
Go 구현을 보니 소문자 ASCII라 괜찮아 보이지만,article-comment같은 복합 타입에는 애매함
복잡한 프로젝트나 ORM은 구분자를 피하기 어려우니 단일 구분자 허용을 검토할 만함
Go 구현에는 테스트가 없는데, 단위 테스트하기 쉬운 코드이니 꼭 추가해야 함
Go에서는 Google의 UUID 구현처럼 제대로 된 파싱 함수와 내부 바이트 배열을 쓰는 쪽이 맞고, 문자열은 렌더링과 prefix에만 쓰는 편이 좋음
지금 파싱은 너무 관대하고 suffix가 비어 있으면 생성 모드로 들어가는 듯하며,SplitN뒤 인덱싱은 밑줄이 없으면 패닉이 날 것 같음
설계 자체는 흠을 찾으려 했지만 결국 트레이드오프의 sweet spot을 잘 잡은 듯함- base32 인코딩처럼 가장 복잡한 부분에는 테스트가 있음: https://github.com/jetpack-io/typeid-go/blob/main/base32/bas...
그래도 지적이 맞고, 여러 언어 구현이 늘어날수록 서로 호환되는지 확인해야 하니 더 엄격한 테스트 묶음을 추가할 예정임
prefix와 관련해서는 허용 문자 집합을 명세에 정의하지 않은 점이 걱정인지 궁금함 - README에서 다른 구현을 요청하고 있으니, 테스트 묶음이 있으면 서드파티 코드에도 좋음
- 후속으로 꽤 철저한 테스트를 구현했음: https://github.com/jetpack-io/typeid-go/blob/main/typeid_tes...
그리고 명세에서 prefix도 명확히 했음 - 이걸 위한 테스트를 작성해보겠음
- prefix가 고정된 것이라고 오해한 듯함
prefix는 사용 도메인에 따라 정하는 것임
- base32 인코딩처럼 가장 복잡한 부분에는 테스트가 있음: https://github.com/jetpack-io/typeid-go/blob/main/base32/bas...
-
이런 방식을 몇 년째 써왔는데 두 가지 차이가 있음
첫째, 사람들이 실제로 값을 손으로 입력한다고 보지 않아서l과1혼동은 크게 신경 쓰지 않음
대신 단어, 특히 욕설이 우연히 생길 가능성을 줄이려고eiou모음을 뺀 base32를 씀
둘째, 솔트가 들어간 base32 문자 두 개를 체크섬으로 추가함
값이 실수나 악의로 이상할 때 데이터 저장소까지 조회하지 않아도 되는데, 다른 구현들이 왜 이걸 안 하는지 모르겠음- 몇 년 전 자동 생성 비밀번호로
analrita가 나와서 항의를 받은 적이 있음
제외 문자에a도 넣는 걸 고려할 만함 - 체크섬 추가에는 동의하지만, “실수나 악의”라는 부분이 궁금함
사람이 손으로 입력하지 않는다고 본다면 실수로 잘못된 값을 넣는 경로가 무엇인지 의문임
복사/붙여넣기 오류나 페이지가 대문자로 렌더링하는 경우는 있겠지만, base32라면 대소문자를 정규화하면 됨
또 2바이트짜리, 암호학적으로 안전하지 않은 체크섬이 악의적인 경우에 어떻게 도움이 되는지도 궁금함 - 체크섬 아이디어가 흥미로움
TypeID 명세에 포함하는 게 맞을지 검토해보고 있음 - 몇 달 전 인코딩 방식의 일부로 두 번째 방식을 구현했음
데이터베이스 조회를 얼마나 줄였는지는 모르겠지만, 디코딩 중 더 모호한 오류로 빠지지 않는다는 점이 보기 좋음
- 몇 년 전 자동 생성 비밀번호로
-
본문과는 별개로 “Crockford's alphabet” 링크가 있음: https://www.crockford.com/base32.html
이 base32 체계는 영숫자 중1과 혼동되는I,L,0과 혼동되는O, 그리고U를 제외함
페이지에서는U를 뺀 이유가 “우발적 외설”이라고 하는데, 대체 무슨 뜻인지 모르겠음- Crockford가 장난스럽게 쓴 것임
헷갈리지 않는 영숫자로 괜찮은 base32 알파벳을 만들려면O,I,L만 빼면 됨
그래도 33자가 남으니 하나를 더 빼야 하고, 무엇을 빼도 상관없으니 마지막 제거 문자에 임의의 이유를 붙인 셈임
사용자에게 보이는 ID라면 아주 말도 안 되는 이유는 아니지만, 당연히 완벽한 방지는 전혀 아님 I와O를 이미 제외하고U까지 빼면 거칠게 보이는 세 글자 조합이나 네 글자 조합을 꽤 많이 제거할 수 있음
물론A가 있어서 아직 가능한 조합도 있지만 가능성은 매우 낮음- 진정한 라틴어주의자에게
U는 키케로 시대에는 존재하지 않았으므로 외설에 가까울 정도로 천박한 글자임 - 이런 것도 더 있음: base58은 Satoshi/Bitcoin의 방식 https://en.wikipedia.org/wiki/Binary-to-text_encoding#Base58, “base62”는 Keybase의 saltpack https://github.com/keybase/saltpack, 유명한 “Adobe 85” https://en.wikipedia.org/wiki/Ascii85, basE91 https://base91.sourceforge.net
회사에서는 QR 코드용으로 새로운 “진법”을 여러 개 정의했는데, 개인적으로는 컴퓨터과학에서 덜 활용되는 영역이라고 봄 - Crockford보다 4년쯤 전에 동료와 거의 같은 문자 집합을 생각해낸 적이 있음
URL 슬러그 문제를 풀고 있었고, 길이가 꽤 길어서 바이트당 5비트면 필사 불편을 줄일 수 있다고 봤음
결국 몇 글자를 더 뺄 여유가 있었고, 다들 퇴근한 뒤 둘이 앉아 욕설의 모욕 정도를 순위 매겨 제거할 글자를 정했음
이후 비하 표현이 더 큰 문제라고 설득해서u대신n을 제거하는 쪽에 집중했음
tggr는 그냥 귀엽지만,n**r는 여러 HR 팀과 불편한 대화를 해야 하는 문제였음
최종 문자 집합은 이제 조금 흐릿하지만, 보통[a-z][0-9]를 다루고 URL에 쓸 수 없는 기호나 불러주기 어려운 기호가 많았음
기억으로는0,l,1을 모두 제거했고, 필사는 모두 대문자나 모두 소문자에서 이뤄진다고 봤던 것 같음
0o는 문제가 아니고1L도 마찬가지임
- Crockford가 장난스럽게 쓴 것임
-
Crockford 인코딩은 별로 마음에 들지 않음
실제로 이런 방식으로 인코딩된 값을 기술 지원하거나 분석할 때 명백한 실수였다고 느꼈음
이 알파벳은 전화로 식별자를 읽어주는 것처럼 실제로는 드문 목표에 맞춰 설계됐고, 모호성을 도입함
인코딩 문자열 자체로 로그를grep하거나 교차 참조하는 상황에서는 재앙이고, 하이픈까지 허용해 복사/붙여넣기와 줄바꿈 오류의 주요 원인이 됨
객체 식별자를 직접 타이핑하는 경우는 드물지만, 애플리케이션·채팅·포럼 사이에서 복사/붙여넣기하고 이메일로 전달하며 로그 파일에서 검색하는 일은 흔함
이런 조건에서는 발음 가능성은 무의미하고 대소문자 비구분은 방해가 되며, 일관성과 붙여넣기·줄바꿈 내성이 필요함
base58은 이런 요구에 더 잘 맞는 전단사 인코딩이고 더 짧기까지 함
Stripe에서 영감을 받아 몇 년째user_1BzGURpnHGn6oNru84B3Ri같은 타입 prefix가 붙은 base58 인코딩 UUID를 객체 식별자로 쓰고 있음
다만 Douglas Crockford의 base32 인코딩은 20년 전, 사용 환경이 꽤 달랐던 시기에 설계됐다는 점은 감안해야 함- base58이나 base64url도 고민했고, 더 짧은 인코딩은 마음에 들었음
하지만 대소문자 구분을 전제로 하고 싶지 않아 결국 base32 쪽으로 기울었음
예를 들어 ID를 파일명으로 쓰고 싶을 수 있고, 대소문자 비구분 파일 시스템에 묶인 환경일 수도 있음
TypeID는 Crockford 알파벳을 항상 소문자로 쓰는 것이지, Crockford 인코딩 규칙 전체를 쓰는 건 아님
TypeID에는 하이픈이 허용되지 않고, 모호한 문자를 다르게 써서 같은 ID를 여러 방식으로 인코딩하는 것도 허용되지 않음 - 전화로 식별자를 읽어주는 일은 드물다는 데 동의함
하지만 스크린샷이나 화면 공유에 나온 식별자, 혹은 식별자를 쉽게 가져올 수 없는 다른 기기에서 가끔 직접 입력할 때가 있음 - base58이나 KSUID가 쓰는 base62는 장점이 많음
Crockford base32도 동작은 하지만 아주 마음에 들지는 않음
개인적으로는 타입 prefix가 붙은 KSUID를 쓰는 쪽이 1순위임
base62 인코딩의 160비트 K-정렬 가능 ID를 얻을 수 있고, 호환성 때문에 128비트 ID가 꼭 필요한 경우가 아니라면 잘 맞음 - Crockford Base32에서 하이픈이 어디에 있는지 모르겠음: https://en.wikipedia.org/wiki/Base32#Crockford's_Base32
좋아하는 base32 인코딩은 z-base-32인데, 눈에 더 편하다고 느낌: https://philzimmermann.com/docs/human-oriented-base-32-encod...
base58의 가장 큰 문제는 정수에는 잘 맞지만 암호 키 같은 임의의 바이너리 데이터에는 덜 맞고, 대소문자 구분 문자열은 보기 좋지 않다는 점임
- base58이나 base64url도 고민했고, 더 짧은 인코딩은 마음에 들었음
-
깔끔함
“타입 안전” prefix가 마음에 듦
우리 ORM에서는 비슷하게 DB 안의 정수 ID에 엔티티별 태그를 자동으로 붙이는 방식을 tagged ids라고 불렀음: https://joist-orm.io/docs/advanced/tagged-ids
구분자로:를 썼지만, 더블클릭으로 복사/붙여넣기하는 면에서는_를 쓰지 않은 게 조금 후회됨
이론적으로는 Joist가 “DB의 UUID 컬럼”을 “도메인 모델의 TypeID”로 바꾸게 하는 것도 아주 쉬울 텐데, 현재는 사용자 영역에서 설정만으로 되지는 않을 듯함
그래도 좋은 아이디어임- Reddit도 비슷하지만 문자열 길이에 최적화된 방식을 씀
요소 ID는t3_15bfi0같은 형태이고,t3_는 타입 prefix임
t3는 게시물,t1은 댓글,t5는 서브레딧이고 나머지는 자동 증가 기본 키의 base36 인코딩임 - 우리 회사의 타입 있는 내부 ID 시스템은 처음에 콜론을 구분자로 썼지만 곧 점(
.)으로 바꿨음
콜론은 URL 인코딩에서%인코딩되어 ID 길이가 커지고, URL이 못생기고 길어져서 아주 짜증났음
- Reddit도 비슷하지만 문자열 길이에 최적화된 방식을 씀
-
UUIDv7은 몇 년째 HN에서 인기인데, 언제 제대로 된 표준이 되고 라이브러리·데이터베이스·나머지 생태계가 언제 네이티브로 지원할지 궁금함
- 어떤 지원을 기대하는지 모르겠음
대부분의 소프트웨어는 UUID 내부의 특정 비트를 신경 쓰지 않으니 오늘도 쓸 수 있을 것임
특정 비트를 신경 쓰는 소프트웨어가 있다면 UUIDv4처럼 흉내 내면 되고, 그런 비트도 무작위로 생성될 수 있음
생성 절차가 필요하면 직접 쓰면 되며 어렵지 않음 - IETF에서 표준화 최종 단계에 있는 듯함: https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122b...
- 초안과 개선을 거쳐왔고 표준화에 매우 가까워졌으며, 이미 많은 라이브러리가 지원하거나 새 기능을 추가하고 있음
예를 들어 Dart UUID 라이브러리를 관리하는데, 최신 베타 메이저 릴리스에는 v6, v7, 커스텀 v8이 들어 있음
구현 목록도 어딘가에 있고, 해당 페이지에 라이브러리 관리자 명단으로 올라가 있어서 새 초안이 나올 때마다 작성자들에게 알림을 받음
- 어떤 지원을 기대하는지 모르겠음
-
“더블클릭으로 복사/붙여넣기할 수 있다”는 부분이 좋음
디테일이 중요함 -
UUID에 불만이 좀 있음
왜 UUID 버저닝 같은 의식을 치르지 말고 그냥 시간 + 난수를 합치면 안 되는지 모르겠음
지역성이 중요하지 않으면 128비트 난수를 그대로 쓰면 됨
경험상 대부분의 사람이 UUID를 사람이 읽기 좋은 16진수 표현, 심지어 하이픈까지 포함해서 저장해야 한다고 생각하는 듯함
데이터베이스, 네트워크, 메모리에서 공간을 너무 낭비함- 많은 사람이 같은 생각을 했음
예를 들어 ULID https://github.com/ulid/spec는 더 짧고 시간을 저장해서 사전식으로 정렬됨 - UUID 쪽에서는 하나의 사용 사례가 전체를 대표하고, 따라서 최고의 UUID 형식이 하나 있다는 식으로 말하는 경향이 있음
현실은 다른 공학 문제와 같아서, 요구사항을 적고 무엇이 해결해주는지 보면 됨
여러 형식의 장점을 읽는 것은 남들이 어렵게 배운 것을 대신 활용하게 해주므로 도움이 큼
정렬 가능성과 시간 기반 지역성은 자연스럽게 떠올리지 못할 수 있는데, 필요하다면 프로젝트 4년 뒤에 그 데이터를 버렸다는 걸 깨닫는 것보다 미리 아는 게 훨씬 나음
일부 UUID 형식은 작은 보안 문제도 스스로 도입했는데, UUID v1의 MAC 주소 누출 같은 것은 피하는 게 좋음
기존 해법이 맞는 사용 사례라면 그냥 쓰면 됨
그렇지 않다면 크게 걱정하지 않아도 됨
개인적으로 UUID를 가장 많이 쓰는 곳에서는 “고유 문자열만 보내라, 마음이 편하면 원하는 UUID 라이브러리를 써라”라고 지정했음
이 시스템에는 데이터 출처마다 고유 형식이 있는 것 같지만 괜찮음
하루 수만 건 수준이라 규모 문제도 없고, UUID로 정렬할 필요도 없음
실제 식별자라기보다 발신자가 메시지마다 만든 고유 토큰이라, 여러 큐가 있는 이질적인 시스템에서 하류 중복 도착을 감지하는 용도임
전역 고유성도 필요 없고 작은 샤드 안에서만 고유하면 되며, 원칙적으로는 시간이 지나 반복되어도 상관없음
다만 단순성을 위해 전체 기간에서 유일하도록 시스템을 유지하고 있음
직접 생성하는 경우에는/dev/urandom에서 데이터를 읽어 base64로 인코딩하며,==로 끝나지 않는 크기를 골랐음
이것도 실제 문제 때문이 아니라 미관상 이유임 - 시간과 난수를 합친다고 전역 고유성을 보장할 수는 없음
- 개인적으로 가치 있는 UUID 표준은 단순 난수인 v4뿐이라고 봄
그래도 왜 하이픈이 필요한지는 여전히 모르겠고, 다른 버전들은 별 의미를 못 한다고 봄 - 하이픈의 최악은 전체를 더블클릭으로 쉽게 복사/붙여넣기할 수 없다는 점임
- 많은 사람이 같은 생각을 했음
-
UUIDv7이나 UUID가 꼭 필요하지 않다면 https://github.com/segmentio/ksuid가 훨씬 큰 키 공간을 제공함
네임스페이스가 필요하면 문자열 prefix를 붙이면 되고, KSUID 충돌 가능성은 어떤 버전의 UUID보다도 훨씬 작음
정렬 가능한 타임스탬프가 있는 범용 ID 생성기 중에는 ksuid가 가장 좋았고, 대부분의 언어에 라이브러리가 있음
UUID v1~v7은 낭비가 큼- 타입 prefix가 붙은 KSUID를 생성하는 얇은 JavaScript 패키지를 배포해둠
추가 비트를 감당할 수 있다면, 대부분은 그렇겠지만, KSUID는 훌륭한 형식임
[1] https://github.com/sophiabits/resource-id - ULID로 옮긴 이유는 항상 소문자라서 대소문자 비구분으로 다룰 수 있기 때문임
- 인기 있는 ksuid 라이브러리 몇 개를 유지보수하고 있고 실제로 쓰고 있어서 ksuid를 좋아함
다만 ksuid의 큰 문제는 160비트라 데이터베이스의 네이티브 UUID 타입, 예를 들어 PostgreSQL에 맞지 않아 성능 비용이 생긴다는 점임
1: https://github.com/svix/rust-ksuid
2: https://github.com/svix/python-ksuid
- 타입 prefix가 붙은 KSUID를 생성하는 얇은 JavaScript 패키지를 배포해둠
-
K-정렬 가능은 훌륭한 개념이고, 약하게 정렬되는 키는 여러 사용 사례를 해결해줌
타입이 있는 압축 문자열 표현도 마음에 듦
다만 UUID v7의 의도치 않은 부작용으로 보안 문제가 많이 생길지 걱정됨
사람들은 UUID를 토큰으로 쓰면 안 되고 DB의 기본 키도 그렇게 쓰면 안 되지만 실제로는 그렇게 함
UUID v4는 사실상 암호학적 난수라서 지금까지 많은 보안 취약점이 우연히 무사히 넘어갔다고 봄
UUID v7에서는 실제 난수 데이터가 32비트로 돌아가는 줄 알았고, 개발자에게 UUID가 추측 가능하다는 점을 잘 교육해야 한다고 생각했음
수정: v7 UUID의 추측 가능성에 대해 내가 잘못 본 듯함
초안은 난수 비트에 CSPRNG를 권장하고, 엔트로피가 최소 74비트이며 “추측 불가능”하도록 설계됐다고 되어 있음
보안 관련 용도에는 “UUID v4”를 쓰라고 하긴 하지만, 아마 타임스탬프와 관련된 의미일 수 있음- 흥미로운 사용성 문제가 있음
이름이 UUIDv4와 UUIDv7이면, 어느 쪽이 데이터베이스에 좋고 어느 쪽이 일회성 토큰에 좋은지 계속 기억해야 하는 혼란이 끝없이 생기지 않을까 싶음
하위 호환되는 해결책도 잘 모르겠음
Elixir에서는 v4 UUID를 만들 때UUID.uuid4()함수를 씀
이론적으로는 코드에서 사용 여부를 스캔할 수 있겠지만, 이런 것들이 모두 오류 가능성을 높임 - 쓸 만한 경우가 일부 보이긴 하지만, 과거에 v1이나 v5 같은 부분 순차 UUID를 만날 때마다 오용되고 있었음
v3 같은 해시 기반도 마찬가지였음
v4는 단순하고 오용 가능성이 낮음
- 흥미로운 사용성 문제가 있음