UUIDv47: 데이터베이스에는 UUIDv7로 저장하고, 외부에는 UUIDv4로 제공하기
(github.com/stateless-me)- UUIDv47는 데이터베이스에는 정렬 가능한 UUIDv7을 저장하면서, 외부 API에는 UUIDv4처럼 보이는 값을 제공
- 타임스탬프 필드만 XOR 마스킹하여 UUIDv7의 시간 정보를 보호하고, 나머지 랜덤 필드는 그대로 유지함
- SipHash-2-4를 사용한 128비트 키로 마스킹하여, 키 노출 위험 없이 안전하게 정보 보호 가능
- encode/decode가 확정적이고 가역적이며, 랜덤성이 유지되어 충돌 위험이 낮음
- 벤치마크 결과 매우 빠른 성능과 간단한 통합 방법을 제공, PostgreSQL 등 데이터베이스와 쉽게 연동 가능함
프로젝트 개요 및 의의
- UUIDv47은 데이터베이스 내부에는 정렬 및 인덱싱에 유리한 UUIDv7을 저장하면서, 외부 API 및 시스템에는 UUIDv4처럼 보이는 값을 노출함으로써 개인정보 보호와 고성능 처리를 동시에 달성하는 오픈소스 C 라이브러리
- 타 UUID 변환 알고리듬에 비해 가역적 매핑, RFC 호환, 키 회수 불가 보안성, zero-deps, 단순 헤더 파일만 포함하는 구조 등에서차별화된 강점을 갖고 있음
주요 특징
- Header-only C (C89) , 외부 의존성 없이 간단한 통합 가능함
- UUIDv7의 타임스탬프 필드만 XOR 마스킹하여 시간 정보 노출 방지하고, 나머지 랜덤 필드는 변경하지 않음
- 키드 SipHash-2-4로 마스킹하여 128비트 키로 안전하게 정보 보호 가능함
- encode/decode 과정이 결정적이며 완전히 가역적임 (정확히 원형 복구 가능)
- 데이터베이스 저장용(v7)과 외부 노출용(v4) UUID 간 신속 매핑 지원
- 테스트 코드 및 벤치마크 도구 등 풍부한 예제 제공
사용 목적 및 이점
- DB 내 인덱스 locality 및 페이징 효율성을 극대화하는 정렬 가능한 UUIDv7 활용 가능함
- 외부에는 UUIDv4처럼 보이는 패턴만 노출하여 타임스탬프 유출 및 추적 방지
- SipHash를 사용하여 키 회수 불가능, 비밀키 안전 보장
- RFC 호환 버전/변종 비트 처리
- 동작 속도가 빨라 실시간 처리, 대량 생성 환경에서도 효율적임
주요 구조 및 내부 작동 원리
UUIDv7 Layout
- ts_ms_be: 48비트 big-endian 타임스탬프
- ver: high nibble 6번째 바이트 (0x7=DB, 0x4=외부)
- rand_a: 12비트 무작위값
- var: RFC variant (0b10)
- rand_b: 62비트 무작위값
마스킹 및 매핑 로직 (Façade mapping)
- 인코딩: ts48 XOR mask48(R), version=4 설정
- 디코딩: encTS XOR mask48(R), version=7 설정
- 랜덤 필드 변경 없음
- SipHash 인풋으로 10바이트 무작위 필드 사용
- XOR 마스킹은 키를 알면 즉시 역변환 가능
보안 모델
- 목표: 키가 입력을 선택적으로 줘도 노출되지 않는 것
- 구현: SipHash-2-4라는 키드 의사 랜덤 함수(PRF) 사용
- 128비트 키 활용, HKDF 등을 통해 키 파생 권장
- 키 회전 시 UUID 내부엔 저장하지 않고, 별도 작은 키ID 유지 권장
퍼블릭 API (C)
- uuidv47_encode_v4facade : v7→v4 변환
- uuidv47_decode_v4facade : v4→v7 복원
- 기타 버전설정, 파싱, 포매팅 관련 함수 제공
성능 및 벤치마크
- SipHash 마스킹(10B) 연산에서 14ns/op 이하, encode+decode 전체 라운드 트립 33ns/op 수준 (Apple M1 기준)
- 대량 UUID 생성·매핑 시에도 빠른 처리보장
- -O3 -march=native 옵션에서 최적 성능
통합 및 확장
- API 경계에서 encode/decode 처리 권장
- PostgreSQL 연동 시에는 C 확장 작성
- 샤딩할 때는 v4 façade를 xxh3, SipHash 등으로 해싱 가능
기타
- 다른 언어 포트: Go(n2p5/uuid47) 등 제공
- 추천 해시: xxHash는 비 PRF라 정보 유출 우려, SipHash 사용 권장
라이선스
- MIT 라이선스 (Stateless Limited, 2025)
Hacker News 의견
-
안녕하세요, uuidv47의 작성자임. 기본 아이디어는 내부적으로는 UUIDv7을 사용해 데이터베이스 인덱싱과 정렬성을 확보하지만, 외부에는 UUIDv4처럼 보이는 값을 보여줘서 클라이언트에게 타이밍 패턴이 노출되지 않게 하는 것임
작동 방식은 48비트 타임스탬프를 UUID의 랜덤 필드에서 파생한 SipHash-2-4 스트림과 XOR 마스킹하는 방법임
랜덤 비트는 그대로 유지되고, 버전은 내부에서는 7, 외부에서는 4로 전환되며, RFC 변종 값도 유지됨
매핑은 injective함: (ts, rand) → (encTS, rand) 구조임
디코드는 encTS ⊕ mask 방식이어서 완벽한 왕복 변환이 가능함
보안 측면에서는 SipHash가 PRF라서 외부에서 패키지된 값을 본다 해도 키가 노출되지 않음
키가 잘못되면 타임스탬프도 완전히 달라짐
키-ID 외부 관리로 키 롤테이션도 지원 가능함
성능은 10 바이트에 한 번 SipHash, 48비트 로드/스토어 몇 번 정도라서 나노초 수준의 오버헤드이고, C11 헤더 전용, 외부 의존 없고, 할당도 필요 없음
테스트는 SipHash 레퍼런스 벡터, 왕복 인코드/디코드, 버전/변종 불변성 테스트함
피드백이 궁금함-
이 아이디어가 마음에 듦
UUID는 종종 클라이언트 측에서 생성되는데, 이 방식에서는 불가능한 것 같음
혹시 클라이언트가 만든 UUID를 받아가면서 마스킹 버전을 돌려줘도, ts가 다르고 rand가 같은 두 UUID를 누군가 줄 수 있어서 취약점이 생기는 것 아님?
결국 이 방식은 직접 UUIDv7을 생성하는 경우에만 적합한가 궁금함 -
두 가지 의견이 있음
- 이 방식으로 UUID v7의 가치를 남이 더 활용할 수 있는 가능성을 사라지게 해서, API를 쓰는 입장에선 아쉬움
- 외부 API와 내부 저장 방식이 달라지면, 항상 이런 변환 절차를 거쳐야 해서 관리가 살짝 더 까다로워짐
그 정도 번거로움이 감수할 만한 가치가 있는지는 모르겠음
-
가장 큰 우려는 랜덤 비트의 엔트로피 품질임
UUIDv7은 충돌 방지에 더 신경을 쓰기 때문에 예측 가능성보다는 충돌 가능성에 초점이 맞춰져 있음
따라서 RFC 기준으로 비랜덤성에 대해 강제(must)보다는 권장(should)만 명시하며, 약한 PRNG나 카운터 사용, 심지어 랜덤 비트 자리에 추가적인 시계 데이터를 넣는 구현도 있음 (참고: RFC9562 s6.2 & s6.9)
그래서 v7의 rand_a, rand_b를 직접 PRF에 시드로 쓰기에는 신뢰 경계 바깥에서 온 데이터라면 생각보다 위험할 수 있음
PostgreSQL 18의 새 uuidv7()만 해도 고정밀 타임스탬프에서 rand_a를 통째로 채우는데 이것도 RFC 상으론 문제 없음
대량 import에서 생성된 UUID들을 보면, 결국 이 v7-to-v4 방식도 그룹화가 가능해지므로 정보가 노출될 수 있음
엔진 부품 데이터 수집 등은 문제 없겠지만, 사람과 직결된 식별자 데이터라면 주의가 필요함
결과적으로 신뢰할 만한 엔트로피를 직접 보장하지 않는 이상, 이 스킴도 타이밍이나 시리얼, 상관관계 정보 유출 가능성이 있으니 반드시 v7 구현 소스를 직접 확인해야 함 -
좋지 않은 아이디어라고 생각함
PostgreSQL 18에서는 선택적 파라미터 shift가 주어진 간격만큼 타임스탬프를 이동시킴
https://www.postgresql.org/docs/18/functions-uuid.html
-
-
몇 년 전 나만의 스킴을 만들어서 DB에는 순차적으로 증가하는 숫자형 ID를 쓰고, 외부에는 4~20자 길이의 짧은 랜덤 문자열로 노출하는 방법을 적용했었음
이때는 Speck 암호군의 커스텀 인스턴스를 사용했는데, 견고하고 꽤 그럴싸하다고 생각함
완성은 했지만 실제로 쓸 프로젝트를 미뤄뒀어서 발표하지 않았음
올해나 내년에 해당 자료를 정식으로 공개할 예정임
구현 방식, 장단점이 잘 정리된 노트도 있으니 궁금하면 참고 바람
https://temp.chrismorgan.info/2025-09-17-tesid/-
나도 예전에 Speck으로 bigserial PKID를 난독화하려고 시도한 적 있었지만, 크로스플랫폼 구현이 부족했고 특히 pgcrypto에서는 지원이 약해서
base58(AES_K1(id{8} || HMAC_K2(id{8})[0..7]))을 선택했음
결과물은 길이가 보통 22자 정도로 더 길긴 하지만, 거의 모든 환경에서 구현도 가능하고 성능도 충분히 만족스러움 -
좋은 아이디어임
비슷한 개념으로 sqids(이전 명칭: hashids)도 참고할 만함
https://sqids.org/
-
-
예전에 비슷한 경험이 있는데, 공개용 uuid랑 api에 노출되지 않는 bigint PK로 컬럼 두 개를 두고 처리했었음 (uuidv7 나오기 한참 전 얘기임)
uuid 편의성은 좀 부족하지만, PK만 잘 빼낸다면 서로 다른 DB 덤프를 쉽게 병합할 수 있던 점이 장점이었음
해시 기반으로 조회한다 해도 결국 두 개 컬럼을 써야 하긴 할 것 같은데 내가 해시 동작 원리를 잘못 이해한 걸수도 있다는 생각임- 변환은 비밀 크립토그래픽 키로 역전환이 가능함
요청에서 uuidv4 값을 db의 uuidv7로 변경할 수 있음
- 변환은 비밀 크립토그래픽 키로 역전환이 가능함
-
아이디어 자체는 흥미롭지만, 이런 처리를 데이터베이스가 직접 지원해줬으면 좋겠다는 바람이 있음
즉, UUIDv7을 “UUIDv4”와 상호 변환할 수 있고, 쿼리에서도 두 포맷을 명시적으로 구분해 쓸 수 있었으면 좋겠음 -
정말 멋진 프로젝트임
dchest의 siphash 라이브러리를 활용해서 Go 구현체를 만들어 봄
https://github.com/n2p5/uuid47
참고: https://github.com/dchest/siphash -
프로젝트가 흥미로운데, uuid v7에서 타임 부분이 노출될 위험을 실제로 예시로 보여줄 수 있는지 궁금함
-
사용자의 행동 패턴이나 시퀀스가 노출되면 곤란한 상황에 노출될 수 있음
- “전 남편: 너의 dating 사이트 userID를 보니, 톰의 파티에서 계정 만든 게 확실하네?”
- “자기 TZ는 XYZ라고 해놓고, imageID(생성시점 고유) 로그가 항상 새벽 3시로 찍히는 것 같은데?”
개별 메시지나 실시간 거래 등에는 상관 없지만, 사용자 계정 생성이나 장기 데이터를 다룰 때는 누군가 신원 추적에 악용할 수 있음
-
예전에 CTF에서 UUID 일부를 AES 키로 brute force 한 적 있음
키가 타임 소스에서 부분적으로 파생되다 보니, 키 생성 시점의 system time만 알아내면 공격이 가능했음
또 하나의 간단한 예시는 파일 공유 서비스에서 웹사이트.com/GUID 이런 구조만 공개하고, 파일 업로드 시점 정보를 따로 공개하지 않아도
UUIDv7을 쓰면 그 자체로 파일 업로드 시간을 추정할 수 있음
이게 꼭 큰 보안 위협은 아닐 수도 있지만, 의도치 않은 정보 노출임 -
예를 들어 의료 데이터를 저장하는 시스템을 상상해 보면
분석을 위해 MRI 촬영 후 곧바로 결과를 업로드하고, 개인정보를 삭제할 거라고 해도
uuidv7의 타임스탬프로 외부 상관관계 분석을 할 수 있어서, “이 날짜에 MRI 받은 사람은 한 명뿐이니 누구 MRI인지 알아낼 수 있음”
-
-
uuidv7의 가장 불편한 점은 리스트에서 사람이 직접 육안 비교(diff)하기 너무 힘들다는 점임
psql에서 랜덤 비트가 앞으로 가고 실제 정렬은 타임스탬프 기준으로 유지되는 시각화 레이어가 있으면 엄청난 UX 향상이 될 것임-
나는 그냥 UUID의 마지막 부분만 보도록 습관을 들였음
-
직접 함수를 하나 만들어서 쿼리에서 사용하면 됨
예를 들어, hex representation 후 문자열 반전, 아니면 reversed base64로 출력하면 짧고 구분도 쉬울 것임
-
-
이 방식이 매우 괜찮아 보임
하지만 타임스탬프가 노출된다고 너무 호들갑 떠는 것, 그리고 시퀀셜 ID 노출이 공격 노출, 비즈니스 정보 노출과 직결된다는 주장 자체가 진짜 보안 문제라기보다는 괜한 걱정에 가까워 보임
그냥 주기적으로 int값에 랜덤 큰 값을 더해주면, 여전히 단조증가(monotonic) 특성은 유지하면서도 외부 관찰자가 패턴을 파악하기 힘들 것임
결국 중요한 정보 유출을 걱정하는 척 하면서 너무 오버하는 면도 있다고 봄- 여기서 노출되는 건 비즈니스 정보가 아니라 클라이언트 정보임
시스템이 흘리는 정보 자체는 별 의미 없을 수도 있지만, 대량 또는 시계열로 관찰하면 추가 데이터 유추가 가능함
예시로, 데이비드 크라이젤의 SpiegelMining 강연처럼, 신문 기사 날짜, 저자만 긁어도 누가 언제 휴가 가는지 패턴이 추출됨
여러 저자 데이터를 비교하다 보면 사내 연애 등도 모두 들통날 수 있음
- 여기서 노출되는 건 비즈니스 정보가 아니라 클라이언트 정보임
-
왜 세션마다 다른 암호키를 쓰고, 외부에는 암호화된 id만 노출하지 않는지 궁금함
이렇게 하면 DB는 그냥 단순한 순차 id만 써도 되지 않을까 생각임- 토큰에 숨겨진 타임스탬프 비트를 복호화하려면 어떤 키를 써야 하는지 알아야 함
주기적으로 키를 바꿔주면 열쇠 관리가 엄청 복잡해지고, 그때그때 알맞은 키를 어떻게 찾을지도 문제임
- 토큰에 숨겨진 타임스탬프 비트를 복호화하려면 어떤 키를 써야 하는지 알아야 함
-
왜 버전 4 대신 버전 8을 안 썼는지 궁금함
v4는 랜덤 비트라는 의미인데, 실제로는 그다지 랜덤하지 않음
v8은 비트 의미에 대한 제약이 없음- 나도 정답은 모르겠지만, 엔트로피가 높다면 시드 기반 PRNG처럼 볼 수도 있을 것임
이 방식의 목적 자체가 외부에 랜덤하게 보이도록 하는 거라서, 오히려 v8이 더 눈에 띄었을 수도 있다고 생각함
- 나도 정답은 모르겠지만, 엔트로피가 높다면 시드 기반 PRNG처럼 볼 수도 있을 것임