포스트

UUIDv7

UUIDv7

분산 환경에서 식별자를 생성할 때 UUID는 편리한 선택지다. 별도의 중앙 발급기 없이도 각 애플리케이션에서 충돌 가능성이 매우 낮은 식별자를 만들 수 있기 때문이다.

가장 익숙한 UUIDv4는 대부분의 비트를 무작위 값으로 채운다. 고유성 측면에서는 유리하지만, 연속해서 생성한 값도 서로 멀리 떨어져 있어 생성 시점을 기준으로 정렬할 수 없다. 데이터베이스의 B-tree 인덱스에서도 새로운 값이 임의의 위치에 삽입되는 문제가 있다.

UUIDv7은 이 문제를 개선하기 위해 UUID 앞부분에 Unix Epoch Timestamp를 배치한다. 그래서 흔히 “UUIDv7은 생성 순서를 보장한다”고 설명한다.

하지만 이 문장은 절반만 맞다.

UUIDv7은 시간 정보를 상위 비트에 배치해 시간순으로 정렬하기 쉽게 만든다. 다만 UUIDv7 형식만으로 같은 밀리초 안의 생성 순서나 분산 노드 전체의 순서를 보장하지는 않는다.

I. UUIDv4와 UUIDv7

2024년 5월 발표된 RFC 9562는 기존 RFC 4122를 대체하고 UUIDv6, UUIDv7, UUIDv8을 새롭게 표준화했다.

UUIDv4와 UUIDv7의 가장 큰 차이는 시간 정보의 위치다.

구분UUIDv4UUIDv7
주요 데이터무작위 값Unix Timestamp + 무작위 값 또는 Counter
시간 정보없음앞쪽 48비트
자연 정렬생성 시간과 무관Timestamp 기준 시간순
같은 밀리초의 순서해당 없음Generator 구현에 따라 다름
인덱스 지역성낮음상대적으로 높음

UUIDv4는 버전과 Variant를 제외한 122비트를 무작위 값으로 사용한다. UUIDv7은 앞쪽 48비트를 밀리초 단위 Timestamp로 사용하고, 버전과 Variant를 제외한 나머지 74비트를 무작위 값이나 단조성 보강 필드로 사용한다.

II. UUIDv7의 구조

UUIDv7은 총 128비트이며 다음과 같이 구성된다.

1
2
3
4
 0                                      47 48   51 52          63 64 65 66                         127
+-----------------------------------------+-------+--------------+-----+------------------------------+
|           unix_ts_ms (48bit)            | ver=7 | rand_a (12)  | var |         rand_b (62)          |
+-----------------------------------------+-------+--------------+-----+------------------------------+
필드크기설명
unix_ts_ms48비트1970-01-01 UTC부터 경과한 밀리초
ver4비트UUID Version, 0111
rand_a12비트무작위 값 또는 단조성 보강 값
var2비트RFC 9562 Variant, 10
rand_b62비트무작위 값 또는 Counter

RFC 9562의 공식 테스트 벡터는 다음과 같다.

1
2
3
017f22e2-79b0-7cc3-98c4-dc0c0c07398f
└──────────────┘
  48bit Timestamp

앞의 12개 16진수인 017f22e279b0이 48비트 Timestamp다. 그 다음 16진수 7은 UUID Version을 나타낸다.

Timestamp가 UUID의 최상위 비트부터 Big-endian으로 저장되기 때문에 동일한 UUIDv7 형식끼리 비교하면 Timestamp 값이 커질수록 UUID 값도 커진다. RFC는 UUIDv7이 별도의 파싱 없이 바이트 단위로 정렬되고, 표준 문자열 표현에서도 사전식 정렬이 가능하도록 설계되었다고 설명한다. 다만 데이터베이스에서는 문자 Collation의 영향을 피하고 저장 공간을 줄이기 위해 문자열보다 UUID 전용 자료형을 사용하는 편이 안전하다.

1
2
3
Timestamp T1 < Timestamp T2
             ↓
      UUID(T1) < UUID(T2)

단, 여기서 비교되는 것은 실제 사건의 발생 시점이 아니라 UUID에 기록된 Timestamp 값이다.

III. 무엇이 보장되는가

UUIDv7의 순서에 대한 표현은 다음 세 가지로 나눠야 한다.

