
작성 목적: Spring Boot/Hibernate 환경에서 UUID v7을 기본 키로 사용하는 방법과,
__DEFAULT__,__SYSTEM__같은 예약 ID를 안전하게 설계·정렬하는 실무 가이드를 제공합니다.
Tip: 대량 쓰기 중심 테이블의 PK/클러스터드 인덱스에서 유리합니다.
Hibernate 7부터 v7이 정식 옵션입니다.
import org.hibernate.annotations.UuidGenerator;
import jakarta.persistence.*;
@Entity
public class Order {
@Id
@UuidGenerator(style = UuidGenerator.Style.VERSION_7)
private java.util.UUID id;
// ...
}
Hibernate 6.x는 기본 제공이 v4 중심이므로, 라이브러리로 v7을 생성해서 주입합니다. (예: uuid-creator)
Maven
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>uuid-creator</artifactId>
<version>5.3.6</version> <!-- 최신 버전 확인 -->
</dependency>
엔티티
import com.github.f4b6a3.uuid.UuidCreator;
import jakarta.persistence.*;
@Entity
public class Order {
@Id
private java.util.UUID id;
@PrePersist
void init() {
if (id == null) {
id = UuidCreator.getTimeOrdered(); // = UUID v7
}
}
}
pg_uuidv7 확장(또는 사용자 함수)로 DEFAULT uuid_generate_v7() 사용.-- 예: PostgreSQL
CREATE EXTENSION IF NOT EXISTS pg_uuidv7;
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuid_generate_v7(),
-- ...
);
UUID vs StringUUID 타입 사용 권장.uuid, MySQL/MariaDB: BINARY(16)UUID를 자동 문자열 직렬화 → DTO는 String으로 노출하면 충분.// Controller
@GetMapping("/{id}")
public OrderDto get(@PathVariable java.util.UUID id) { ... }
// DTO
public record OrderDto(String id, String name) {
public static OrderDto from(Order e) {
return new OrderDto(e.getId().toString(), e.getName());
}
}
__DEFAULT__, __SYSTEM__ 같은 토큰을 정하고, 고정 네임스페이스와 함께 v5로 변환 → 항상 동일한 UUID.all-1, all-2 같은 패턴값은 비권장(비표준·호환성 위험).00000000-0000-0000-0000-000000000000는 RFC 정의 특수값이라 1개 용도에 한해 허용.is_reserved, reserved_type 등으로 분리.SELECT *
FROM my_table
ORDER BY
(id IN (
'65e71d97-8821-5c05-b3b0-6afba2292c8f', -- DEFAULT
'd180242f-91e4-5c72-909e-d7b98c3625f5', -- SYSTEM
'f3138000-cd4a-5869-9340-bffc90048cec', -- DELETED
'17d8467d-e80e-5a3a-96bc-c837c578530c' -- UNKNOWN
)) DESC,
id ASC;
가장 분명하고 유지보수 친화적인 방법입니다. 컬럼으로 분리했다면
ORDER BY is_reserved DESC, id ASC가 더 깔끔합니다.
주의: 내림차순이면 반대로 맨 아래로 갑니다. 정렬 방향을 고정하세요.
uuid, MySQL/MariaDB BINARY(16)).all-1, all-2 등 패턴값 금지(표준·호환성 위험).UUID 타입으로 유지.Maven
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>uuid-creator</artifactId>
<version>5.3.6</version>
</dependency>
Gradle (Kotlin DSL)
implementation("com.github.f4b6a3:uuid-creator:5.3.6")
// 엔티티: v7 자동 주입 (Hibernate 6.x + uuid-creator)
@Entity
public class Customer {
@Id
private java.util.UUID id;
@Column(nullable = false)
private String name;
@PrePersist
void init() {
if (id == null) id = com.github.f4b6a3.uuid.UuidCreator.getTimeOrdered();
}
}
// DTO: 문자열로 노출
public record CustomerDto(String id, String name) {
public static CustomerDto from(Customer e) {
return new CustomerDto(e.getId().toString(), e.getName());
}
}
import com.github.f4b6a3.uuid.UuidCreator;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.Arrays;
public enum WellKnownId {
DEFAULT("__DEFAULT__"),
SYSTEM("__SYSTEM__"),
DELETED("__DELETED__"),
UNKNOWN("__UNKNOWN__");
private static final UUID NAMESPACE = UUID.fromString("11111111-2222-3333-4444-555555555555");
private final String token;
private final UUID uuid;
WellKnownId(String token) {
this.token = token;
this.uuid = UuidCreator.getNameBasedSha1(NAMESPACE, token); // UUID v5
}
public UUID uuid() { return uuid; }
public String token() { return token; }
private static final Map<UUID, WellKnownId> byUuid =
Arrays.stream(values()).collect(Collectors.toMap(WellKnownId::uuid, e -> e));
public static WellKnownId fromUuid(UUID id) { return byUuid.get(id); }
}
라이브러리별 API가 다르므로 컨셉을 보여주는 예시입니다.
import com.github.f4b6a3.uuid.UuidCreator;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
public final class ReservedV7Factory {
private static final Map<String, Instant> ANCHOR = Map.of(
"__DEFAULT__", Instant.parse("1970-01-01T00:00:00Z"),
"__SYSTEM__", Instant.parse("1970-01-02T00:00:00Z"),
"__DELETED__", Instant.parse("1970-01-03T00:00:00Z"),
"__UNKNOWN__", Instant.parse("1970-01-04T00:00:00Z")
);
public static UUID ofToken(String token) {
Instant t = ANCHOR.get(token);
if (t == null) throw new IllegalArgumentException("Unknown token: " + token);
byte[] rnd = hash128(token);
// uuid-creator에는 직접 랜덤 바이트를 넣는 v7 빌더가 있으므로 해당 API로 치환하세요.
// 여기선 개념적으로 표현:
return buildUuidV7(t, rnd);
}
private static byte[] hash128(String s) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] h = md.digest(s.getBytes(java.nio.charset.StandardCharsets.UTF_8));
return java.util.Arrays.copyOf(h, 16); // 상위 128비트 사용
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static UUID buildUuidV7(Instant instant, byte[] random16) {
// 실사용 시 uuid-creator의 v7 빌더 API 사용 권장.
// 여기서는 v7 비트 배치에 맞춰 조립하는 의사 코드입니다.
long ms = instant.toEpochMilli();
long timeHigh = (ms & 0xFFFFFFFFFFFFL); // 상위 48비트
long most = ((timeHigh & 0xFFFFFFFFFFFFL) << 16);
// version(4 bits) = 0b0111 for v7
most |= (0x7L << 12); // 간략화된 표기(의사 코드)
java.nio.ByteBuffer bb = java.nio.ByteBuffer.wrap(random16).order(java.nio.ByteOrder.BIG_ENDIAN);
long least = bb.getLong();
long mostRand = bb.getLong();
return new UUID(most ^ mostRand, least);
}
}
-- 예약 ID 우선 + v7 타임순
SELECT *
FROM my_table
ORDER BY is_reserved DESC, id ASC;
@Query("""
select e from MyEntity e
order by case when e.isReserved = true then 1 else 0 end desc, e.id asc
""")
List<MyEntity> findAllOrdered();
Q1. 예약 ID를 all-1, all-2 같은 패턴 값으로 써도 되나요?
A. 비권장입니다. UUID의 버전/variant 규격을 위반할 수 있어 도구/라이브러리 간 호환 문제가 납니다.
Q2. 예약 ID가 여러 개인데, 항상 위로 오게 할 수 있나요?
A. 두 가지 방법이 있습니다.
is_reserved 컬럼으로 명시적 정렬. Q3. 엔티티는 UUID, 노출은 String으로?
A. 네. 내부는 타입 안정성과 성능을 위해 UUID, 외부는 문자열로 충분합니다.
Q4. Hibernate 6.x에서 v7을 어떻게?
A. uuid-creator 같은 라이브러리로 v7을 생성해 @PrePersist에서 주입하세요.
Q5. PostgreSQL/마리아DB 권장 매핑은?
A. PostgreSQL: uuid, MySQL/MariaDB: BINARY(16)이 권장입니다.
필요 시: 현재 프로젝트(부트/하이버네이트/DB 조합)에 맞춘 의존성, 설정, 유틸, 마이그레이션 SQL, 테스트 코드까지 즉시 확장 가능합니다.