BlueNyang
[Spring Boot] Hibernate 6.x에서 UUIDv7 사용하기
Spring

[Spring Boot] Hibernate 6.x에서 UUIDv7 사용하기

BlueNyangBlueNyang
·
·
약 5분
·
# uuidv7# hibernate# b-tree# database-index# querydsl

1. UUID란 무엇인가?

UUID(Universally Unique Identifier)는 네트워크 상에서 고유성을 보장하기 위해 설계된 128비트의 식별자이다. 중앙 집중식 관리 주체 없이도 중복 가능성이 극도로 낮은 ID를 생성할 수 있다는 것이 가장 큰 장점이다. 표준적인 텍스트 표현은 8-4-4-4-12 형태의 36자(하이픈 포함) 문자열이다.

1.1. 주요 버전별 특징

  • v1 (Timestamp): 생성 시점의 타임스탬프와 노드 ID(일반적으로 MAC 주소)를 조합한다. 시간 순서대로 생성되지만, 생성 기기의 MAC 주소가 노출되어 보안 취약점이 존재한다.
  • v4 (Random): 128비트 중 버전과 변트 비트를 제외한 나머지를 완전히 무작위로 생성한다. 구현이 가장 간단하여 널리 쓰이지만, 순서가 무작위이기 때문에 데이터베이스 인덱싱 시 성능 저하를 유발한다.
  • v5 (Name-based): 특정 네임스페이스와 이름을 조합하여 SHA-1 해시값으로 생성한다. 동일한 입력에 대해 항상 동일한 UUID를 보장해야 할 때 사용한다.
  • v7 (Time-ordered): RFC 9562에서 정의된 최신 표준이다. 앞부분 48비트에 Unix Timestamp를 배치하여 **시간 순으로 정렬(Sortable)**이 가능하면서도, 뒷부분에 무작위 값을 추가하여 충돌 가능성을 최소화했다.
게시글 이미지

2. 왜 UUIDv7을 선택해야 하는가?

2.1. DB 인덱스 친화성 (B-Tree Index)

RDBMS의 인덱스는 주로 B-Tree 구조를 사용한다. UUIDv4처럼 완전히 무작위인 값을 PK로 사용하면 새로운 데이터 삽입 시 인덱스 페이지의 임의 위치에 쓰기가 발생하여 '페이지 분할(Page Split)'과 디스크 I/O 부하가 급증한다. 반면, UUIDv7은 시간 순으로 정렬되기 때문에 인덱스 끝에 순차적으로 삽입되어 삽입 성능이 Auto Increment 방식과 유사하게 최적화된다.

UUIDv4 vs UUIDv7 인덱스 삽입 비교
UUIDv4 vs UUIDv7 인덱스 삽입 비교

2.2. 분산 환경의 자율성

Auto Increment는 중앙 DB의 시퀀스에 의존하므로 분산 DB 환경에서 ID 충돌을 방지하기 까다롭다. UUIDv7은 각 서버가 독립적으로 생성해도 전역적인 고유성과 정렬 순서를 보장한다.

분산 시스템에서의 UUIDv7 활용
분산 시스템에서의 UUIDv7 활용

3. 구현 단계

Java 기본 라이브러리(java.util.UUID)는 현재 v4 생성만 지원하며 v7 생성 기능이 포함되어 있지 않다. 따라서 UUIDv7 생성을 지원하는 신뢰할 수 있는 라이브러리를 추가해야 한다. 대표적으로 uuid-creator 라이브러리가 가장 많이 사용된다.

3.1. 의존성 추가

Maven을 사용하는 경우 pom.xml에 다음 의존성을 추가한다.

xml
<dependency>
    <groupId>com.github.f4b6a3</groupId>
    <artifactId>uuid-creator</artifactId>
    <version>6.1.1</version>
</dependency>

Gradle을 사용하는 경우 build.gradle에 다음 의존성을 추가한다.

groovy
implementation 'com.github.f4b6a3:uuid-creator:6.1.1'

3.2. IdentifierGenerator 구현

먼저 UUIDv7을 생성하는 로직을 담은 클래스를 작성한다.