구분보장 여부
서로 다른 UUIDv7 Timestamp 값의 정렬보장
같은 밀리초에서 한 Generator의 생성 순서구현 방식에 따라 가능
여러 노드의 전역 생성 순서보장하지 않음
데이터베이스 Transaction Commit 순서보장하지 않음
업무 Event의 인과 순서보장하지 않음

1. 서로 다른 밀리초

두 UUID의 Timestamp가 서로 다르다면 앞쪽 48비트에서 이미 대소 관계가 결정된다.

1
2
[Timestamp: 1725000000000][나머지 비트]
[Timestamp: 1725000000001][나머지 비트]

나머지 80비트의 값과 관계없이 두 번째 UUID가 더 크게 정렬된다. 따라서 UUID에 기록된 밀리초가 다르면 Timestamp 순서는 보장된다.

2. 같은 밀리초

기본적인 UUIDv7 구현은 Timestamp 뒤의 74비트를 무작위 값으로 채울 수 있다. 같은 밀리초에 여러 UUID가 생성되면 Timestamp가 동일하므로 나머지 값이 정렬 순서를 결정한다.

1
2
3
4
5
6
먼저 생성: [Timestamp T][random = F...]
나중 생성: [Timestamp T][random = 1...]

정렬 결과:
[Timestamp T][random = 1...]
[Timestamp T][random = F...]

나중에 생성한 UUID의 무작위 값이 더 작다면 정렬 결과에서는 먼저 나타난다. 즉, UUIDv7의 비트 구조만으로 같은 밀리초의 생성 순서가 보장되는 것은 아니다.

IV. Python으로 같은 밀리초의 정렬 확인

다음 코드는 동일한 Timestamp를 가지면서 rand_a 값만 다른 UUIDv7 두 개를 구성한다. UUIDv7의 비트 구조를 확인하기 위한 예제이며, 실제 Generator로 사용하기 위한 코드는 아니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from uuid import UUID


def build_uuid7(timestamp_ms: int, rand_a: int, rand_b: int) -> UUID:
    assert 0 <= timestamp_ms < (1 << 48)
    assert 0 <= rand_a < (1 << 12)
    assert 0 <= rand_b < (1 << 62)

    value = timestamp_ms << 80
    value |= 0x7 << 76       # Version 7
    value |= rand_a << 64
    value |= 0b10 << 62      # RFC 9562 Variant
    value |= rand_b

    return UUID(int=value)


timestamp_ms = 1_725_000_000_000

first = build_uuid7(timestamp_ms, rand_a=0xF00, rand_b=1)
second = build_uuid7(timestamp_ms, rand_a=0x100, rand_b=2)

print("먼저 생성:", first)
print("나중 생성:", second)
print("정렬 결과:")

for value in sorted([first, second]):
    print(value)

실행 결과는 다음과 같다.

1
2
3
4
5
6
먼저 생성: 0191a203-2200-7f00-8000-000000000001
나중 생성: 0191a203-2200-7100-8000-000000000002

정렬 결과:
0191a203-2200-7100-8000-000000000002
0191a203-2200-7f00-8000-000000000001

두 UUID의 Timestamp는 동일하지만, 두 번째 UUID의 rand_a가 더 작다. 그 결과 나중에 생성한 UUID가 정렬상 먼저 나타난다.

이 예제는 UUIDv7이 시간순 정렬 형식이라는 것과 모든 생성 순서를 보장한다는 것이 서로 다른 의미임을 보여준다.

V. 같은 밀리초에서 단조성을 만드는 방법

RFC 9562는 같은 밀리초에서도 나중에 생성한 UUID가 항상 더 커지도록 만드는 세 가지 방법을 제시한다.

1. 고정 길이 Counter

rand_a의 왼쪽 비트를 Counter로 사용한다. 같은 밀리초에 UUID를 생성할 때마다 Counter를 증가시키고, 다음 밀리초가 되면 다시 초기화한다.

1
2
3
[Timestamp T][Counter 1][Random]
[Timestamp T][Counter 2][Random]
[Timestamp T][Counter 3][Random]

Counter가 Timestamp 바로 뒤에 위치하기 때문에 생성 순서대로 정렬된다.

2. 단조 증가 난수

