UUIDv7 doesn't have the properties you think it does
UUIDv7은 생각만큼 많은 속성을 보장하지 않습니다
UUIDv7은 생성 시각을 담고 대체로 정렬되지만, 정확한 시각이나 엄격한 단조 증가를 보장하지 않습니다. 시계가 역행할 때 Python 구현의 타임스탬프가 실제 시각보다 계속 앞서가는 사례를 보이고, 구현별 보장과 전제 조건을 확인하라고 제안합니다.
- 주제
에디터 노트
UUIDv7을 고르는 실용적 이유와 이 글의 비판은 서로 다른 층위에 있습니다. 댓글에서 지적하듯 B-tree 인덱스의 캐시 지역성에는 완벽한 단조 증가가 필요하지 않고, 글쓴이도 대략적인 정렬은 유용하다고 인정합니다. 따라서 엄격한 순서 보장이 없다는 사실만으로 UUIDv7의 효용이 사라지지는 않습니다. 더 직접적인 문제는 같은 UUIDv7이라도 구현과 시계 상태에 따라 타임스탬프 의미가 달라진다는 점입니다. Python 사례는 시계 역행과 높은 생성 부하가 겹칠 때 시각 오차가 누적될 수 있음을 보여줍니다. 구현을 고를 때 RFC 준수만 확인해서는 부족하며, 정렬과 시각 정확도가 필요한 시스템은 해당 구현의 보장 조건까지 따져야 합니다.
AI 요약
UUIDv7은 밀리초 타임스탬프를 포함해 대체로 정렬되는 ID를 만드는 형식입니다. 흔히 생성 시각을 알 수 있고 단조 증가하며 추측하기 어렵다고 소개되지만, 글쓴이는 RFC 9562가 이 속성들을 엄격하게 보장하지 않는다고 설명합니다. 글에서 확실한 속성으로 꼽는 것은 충돌을 피하고 ID를 유일하게 만드는 성질입니다.
타임스탬프는 생성 시각과 다를 수 있습니다
UUIDv7의 타임스탬프를 데이터베이스의 생성 시각 컬럼 대신 쓰면 컬럼이나 인덱스를 줄일 여지가 있습니다. 하지만 RFC 9562는 구현이 타임스탬프를 실제 시스템 시각에서 벗어나게 조정하도록 허용하며, 실제 시각과 얼마나 가까워야 하는지도 정하지 않습니다. 단조 증가를 유지하려고 타임스탬프를 실제 시각보다 앞당길 수도 있습니다. 12비트 카운터를 쓰면 밀리초마다 노드당 4,096개를 넘게 생성할 때 오버플로가 문제가 될 수 있습니다.
Python 구현에서 시계가 역행하는 사례
Python 표준 라이브러리의 uuid.uuid7()은 특정 상황에서 카운터가 넘칠 때 타임스탬프를 증가시킵니다. 글쓴이는 먼저 UUID를 생성한 뒤 시스템 시계를 한 시간 뒤로 돌리는 실험을 제시합니다. Python 3.14에서 생성 부하가 초당 밀리초당 한 개를 넘으면 UUID에 들어간 시각과 시스템 시각의 차이가 처음에는 한 시간 정도였다가 계속 커졌습니다. 제시된 출력에서는 차이가 약 3,695초에서 3,994초로 늘어납니다. 시계가 뒤로 움직이지 않도록 하는 동작이 높은 생성 부하와 결합하면 타임스탬프가 실제 시각을 앞질러 계속 달릴 수 있다는 설명입니다. 낮은 부하에서는 시계가 따라잡는 데 더 오래 걸리지만 같은 runaway 현상은 나타나지 않는다고 덧붙입니다.
글쓴이는 이 동작이 RFC에 어긋나지 않고 다른 구현에서도 나타난다는 관련 논의에는 동의하면서, 구현이 시계 역행을 더 잘 처리하면 좋겠다고 말합니다. 실제 환경에서는 시계가 과거로 움직이거나 현재와 크게 어긋나는 사례가 있다고 지적합니다.
시각 노출과 정렬의 한계
UUIDv7을 외부에 공개하면 ID에 포함된 타임스탬프도 함께 드러납니다. 계정 생성 시각을 공개하지 않는 프로필 URL이 고정밀 타임스탬프를 노출하는 사례를 들지만, 보안상 우선순위는 대체로 낮다고 평가합니다.
UUIDv7 ID가 엄격하게 단조 증가한다는 보장도 없습니다. 노드 간 시계는 충분한 정밀도로 동기화하기 어렵고, 한 노드 안에서도 카운터 없이 밀리초 시각만 사용하는 구현은 짧은 반복문에서 순서가 뒤바뀔 수 있습니다. 구현에 따라 서브밀리초 시각이나 카운터를 써서 더 나은 정렬을 제공할 수 있지만, 이는 구현과 시계 상태에 달렸습니다. 다만 대략적인 정렬만으로도 유용한 경우가 있다고 글쓴이는 인정합니다.
재시작과 구현 선택
시계 역행 뒤에도 이전 타임스탬프보다 작은 값을 쓰지 않으려면 마지막 타임스탬프를 영구 저장하고 재부팅이나 프로세스 재시작 뒤 다시 읽어야 합니다. 글쓴이가 살펴본 구현에서는 그런 저장을 찾지 못했으며, 시계가 역행한 뒤 프로세스가 재시작하면 ID 순서가 뒤바뀔 수 있다고 설명합니다. 반대로 시계가 미래로 잘못 설정되면 마지막 타임스탬프를 지키는 방식이 잘못된 미래 시각을 계속 사용하게 만들고 카운터 오버플로를 일으킬 수도 있습니다.
UUIDv7은 구현자가 여러 기법을 선택하는 형식이므로, RFC 준수 여부만으로 실제 보장 내용을 알 수 없습니다. 구현이 추가로 보장하는 속성, 그 보장이 성립하는 부하와 시계 조건, 타임스탬프를 바꾸는 상황, ID의 추측 가능성을 확인해야 한다고 제안합니다.
Lobsters 반응
- @kevinmehall — UUIDv7을 쓰는 주된 이유는 정렬된 키가 B-tree 인덱스의 캐시 지역성을 높이기 때문이며, 이 이점은 속성이 엄격하게 성립하지 않아도 얻을 수 있습니다.
- @doctor_eval — 맞습니다. 글은 이 점을 언급하지 않지만 제가 UUIDv7을 쓰는 주된 이유입니다. Postgres가 기본 키에 해시 인덱스를 쓸 수 있다면 UUIDv7을 굳이 쓰지 않겠습니다.
- @daanvdk — 글이 이 점을 전혀 언급하지 않는다고 하기는 어렵습니다. 정렬이 엄격한 단조 증가가 아니라고 설명한 뒤에도 대략적인 정렬은 유익하다고 말합니다. 그 이점이 구체적으로 무엇인지 설명하지 않았을 뿐입니다. UUIDv7을 쓰지 말라는 주장이 아니라 속성을 가정할 때 생길 수 있는 함정을 알자는 글이라고 봅니다.
- @kel — 실제로 단조 증가가 필요해서 UUIDv7을 선택한 사람은 만나본 적이 없지만, 제가 만나지 못한 사람이 많을 수도 있습니다.
원문: alexsci.com / 번역·요약: Trawling