java
public class UuidV7Generator implements IdentifierGenerator {
    @Override
    public UUID generate(SharedSessionContractImplementor session, Object owner) {
        // 내부적으로 UUIDv7 라이브러리(예: f4b6a3/uuid-creator) 등을 사용
        return GUID.v7().toUUID();
    }
}

3.3. 커스텀 어노테이션 정의

Hibernate 6 방식에 맞춰 UuidV7Generator를 연결할 어노테이션을 만든다.

java
@IdGeneratorType(UuidV7Generator.class) // Hibernate 6.x 방식의 커스텀 ID 생성기 연결
@Target({ElementType.METHOD, ElementType.FIELD}) // 어노테이션 적용 대상 지정(필드와 메서드에 사용 가능)
@Retention(RetentionPolicy.RUNTIME) // 런타임 시점까지 어노테이션 정보 유지
public @interface UuidV7 {
}

4. DB 테이블 설계: RAW(16) 활용

UUID를 DB에 VARCHAR(36) 형태로 저장하는 것은 공간 낭비와 성능 저하의 주범이다.

  • VARCHAR(36): 36바이트 점유. 인덱스 크기가 커지고 비교 연산이 느리다.
  • RAW(16) / BINARY(16): 128비트 데이터를 있는 그대로 16바이트에 저장한다. 절반 이상의 공간을 절약할 수 있으며 인덱싱 효율이 극대화된다.

JPA 엔티티에서 UUID 타입을 사용하고 적절한 컬럼 정의를 추가하면 Hibernate가 이를 자동으로 이진 데이터로 매핑한다.


5. 실습: User 엔티티 적용

위에서 정의한 내용들을 종합하여 Users 엔티티를 구성한다.

java
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class Users {

    @Id
    @UuidV7 // 직접 만든 커스텀 어노테이션 적용
    @Column(columnDefinition = "BINARY(16)", updatable = false, nullable = false)
    // 또는
    // @Column(columnDefinition = "RAW(16)", updatable = false, nullable = false)
    private UUID userId;

    @Column(length = 30)
    private String userName;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedAt;

    // 비즈니스 로직에 따른 업데이트 메서드
    public void updateUserName(String newName) {
        this.userName = newName;
    }
}

6. QueryDSL사용 시 주의점

UUIDv7을 도입하여 BINARY(16) 혹은 RAW(16) 형태로 저장했다면, QueryDSL을 통한 조회 시에도 몇 가지 주의할 점이 존재한다. 단순히 Long 타입을 다룰 때와는 다른 특성이 있으므로 이를 명확히 인지해야 성능과 정확성을 모두 잡을 수 있다.

6.1. QueryDSL 조회 시 주요 주의 사항

6.1.1. 타입 불일치 및 자동 형변환 주의

QueryDSL은 QClass를 생성할 때 UUID 필드를 ComparablePath<java.util.UUID>로 생성한다. 조회 조건에 값을 넣을 때 문자열(String)을 그대로 전달하면 안 된다.

  • 잘못된 예: user.userId.eq("018d1234-...") (컴파일 에러 혹은 런타임 타입 미스매치 발생)
  • 올바른 예: user.userId.eq(UUID.fromString("018d1234-..."))

반드시 Java의 UUID 객체로 변환하여 전달해야 Hibernate가 이를 DB의 이진 데이터 형식에 맞춰 바인딩해준다.

6.1.2. 정렬(OrderBy)의 의미 변화

UUIDv4는 무작위 값이므로 orderBy(user.userId.asc())를 수행하면 데이터가 물리적으로 섞여 성능이 저하되고 비즈니스적으로도 의미가 없다. 하지만 UUIDv7은 시간 순서대로 정렬된다.

  • 장점: 별도의 createdAt 컬럼 없이도 PK 정렬만으로 최신순 조회가 가능하다. 인덱스를 그대로 타기 때문에 매우 효율적이다.
  • 주의: 만약 동일한 밀리초(ms) 내에 생성된 데이터들 사이의 정밀한 순서가 중요하다면, 추가적인 정렬 조건이 필요할 수 있으나 일반적인 웹 서비스 수준에서는 PK 정렬만으로 충분하다.