무작위 값으로 시작하되, 같은 밀리초에서는 이전 값보다 큰 값으로 증가시키는 방식이다. 난수 영역 자체를 무작위로 초기화된 Counter처럼 사용한다.

단순히 항상 1씩 증가시키면 값이 예측하기 쉬워질 수 있으므로, 추측 불가능성이 중요하다면 Counter 정책도 함께 고려해야 한다.

3. Sub-millisecond Timestamp

시스템이 밀리초보다 정밀한 시계를 제공한다면 rand_a의 최대 12비트를 더 세밀한 시간 정보로 사용할 수 있다.

1
[millisecond][sub-millisecond][random]

다만 동일한 Sub-millisecond 구간에서도 여러 UUID가 생성될 수 있다. 엄격한 단조성이 필요하다면 Counter와 함께 사용해야 한다.

Counter가 모두 소진되거나 시스템 시계가 뒤로 이동하는 경우도 처리해야 한다. RFC는 시간이 앞으로 진행될 때까지 기다리거나, 이전 Timestamp를 유지한 채 Counter를 증가시키는 방법 등을 제시한다.

VI. Python 3.14의 uuid7()

Python은 3.14부터 표준 uuid 모듈에 uuid6(), uuid7(), uuid8()을 추가했다.

Python의 uuid.uuid7()은 플랫폼의 Sub-millisecond 정밀도와 관계없이 동작할 수 있도록 42비트 Counter를 사용해 같은 밀리초 안의 단조성을 보강한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import uuid
from datetime import datetime, timezone


ids = [uuid.uuid7() for _ in range(5)]

for value in ids:
    created_at = datetime.fromtimestamp(
        value.time / 1000,
        tz=timezone.utc,
    )
    print(value, created_at)

print("생성 순서와 정렬 순서가 같은가:", ids == sorted(ids))

Python의 UUID 객체는 내부의 128비트 정수인 UUID.int를 기준으로 비교한다. 따라서 정상적으로 전진하는 시계에서 Python 3.14 표준 Generator를 한 프로세스에서 연속 호출하면 생성된 UUID를 그대로 정렬할 수 있다.

여기서 단조성은 Python Generator가 추가로 구현한 특성이다. UUIDv7 형식 자체가 모든 환경에서 동일한 동작을 강제하는 것은 아니다.

위 예제는 Python 3.14 이상에서 실행할 수 있다. 이전 Python 버전에서 외부 라이브러리를 사용한다면 RFC 9562 지원 여부뿐 아니라 같은 밀리초의 Counter, 시계 역행, Process 간 상태 공유 정책도 확인해야 한다.

VII. 분산 환경에서는 왜 전역 순서가 보장되지 않을까

각 노드가 독립적으로 UUIDv7을 생성하면 노드마다 서로 다른 시스템 시계를 사용한다.

1
2
3
4
5
Node A 실제 시각: 12:00:00.100
Node A 시스템 시각: 12:00:00.105

Node B 실제 시각: 12:00:00.101
Node B 시스템 시각: 12:00:00.098

실제로는 Node B의 Event가 나중에 발생했지만, UUID에 기록된 Timestamp는 더 작을 수 있다. NTP 보정, 수동 시각 변경, VM Snapshot 복원 등으로 한 노드의 시점 자체가 뒤로 이동할 수도 있다.

또한 각 노드의 Counter는 서로 공유되지 않는다. 한 프로세스 내부에서 단조성을 보장하는 Generator를 사용하더라도 여러 프로세스와 노드를 합친 전체 순서까지 알 수 있는 것은 아니다.

UUIDv7만으로는 다음 순서를 표현할 수 없다.

  • 여러 노드에서 발생한 Event의 전역 순서
  • Event 사이의 인과관계
  • Message Broker가 처리한 순서
  • 데이터베이스 Transaction의 Commit 순서

이러한 순서가 필요하다면 Database Sequence, Aggregate별 Version, Message Offset처럼 해당 목적에 맞는 별도 값을 사용해야 한다.

VIII. 데이터베이스에서의 장점

UUIDv7의 목적은 정확한 Event 순서를 표현하는 것보다 시간 기준 정렬과 인덱스 지역성을 개선하는 것에 가깝다.

