목적: Spring Boot, Java, Kotlin 환경에서 UUID v7을 사용하는 방법에 대해 제공합니다
안녕하세요 :)
최근 정보유출 사건이 많은데요 예전부터 다 털린것 같긴하지만 ..
많은 사건의 원인 중에서 식별값에 대한 의견이 나와서 오랜만에 글을 올려봅니다

저도 여태 개발하면서 아래와 같이 auto increment를 사용해왔었습니다.. ㅜ
- Java
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- Kotlin
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long,
ㅋㅋ 딱히 이슈가 없었거든요..
그냥 주변 개발자들 사이에서 UUID 사용하는게 좋다는 얘기만 들었을 뿐이죠
그래서 지금부터라도 UUID를 왜 사용해야하는지 알아보고 기억하기위해 이렇게 블로그에 기록하려고 합니다
먼저 UUID가 뭔지 알아보겠습니다
UUID란?
UUID(Universally Unique Identifier)는 128비트 길이의 고유 식별자입니다. 분산 시스템에서 중앙 조정 없이도 고유한 ID를 생성할 수 있어 널리 사용됩니다.
왜 UUID가 필요한가?
1. 분산 환경에서의 ID 생성
- 여러 서버나 데이터베이스에서 동시에 ID를 생성해도 충돌 위험이 극히 낮습니다
- 중앙 ID 생성 서버나 DB 시퀀스 없이도 독립적으로 ID 생성 가능
2. 보안과 예측 불가능성
- 순차적인 ID(1, 2, 3...)와 달리 다음 ID를 예측할 수 없습니다
- URL에 노출되어도 전체 데이터 규모나 생성 순서를 추론하기 어렵습니다
3. 데이터 마이그레이션과 병합
- 서로 다른 시스템의 데이터를 병합할 때 ID 충돌 걱정이 없습니다
보시다시피 UUID는 장점이 꽤나 많은 편입니다.. 이걸 왜 안써? 왜 이런걸 알려주는 강의는 없는거지.. 만들어볼까..
UUID의 구조
UUID는 8-4-4-4-12 형식의 16진수로 표현됩니다:
550e8400-e29b-41d4-a716-446655440000
이렇게 UUID 는 RFC 9562에 의해 정의되는데요 가장 널리 사용되고 있는 UUID v4에 대해서 알아보겠습니다
UUID v4
UUID v4는 가장 널리 사용되는 버전으로, 대부분이 랜덤 값으로 생성됩니다.
특징
- 122비트가 랜덤하게 생성(나머지 6비트는 버전/variant 정보)
- 생성이 간단하고 빠름
- 충돌 확률: 약 10억 개를 생성해도 충돌 확률이 10억분의 1정도
- Java 예제
import java.util.UUID;
public class UUIDv4Example {
public static void main(String[] args) {
// UUID v4 생성
UUID uuid = UUID.randomUUID();
System.out.println("UUID v4: " + uuid);
// 여러 개 생성
for (int i = 0; i < 5; i++) {
System.out.println(UUID.randomUUID());
}
}
}
- Kotlin 예제
import java.util.UUID
fun main() {
// UUID v4 생성
val uuid = UUID.randomUUID()
println("UUID v4: $uuid")
// 여러 개 생성
repeat(5) {
println(UUID.randomUUID())
}
}
java util에 있는 UUID 객체는 v4를 지원합니다 그렇지만..! 참 편리하게 쓸 수 있는데 왜 여태 사용을 안해왔을까요?
가장 중요한 이유는 데이터베이스 인덱스 성능 문제가 있기 때문입니다
// 시간 순서대로 생성된 UUID v4들
// 6c84fb90-12c4-11e1-840d-7b25c5ee775a (시간: T1)
// 9c5e1d4c-12c4-11e1-840d-7b25c5ee775a (시간: T2)
// 2f3e4d7a-12c4-11e1-840d-7b25c5ee775a (시간: T3)
```
UUID v4는 랜덤하기 때문에 시간 순서와 무관합니다. 이로 인해:
- B-Tree 인덱스에서 페이지 분할(page split)이 빈번하게 발생
- 인덱스 단편화(fragmentation) 증가
- 삽입 성능 저하
- 캐시 효율성 감소
그래서 이런 문제를 해결하기위해 UUID v4대신 UUID v7을 쓰고 있는데요
UUID v7
UUID v7은 2024년 RFC 9562로 공식 표준화된 최신 버전입니다. 시간 기반 정렬 가능성과 랜덤성을 모두 갖춘 것이 특징입니다.
UUID v7의 구조
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
48비트: Unix timestamp (millisecond 단위)
4비트: 버전 (0111 = v7)
12비트: 랜덤 데이터
2비트: Variant
62비트: 랜덤 데이터
그럼 UUID v7 장점을 정리하자면
- 시간 순서 정렬 가능: 생성 시간 순서대로 정렬됩니다
- DB 인덱스 성능 향상: B-Tree 인덱스에서 순차적 삽입으로 페이지 분할 최소화
- 충분한 랜덤성: 같은 밀리초 내에서도 74비트의 랜덤 값으로 충돌 방지
- 타임스탬프 추출 가능: UUID에서 생성 시간을 복원할 수 있음
구현 예시를 보시겠습니다
UUID v7의 경우 외부 라이브러리를 사용합니다(Hibernate 7 버전에서는 코드에서 설정가능하다고하네요!!)
implementation 'com.github.f4b6a3:uuid-creator:6.1.0'
- Java 예시
import com.github.f4b6a3.uuid.UuidCreator;
import java.util.UUID;
import java.time.Instant;
public class UUIDv7Example {
public static void main(String[] args) {
// UUID v7 생성
UUID uuid = UuidCreator.getTimeOrderedEpoch();
System.out.println("UUID v7: " + uuid);
// 연속 생성 - 시간 순서대로 정렬됨
for (int i = 0; i < 5; i++) {
UUID v7 = UuidCreator.getTimeOrderedEpoch();
System.out.println(v7);
}
// UUID에서 타임스탬프 추출
UUID myUuid = UuidCreator.getTimeOrderedEpoch();
long timestamp = extractTimestamp(myUuid);
Instant instant = Instant.ofEpochMilli(timestamp);
System.out.println("생성 시간: " + instant);
}
// UUID v7에서 타임스탬프 추출
private static long extractTimestamp(UUID uuid) {
long mostSigBits = uuid.getMostSignificantBits();
// 상위 48비트가 타임스탬프
return mostSigBits >>> 16;
}
}
- Kotlin 예시
import com.github.f4b6a3.uuid.UuidCreator
import java.time.Instant
import java.util.UUID
fun main() {
// UUID v7 생성
val uuid = UuidCreator.getTimeOrderedEpoch()
println("UUID v7: $uuid")
// 연속 생성 - 시간 순서대로 정렬됨
repeat(5) {
val v7 = UuidCreator.getTimeOrderedEpoch()
println(v7)
}
// UUID에서 타임스탬프 추출
val myUuid = UuidCreator.getTimeOrderedEpoch()
val timestamp = myUuid.extractTimestamp()
val instant = Instant.ofEpochMilli(timestamp)
println("생성 시간: $instant")
}
// 확장 함수로 타임스탬프 추출
fun UUID.extractTimestamp(): Long {
val mostSigBits = this.mostSignificantBits
// 상위 48비트가 타임스탬프
return mostSigBits ushr 16
}
자 이렇게 라이브러리가 있는 덕분에 저희 개발자들은 아주 편하게 가져다 쓰면 됩니다..!! 그래도 개념은 잘 알아야겠죠?
이젠 Spring Boot에서 Spring Data Jpa를 사용하는 경우 예제를 한번 보시죠
- Java
import jakarta.persistence.*;
import com.github.f4b6a3.uuid.UuidCreator;
import java.util.UUID;
@Entity
@Table(name = "orders")
public class Order {
@Id
@Column(columnDefinition = "UUID")
private UUID id;
private String orderNumber;
@PrePersist
public void generateId() {
if (this.id == null) {
this.id = UuidCreator.getTimeOrderedEpoch();
}
}
// getters, setters...
}
- Kotlin
import jakarta.persistence.*
import com.github.f4b6a3.uuid.UuidCreator
import java.util.UUID
@Entity
@Table(name = "orders")
class Order(
@Id
@Column(columnDefinition = "UUID")
var id: UUID? = null,
var orderNumber: String
) {
@PrePersist
fun generateId() {
if (id == null) {
id = UuidCreator.getTimeOrderedEpoch()
}
}
}
마치며
UUID v7은 UUID v4의 단순성을 유지하면서도 시간 기반 정렬 가능성을 제공하는 현대적인 식별자입니다.
선택 가이드
UUID v4를 선택하는 경우:
- 작은 규모의 애플리케이션
- 생성 시간 정보가 불필요한 경우
- 완전한 랜덤성이 필요한 경우
UUID v7을 선택하는 경우:
- 대용량 트래픽과 데이터를 처리하는 시스템
- DB 인덱스 성능이 중요한 경우
- 시간 순서대로 정렬이 필요한 경우
- 생성 시간 추적이 필요한 경우
추가 고려사항
- 데이터베이스 컬럼 타입: BINARY(16) 또는 UUID 타입 사용을 권장합니다 (문자열보다 저장 공간 절약)
- 인덱스 설계: Primary Key로 UUID v7을 사용하면 클러스터드 인덱스 성능이 크게 향상됩니다
- 마이그레이션: 기존 UUID v4에서 v7로 점진적 전환도 가능합니다
# PostgreSQL
CREATE TABLE orders (
id UUID PRIMARY KEY,
order_number VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
PostgreSQL은 네이티브 UUID 타입을 지원합니다.
# 장점
16바이트로 효율적 저장 (VARCHAR(36)은 36바이트)
UUID 전용 함수와 연산자 지원
인덱스 성능 우수
타입 안정성 (잘못된 형식 입력 방지)
# MySQL
CREATE TABLE orders (
id BINARY(16) PRIMARY KEY,
order_number VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
MySQL은 네이티브 UUID 타입이 없으므로 BINARY(16) 사용을 권장합니다. (MySQL 8.0부터는 UUID 관련 함수가 추가되었습니다)
# 장점
16바이트로 효율적 저장
CHAR(36)보다 2배 이상 저장 공간 절약
인덱스 크기 감소로 성능 향상