6.1.3. DB별 이진 데이터 표현 차이

DB에 따라 BINARY(16)을 처리하는 방식이 다르다. 이는 QueryDSL 자체의 문제라기보다 JPA와 DB 드라이버 간의 궁합 문제일 가능성이 크다.

  • MySQL: BINARY(16)으로 저장된 값을 SELECT 하면 16진수 형태로 보이지만, JPA는 이를 UUID 객체로 잘 복원한다.
  • H2 (테스트 환경): H2는 UUID 타입을 별도로 지원한다. 개발 환경(H2)과 운영 환경(MySQL/Oracle)의 ID 저장 방식이 다를 경우, 테스트 코드가 실제 환경과 다르게 동작할 수 있으므로 주의해야 한다. 가능하면 테스트 환경에서도 운영 환경과 동일한 이진 매핑 설정을 유지하는 것이 좋다.

6.1.4. 문자열 검색 시 성능 저하 (Like 조회 금지)

UUIDv7은 이진 데이터이다. 만약 특정 UUID의 일부만으로 검색하기 위해 like 연산을 사용하거나 substring 등의 함수를 적용하면 DB는 인덱스를 타지 못하고 Full Table Scan을 수행하게 된다.

  • 피해야 할 코드: user.userId.stringValue().like("018d1234%")
  • 대안: UUID는 원칙적으로 전체 일치(Equal) 검색을 전제로 사용해야 한다. 부분 검색이 필요하다면 별도의 비즈니스 식별자(예: 주문번호, 사용자 ID)를 두는 것이 옳다.

7. 실습: QueryDSL 조회 및 정렬 예시

UUIDv7의 장점을 극대화한 조회 로직의 예시이다.

java
public List<UserResponseDTO> findRecentUsers(int limit) {
    return queryFactory
            .select(Projections.constructor(UserResponseDTO.class,
                    user.userId,
                    user.userName,
                    user.createdAt))
            .from(user)
            // 별도의 createdAt 인덱스 없이 PK(UUIDv7)만으로 최신순 정렬
            .orderBy(user.userId.desc())
            .limit(limit)
            .fetch();
}

8. 활용 방안 및 결론

8.1. 적절한 경우 (추천)

  1. 대규모 분산 시스템: 여러 서비스에서 동시에 데이터를 생성하고 병합해야 하는 경우.
  2. 보안이 중요한 외부 API: ID를 통해 데이터의 총량이나 생성 순서(상대적 순서 외의 구체적 값)를 추측하기 어렵게 해야 할 경우.
  3. 대용량 트래픽: 삽입 성능과 정렬 성능을 동시에 챙겨야 하는 B-Tree 기반의 RDBMS 환경.

8.2. 부적절한 경우 (오버엔지니어링)

  1. 소규모 내부 관리 도구: 단순한 CRUD만 존재하고 데이터 양이 적다면 Long 타입의 Auto Increment가 훨씬 직관적이고 관리가 편하다.
  2. 가독성이 최우선인 경우: URL 파라미터 등에 ID가 노출될 때 UUID는 너무 길고 복잡하여 디버깅이나 사용자 경험 측면에서 불리할 수 있다.
  3. 이미 Sequential ID가 체계화된 환경: 기존 인프라가 숫자로 된 ID에 최적화되어 있다면 무리하게 UUIDv7으로 전환할 실익이 적다.

UUIDv7 도입은 단순한 ID 변경이 아니라 저장 공간 최적화와 인덱스 전략의 변화를 의미한다. 신중한 설계와 테스트를 통해 그 이점을 최대한 활용한다면, 대규모 서비스에서의 성능 향상과 확장성 확보에 큰 도움이 될 것이다.

java
System.out.println("UUIDv7 도입 완료!");
BlueNyang
작성자BlueNyang
라이선스
CC BY NC
BlueNyang

BlueNyang

BlueNyang의 개발 log

카테고리

  • Development
  • Framework
  • Language
  • Dev Tools
  • DevOps & Infra
  • Studies

페이지

© 2026 BlueNyang. All rights reserved.

Made with Nuxt.js and Directus