UUIDv4를 B-tree 인덱스의 Key로 사용하면 새로운 값이 인덱스 전체 범위에 흩어져 삽입된다. 반면 UUIDv7은 비슷한 시점에 생성된 값의 앞부분이 같거나 인접하므로 최근 값이 가까운 영역에 모인다.

1
2
3
4
5
UUIDv4
→ 새 Key가 인덱스의 임의 위치에 삽입

UUIDv7
→ 비슷한 시점의 Key가 인접한 범위에 삽입

이는 Page 접근과 Cache 지역성 측면에서 유리할 수 있다. 다만 순차 증가하는 BIGINT와 완전히 같은 삽입 패턴을 보장하는 것은 아니며, 실제 효과는 데이터베이스와 인덱스 구성, 쓰기 동시성에 따라 측정해야 한다.

RFC 9562는 가능한 경우 UUID를 CHAR(36) 같은 문자열보다 기본 128비트 값으로 저장할 것을 권장한다. PostgreSQL 18도 UUIDv7 생성 함수와 UUID 전용 자료형을 제공한다.

UUIDv7이 created_at을 대체할 수 있을까

UUIDv7에서 Timestamp를 추출할 수 있다고 해서 created_at을 제거하는 것은 권장하기 어렵다.

  • UUID의 Timestamp는 식별자 생성 시각이다.
  • 실제 Event 발생 시각과 다를 수 있다.
  • 데이터베이스 Insert 또는 Commit 시각과 다를 수 있다.
  • Generator가 시계 역행을 처리하기 위해 이전 Timestamp를 재사용할 수 있다.
  • 구현에 따라 Timestamp를 보정하거나 의도적으로 이동시킬 수 있다.

RFC도 UUID를 가능한 한 불투명한 식별자로 다루고, 불필요하게 내부 필드를 해석하지 않을 것을 권장한다.

1
2
3
id         → 식별과 인덱스 정렬
created_at → 업무상 생성 시각
sequence   → 엄격한 처리 순서가 필요할 때

UUIDv7의 정렬 특성은 조회와 인덱스에 활용하고, 업무상 시간과 순서는 별도 Column으로 표현하는 편이 안전하다.

IX. 보안상 주의점

UUID는 그 자체로 인증 수단이 아니다. RFC 9562는 UUID를 추측하기 어렵다고 가정하거나, UUID를 알고 있다는 사실만으로 접근 권한을 부여해서는 안 된다고 명시한다.

UUIDv7은 Timestamp와 선택적인 Counter를 포함하므로 대략적인 생성 시각과 순서를 외부에 노출할 수 있다. 생성량이나 생성 시각을 숨겨야 하는 보안 목적의 값이라면 UUIDv7이 적합한지 별도로 검토해야 한다.

1
2
UUIDv7 식별자 ≠ Access Token
UUIDv7 식별자 ≠ 권한 검증

X. 적용 기준

UUIDv7은 다음 조건에서 유용하다.

  • 여러 애플리케이션에서 중앙 조정 없이 식별자를 생성해야 한다.
  • UUID 형식을 유지하면서 시간 기준 정렬이 필요하다.
  • UUIDv4보다 나은 데이터베이스 인덱스 지역성이 필요하다.
  • 대략적인 생성 시간순 조회가 유용하다.

반대로 다음 요구사항을 UUIDv7 하나로 해결해서는 안 된다.

  • 같은 밀리초를 포함한 엄격한 생성 순서
  • 여러 노드의 전역 순서
  • Transaction Commit 순서
  • Event의 인과관계
  • 정확한 업무 발생 시각

정리하면 다음과 같다.

UUIDv7은 시간순으로 정렬 가능한 식별자이지, 전역 순번을 제공하는 Sequence는 아니다.

서로 다른 Timestamp 값은 UUID의 정렬 순서에 반영된다. 같은 밀리초의 단조성은 Generator 구현에 달려 있고, 여러 노드의 전역 순서는 별도의 순서 체계가 필요하다.

XI. Reference

  1. RFC 9562 - UUID Version 7
  2. RFC 9562 - Monotonicity and Counters
  3. RFC 9562 - Distributed UUID Generation
  4. RFC 9562 - Sorting
  5. RFC 9562 - DBMS and Database Considerations
  6. RFC 9562 - Security Considerations
  7. Python 3.14 uuid 모듈
  8. PostgreSQL 18 UUID Functions
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.