Hibernate ID 생성 전략과 Sequence Optimizer
TL;DR
- Hibernate ID 생성 전략은 엔티티 PK를 어떤 방식으로 생성할지 결정하고, Sequence Optimizer는 이 과정의 왕복 비용을 줄인다.
- SEQUENCE 기반 사전 ID 할당은 JDBC Batch를 가능하게 하며 pooled-lo 같은 전략은 성능과 외부 호환성을 동시에 만족시킨다.
- IDENTITY/SEQUENCE/TABLE/AUTO 비교, none·hi/lo·pooled·pooled-lo 동작 차이, Hibernate 6 마이그레이션 포인트를 체계적으로 다룬다.
1. 개념
Hibernate ID 생성 전략은 엔티티 PK를 어떤 방식으로 생성할지 결정하고, Sequence Optimizer는 이 과정의 왕복 비용을 줄인다.
2. 배경
대량 INSERT 환경에서 allocationSize=1은 시퀀스 조회 병목을 유발해 배치 성능을 크게 떨어뜨렸고 이를 보완하는 최적화가 필요해졌다.
3. 이유
SEQUENCE 기반 사전 ID 할당은 JDBC Batch를 가능하게 하며 pooled-lo 같은 전략은 성능과 외부 호환성을 동시에 만족시킨다.
4. 특징
IDENTITY/SEQUENCE/TABLE/AUTO 비교, none·hi/lo·pooled·pooled-lo 동작 차이, Hibernate 6 마이그레이션 포인트를 체계적으로 다룬다.
5. 상세 내용
Hibernate ID 생성 전략과 Sequence Optimizer
작성일: 2026-03-03 카테고리: Backend / Java / JPA / Hibernate 포함 내용: GenerationType, IDENTITY, SEQUENCE, TABLE, allocationSize, Hi/Lo, Pooled, Pooled-Lo, SequenceStyleGenerator, Hibernate 6 마이그레이션, JDBC Batch, INCREMENT BY, new_generator_mappings, UUID v7, TSID, Snowflake ID, 분산 ID 생성
1. ID 생성 전략이란?
핵심 개념
┌─────────────────────────────────────────────────────────────────┐
│ Primary Key 생성 문제 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 모든 DB 테이블의 행(row)은 고유한 식별자(PK)가 필요하다 │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ INSERT INTO users (id, name, email) │ │
│ │ VALUES (???, '김철수', 'kim@example.com'); │ │
│ │ ^^^ │ │
│ │ 이 값을 누가, 언제, 어떻게 결정하나? │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 선택지: │
│ ├── 1) DB가 자동 생성 (AUTO_INCREMENT / IDENTITY) │
│ ├── 2) DB Sequence에서 미리 조회 (SEQUENCE) │
│ ├── 3) 별도 테이블로 관리 (TABLE) │
│ └── 4) 애플리케이션이 직접 생성 (UUID, Snowflake 등) │
│ │
└─────────────────────────────────────────────────────────────────┘
JPA의 4가지 GenerationType
┌─────────────────────────────────────────────────────────────────┐
│ @GeneratedValue(strategy = ?) │
├──────────┬──────────────────────────────────────────────────────┤
│ │ │
│ IDENTITY │ DB의 AUTO_INCREMENT 컬럼 사용 │
│ │ INSERT 실행 후에야 ID를 알 수 있음 │
│ │ → JDBC Batch INSERT 불가능 (치명적 성능 제약) │
│ │ │
│ │ MySQL: AUTO_INCREMENT │
│ │ SQL Server: IDENTITY(1,1) │
│ │ PostgreSQL: SERIAL (내부적으로 sequence 사용) │
│ │ │
├──────────┼──────────────────────────────────────────────────────┤
│ │ │
│ SEQUENCE │ DB Sequence 객체에서 ID를 미리 조회 │
│ │ INSERT 전에 ID 확보 → Batch INSERT 가능 │
│ │ → Hibernate가 가장 권장하는 전략 │
│ │ │
│ │ Oracle, PostgreSQL, SQL Server 2012+, H2 │
│ │ │
├──────────┼──────────────────────────────────────────────────────┤
│ │ │
│ TABLE │ 별도 테이블에 시퀀스 값을 저장 │
│ │ SELECT FOR UPDATE + UPDATE로 값 증가 │
│ │ → 심각한 Lock 경합, 성능 최악 │
│ │ → 특별한 이유 없으면 사용하지 말 것 │
│ │ │
├──────────┼──────────────────────────────────────────────────────┤
│ │ │
│ AUTO │ Hibernate가 DB 방언(Dialect)에 따라 자동 선택 │
│ │ Hibernate 5: 대부분 TABLE → 의도치 않은 성능 저하 │
│ │ Hibernate 6: 대부분 SEQUENCE → 개선됨 │
│ │ │
└──────────┴──────────────────────────────────────────────────────┘
왜 SEQUENCE가 중요한가?
┌─────────────────────────────────────────────────────────────────┐
│ IDENTITY vs SEQUENCE: 배치 성능 차이 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [IDENTITY 전략] - 5개 엔티티 저장 │
│ │
│ App ──INSERT──→ DB (id=1 반환) │
│ App ──INSERT──→ DB (id=2 반환) │
│ App ──INSERT──→ DB (id=3 반환) │
│ App ──INSERT──→ DB (id=4 반환) │
│ App ──INSERT──→ DB (id=5 반환) │
│ │
│ → 5번의 개별 INSERT (배치 불가능) │
│ → INSERT를 실행해야 ID를 알 수 있으므로 Write-Behind 불가 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [SEQUENCE 전략] - 5개 엔티티 저장 │
│ │
│ App ──SELECT nextval──→ DB (id=1) │
│ App ──SELECT nextval──→ DB (id=2) │
│ App ──SELECT nextval──→ DB (id=3) │
│ App ──SELECT nextval──→ DB (id=4) │
│ App ──SELECT nextval──→ DB (id=5) │
│ App ──BATCH INSERT(5건)──→ DB ← 한 번에! │
│ │
│ → ID를 미리 확보하므로 INSERT를 모아서 실행 가능 │
│ → 하지만 여전히 5번의 시퀀스 조회가 필요... │
│ │
│ 이 "시퀀스 조회 횟수"를 줄이는 것이 Optimizer의 역할! │
│ │
└─────────────────────────────────────────────────────────────────┘
2. 등장 배경: 왜 Optimizer가 필요한가?
문제: 시퀀스 조회가 병목이 되는 순간
┌─────────────────────────────────────────────────────────────────┐
│ 대량 INSERT 시나리오: 사용자 10,000명 일괄 등록 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Optimizer 없이 (allocationSize=1)] │
│ │
│ 매 INSERT마다 SELECT nextval('user_seq') 호출 │
│ │
│ 시퀀스 호출: 10,000번 │
│ INSERT 호출: 10,000번 (batch=100이면 100번) │
│ ───────────────────── │
│ 총 DB 왕복: 10,100번 │
│ │
│ Network Latency가 1ms라면 → 시퀀스 조회만 10초 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [Optimizer 사용 (allocationSize=50)] │
│ │
│ 50개 ID를 한 번에 예약하고 메모리에서 할당 │
│ │
│ 시퀀스 호출: 200번 (10,000 ÷ 50) │
│ INSERT 호출: 100번 (batch=100) │
│ ───────────────────── │
│ 총 DB 왕복: 300번 │
│ │
│ → 97% 감소! (10,100 → 300) │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [Optimizer + Batch Size 정렬 (allocationSize=100, batch=100)] │
│ │
│ 시퀀스 호출: 100번 (10,000 ÷ 100) │
│ INSERT 호출: 100번 (batch=100) │
│ ───────────────────── │
│ 총 DB 왕복: 200번 │
│ │
│ → 98% 감소! (10,100 → 200) │
│ │
└─────────────────────────────────────────────────────────────────┘
Hibernate의 역사적 발전
┌─────────────────────────────────────────────────────────────────┐
│ Hibernate ID 생성 전략의 진화 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Hibernate 3.x (2005~) │
│ ├── SequenceHiLoGenerator (레거시) │
│ ├── Hi/Lo 알고리즘 최초 도입 │
│ └── 문제: 외부 시스템과 ID 충돌 │
│ │
│ Hibernate 4.x (2011~) │
│ ├── enhanced.SequenceStyleGenerator 등장 │
│ ├── new_generator_mappings = false (기본값) │
│ │ └── 여전히 레거시 제너레이터 사용 │
│ └── Pooled Optimizer 개념 등장 │
│ │
│ Hibernate 5.x (2015~) │
│ ├── new_generator_mappings = true (기본값 변경!) │
│ │ └── enhanced 제너레이터가 기본으로 │
│ ├── PooledOptimizer가 기본 Optimizer │
│ ├── hibernate_sequence (글로벌 단일 시퀀스) │
│ └── Spring Boot 1.4가 false로 다시 오버라이드 (하위호환) │
│ │
│ Hibernate 6.x (2022~) │
│ ├── hibernate_sequence 제거 → 엔티티별 시퀀스 │
│ │ └── User → user_SEQ, Order → order_SEQ │
│ ├── allocationSize와 INCREMENT BY 불일치 시 에러 │
│ │ └── SchemaManagementException 발생 │
│ ├── new_generator_mappings 설정 제거 │
│ └── 5→6 마이그레이션 시 가장 큰 장애물 중 하나 │
│ │
│ Hibernate 7.0 (예정) │
│ ├── Jakarta Persistence 3.2 표준 준수 │
│ ├── pooled-lo를 기본으로 변경 논의 중 │
│ └── @SequenceGenerator 개선 │
│ │
└─────────────────────────────────────────────────────────────────┘
3. DB Sequence 기초
Sequence란?
┌─────────────────────────────────────────────────────────────────┐
│ Database Sequence 개념 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Sequence = DB가 관리하는 독립적인 카운터 객체 │
│ │
│ 테이블과 독립적으로 존재 │
│ 트랜잭션 롤백해도 시퀀스 값은 돌아가지 않음 (갭 발생 가능) │
│ 여러 테이블이 하나의 시퀀스를 공유할 수 있음 │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ -- PostgreSQL 시퀀스 생성 │ │
│ │ CREATE SEQUENCE user_seq │ │
│ │ START WITH 1 │ │
│ │ INCREMENT BY 50 ← allocationSize과 일치! │ │
│ │ NO MINVALUE │ │
│ │ NO MAXVALUE │ │
│ │ CACHE 1; │ │
│ │ │ │
│ │ -- 다음 값 조회 │ │
│ │ SELECT nextval('user_seq'); → 1 │ │
│ │ SELECT nextval('user_seq'); → 51 │ │
│ │ SELECT nextval('user_seq'); → 101 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 핵심: INCREMENT BY = 50이므로 nextval은 50씩 증가 │
│ Hibernate는 이 간격 사이의 값을 메모리에서 할당한다 │
│ │
└─────────────────────────────────────────────────────────────────┘
JPA @SequenceGenerator 매핑
@Entity
public class User {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "user_gen"
)
@SequenceGenerator(
name = "user_gen",
sequenceName = "user_seq", // DB 시퀀스 이름
allocationSize = 50 // 한 번에 예약할 ID 개수
)
private Long id;
// ...
}
┌─────────────────────────────────────────────────────────────────┐
│ allocationSize의 의미 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ allocationSize = 50 (JPA 기본값) │
│ │
│ 의미: "DB에서 시퀀스 값을 한 번 가져오면, │
│ 메모리에서 50개의 ID를 순차 할당한다" │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ nextval = 1 → 메모리에서 1, 2, 3, ..., 50 할당 │ │
│ │ nextval = 51 → 메모리에서 51, 52, 53, ..., 100 할당 │ │
│ │ nextval = 101 → 메모리에서 101, 102, ..., 150 할당 │ │
│ │ │ │
│ │ DB 호출 50번 → 1번으로 감소 (50배 성능 향상) │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ 주의: allocationSize = 1이면 Optimizer 효과 없음 │
│ 매번 nextval 호출 = allocationSize의 의미가 없어짐 │
│ │
│ ⚠️ 중요: DB의 INCREMENT BY와 반드시 일치시켜야 함 │
│ allocationSize=50이면 → CREATE SEQUENCE ... INCREMENT BY 50 │
│ 불일치 시 Hibernate 6에서 SchemaManagementException 발생 │
│ │
└─────────────────────────────────────────────────────────────────┘
4. Optimizer 4종 상세 분석
전체 비교 요약
┌────────────┬──────────────┬───────────────┬───────────┬───────────────┐
│ Optimizer │ 시퀀스 해석 │ ID 범위 계산 │ 외부호환 │ 상태 │
├────────────┼──────────────┼───────────────┼───────────┼───────────────┤
│ none │ 값 그대로 │ N/A (1:1) │ ✅ 완벽 │ 기본(size=1) │
│ hi/lo │ 버킷 번호 │ hi×size~ │ ❌ 불가 │ 레거시(폐기) │
│ pooled │ 상한값 │ val-size+1~val│ ✅ 가능 │ 현재 기본 │
│ pooled-lo │ 하한값 │ val~val+size-1│ ✅ 가능 │ 권장 (최선) │
└────────────┴──────────────┴───────────────┴───────────┴───────────────┘
4-1. None Optimizer (allocationSize = 1)
┌─────────────────────────────────────────────────────────────────┐
│ None Optimizer │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 동작: 매 persist() 마다 SELECT nextval() 호출 │
│ 시퀀스 값 = 그대로 ID로 사용 │
│ │
│ DB Sequence: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ... │
│ 생성 ID: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ... │
│ ↑ ↑ ↑ ↑ ↑ │
│ 매번 DB 호출 │
│ │
│ 장점: 단순함, ID에 갭이 거의 없음, 외부 시스템 완벽 호환 │
│ 단점: 성능 최악 - INSERT마다 DB 왕복 필요 │
│ │
│ 사용 시기: │
│ ├── 삽입량이 매우 적은 테이블 (설정 테이블 등) │
│ ├── ID 연속성이 비즈니스 요구사항인 경우 │
│ └── allocationSize = 1로 설정하면 자동 적용 │
│ │
│ 설정: │
│ @SequenceGenerator(allocationSize = 1) │
│ DB: CREATE SEQUENCE user_seq INCREMENT BY 1; │
│ │
└─────────────────────────────────────────────────────────────────┘
4-2. Hi/Lo Optimizer (레거시 - 사용 금지)
┌─────────────────────────────────────────────────────────────────┐
│ Hi/Lo Optimizer (레거시) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 핵심 알고리즘: │
│ ├── hi = DB에서 가져온 시퀀스 값 (버킷 번호) │
│ ├── lo = 0부터 incrementSize까지 로컬 카운터 │
│ └── 실제 ID = hi × incrementSize + lo │
│ │
│ 예시 (incrementSize = 3): │
│ │
│ DB Sequence: 0, 1, 2, 3 ... ← INCREMENT BY 1 (주의!) │
│ │
│ hi=0: lo=0 → ID=0, lo=1 → ID=1, lo=2 → ID=2 │
│ hi=1: lo=0 → ID=3, lo=1 → ID=4, lo=2 → ID=5 │
│ hi=2: lo=0 → ID=6, lo=1 → ID=7, lo=2 → ID=8 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ ⚠️ 치명적 문제: 외부 시스템 호환 불가 │
│ │
│ DB 시퀀스 현재 값: 2 │
│ 실제 사용 중인 ID: 0, 1, 2, 3, 4, 5, 6, 7, 8 │
│ │
│ 외부 시스템이 nextval() 호출 → 3 │
│ 외부 시스템이 ID=3으로 INSERT → 충돌! │
│ (Hibernate는 이미 hi=1일 때 ID=3을 사용했음) │
│ │
│ 이유: 시퀀스 값(3)과 실제 ID(3×3=9~11)의 관계를 │
│ 외부 시스템이 알 수 없기 때문 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 결론: │
│ ├── Hibernate 5+ 에서 deprecated │
│ ├── new_generator_mappings=true 시 사용되지 않음 │
│ ├── 단독 시스템에서만 안전 (DB에 직접 INSERT 없을 때) │
│ └── 마이그레이션 필요: pooled 또는 pooled-lo로 전환 │
│ │
│ 마이그레이션 SQL: │
│ ALTER SEQUENCE user_seq RESTART WITH [MAX(id) + 1]; │
│ ALTER SEQUENCE user_seq INCREMENT BY 50; │
│ │
└─────────────────────────────────────────────────────────────────┘
4-3. Pooled Optimizer (현재 기본값)
┌─────────────────────────────────────────────────────────────────┐
│ Pooled Optimizer │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 핵심: 시퀀스 값을 ID 풀의 "상한(upper bound)"으로 사용 │
│ │
│ 알고리즘 (allocationSize = 50): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ nextval() → 1 (첫 호출) │ │
│ │ ID 범위 = [1-50+1, 1] = 사실상 ID=1만 사용 │ │
│ │ │ │
│ │ nextval() → 51 │ │
│ │ ID 범위 = [51-50+1, 51] = [2, 51] │ │
│ │ 메모리에서 2, 3, 4, ..., 51 순차 할당 │ │
│ │ │ │
│ │ nextval() → 101 │ │
│ │ ID 범위 = [101-50+1, 101] = [52, 101] │ │
│ │ 메모리에서 52, 53, 54, ..., 101 순차 할당 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Java 의사코드 (PooledOptimizer.java 기반): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ class PooledOptimizer { │ │
│ │ long hiValue; // DB에서 가져온 시퀀스 값 │ │
│ │ long value; // 현재 할당 중인 ID │ │
│ │ │ │
│ │ long generate() { │ │
│ │ if (hiValue == UNSET) { │ │
│ │ hiValue = nextval(); // DB 호출 │ │
│ │ value = hiValue; // 첫 값 = 상한 │ │
│ │ return value; │ │
│ │ } │ │
│ │ if (value >= hiValue) { // 풀 소진 │ │
│ │ hiValue = nextval(); // DB 호출 │ │
│ │ value = hiValue - incrementSize + 1; │ │
│ │ } │ │
│ │ return value++; │ │
│ │ } │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 장점: │
│ ├── Hi/Lo 대비 외부 시스템 호환 가능 │
│ ├── 시퀀스 값이 실제 ID 범위에 포함됨 │
│ └── DB 호출 횟수 대폭 감소 │
│ │
│ ⚠️ 치명적 버그: 음수 ID 생성 가능! │
│ │
│ 시나리오: 시퀀스를 수동으로 낮은 값으로 리셋한 경우 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 시퀀스 값: 3 (수동 리셋 후) │ │
│ │ allocationSize: 50 │ │
│ │ │ │
│ │ ID 범위 = [3 - 50 + 1, 3] = [-46, 3] │ │
│ │ │ │
│ │ → -46, -45, -44, ... , 0, 1, 2, 3 이 생성됨! │ │
│ │ → 기존 PK와 충돌 가능! │ │
│ │ → Hibernate JIRA HHH-10683 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 이 버그가 발생하는 상황: │
│ ├── DB 마이그레이션 후 시퀀스 리셋 │
│ ├── 테스트 데이터 초기화 │
│ ├── 백업 복원 후 시퀀스 재설정 │
│ └── 개발 환경에서 수동 시퀀스 조작 │
│ │
└─────────────────────────────────────────────────────────────────┘
4-4. Pooled-Lo Optimizer (권장)
┌─────────────────────────────────────────────────────────────────┐
│ Pooled-Lo Optimizer (권장) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 핵심: 시퀀스 값을 ID 풀의 "하한(lower bound)"으로 사용 │
│ │
│ 알고리즘 (allocationSize = 50): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ nextval() → 1 │ │
│ │ ID 범위 = [1, 1+50-1] = [1, 50] │ │
│ │ 메모리에서 1, 2, 3, ..., 50 순차 할당 │ │
│ │ │ │
│ │ nextval() → 51 │ │
│ │ ID 범위 = [51, 51+50-1] = [51, 100] │ │
│ │ 메모리에서 51, 52, 53, ..., 100 순차 할당 │ │
│ │ │ │
│ │ nextval() → 101 │ │
│ │ ID 범위 = [101, 101+50-1] = [101, 150] │ │
│ │ 메모리에서 101, 102, 103, ..., 150 순차 할당 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Java 의사코드 (PooledLoOptimizer.java 기반): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ class PooledLoOptimizer { │ │
│ │ long lastSourceValue; // DB에서 가져온 시퀀스 값 │ │
│ │ long value; // 현재 할당 중인 ID │ │
│ │ long hiValue; // 이 풀의 상한 │ │
│ │ │ │
│ │ long generate() { │ │
│ │ if (lastSourceValue == UNSET │ │
│ │ || value >= hiValue) { // 풀 소진 │ │
│ │ lastSourceValue = nextval(); // DB 호출 │ │
│ │ value = lastSourceValue; // 하한 │ │
│ │ hiValue = lastSourceValue │ │
│ │ + incrementSize - 1; // 상한 │ │
│ │ } │ │
│ │ return value++; │ │
│ │ } │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ Pooled vs Pooled-Lo 핵심 차이: │
│ │
│ 시퀀스 값 = 3, allocationSize = 50 │
│ │
│ Pooled: [3-50+1, 3] = [-46, 3] ← 음수 ID 발생! │
│ Pooled-Lo: [3, 3+50-1] = [3, 52] ← 직관적, 안전 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 왜 Pooled-Lo가 기본값이 아닌가? │
│ │
│ Steve Ebersole (Hibernate ORM Lead): │
│ "pooled-lo should really be the default, but changing it │
│ would break backward compatibility" │
│ │
│ → Quarkus는 자체 설정 레이어에서 pooled-lo를 기본으로 채택 │
│ → Hibernate 7에서 기본값 변경 논의 중 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ Pooled → Pooled-Lo 전환 안전성: │
│ │
│ 기존에 pooled가 시퀀스를 151까지 사용한 상태에서 │
│ pooled-lo로 전환하면: │
│ → nextval() = 151 │
│ → pooled-lo는 [151, 200] 범위를 사용 │
│ → 기존 ID(~150)와 충돌 없음 (갭만 생김) │
│ → 안전하게 전환 가능! │
│ │
│ 설정 방법: │
│ # application.yml │
│ spring.jpa.properties.hibernate.id.optimizer.pooled.preferred: │
│ pooled-lo │
│ │
└─────────────────────────────────────────────────────────────────┘
시각적 비교: 4가지 Optimizer 동작
┌─────────────────────────────────────────────────────────────────┐
│ 4가지 Optimizer 동작 비교 (allocationSize=5) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ DB Sequence (INCREMENT BY 5): 1, 6, 11, 16, 21 ... │
│ │
│ [None] │
│ nextval=1 → ID=1 │
│ nextval=6 → ID=6 ← ID 2,3,4,5 건너뜀! │
│ nextval=11 → ID=11 │
│ → DB 호출: 매번 / 갭: 큼 │
│ │
│ [Hi/Lo] (DB INCREMENT BY 1일 때) │
│ nextval=0 → ID: 0,1,2,3,4 │
│ nextval=1 → ID: 5,6,7,8,9 │
│ nextval=2 → ID: 10,11,12,13,14 │
│ → DB 호출: 5회당 1회 / 외부 호환: ❌ │
│ │
│ [Pooled] │
│ nextval=1 → ID: 1 │
│ nextval=6 → ID: 2,3,4,5,6 │
│ nextval=11 → ID: 7,8,9,10,11 │
│ → DB 호출: 5회당 1회 / 외부 호환: ✅ │
│ │
│ [Pooled-Lo] │
│ nextval=1 → ID: 1,2,3,4,5 │
│ nextval=6 → ID: 6,7,8,9,10 │
│ nextval=11 → ID: 11,12,13,14,15 │
│ → DB 호출: 5회당 1회 / 외부 호환: ✅ / 직관적: ✅ │
│ │
└─────────────────────────────────────────────────────────────────┘
5. Hibernate 6 Breaking Changes
핵심 변경사항
┌─────────────────────────────────────────────────────────────────┐
│ Hibernate 5 → 6 ID 생성 관련 변경 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ① 글로벌 시퀀스 제거 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Hibernate 5: │ │
│ │ 모든 엔티티 → hibernate_sequence (하나의 시퀀스) │ │
│ │ │ │
│ │ User.id ──┐ │ │
│ │ Order.id ──┼──→ hibernate_sequence │ │
│ │ Item.id ──┘ │ │
│ │ │ │
│ │ Hibernate 6: │ │
│ │ 각 엔티티 → 전용 시퀀스 │ │
│ │ │ │
│ │ User.id ──→ user_SEQ │ │
│ │ Order.id ──→ order_SEQ │ │
│ │ Item.id ──→ item_SEQ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 영향: 기존 hibernate_sequence에 의존하던 앱은 │
│ 마이그레이션 시 시퀀스를 새로 생성해야 함 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ ② allocationSize / INCREMENT BY 일치 강제 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Hibernate 5: │ │
│ │ @SequenceGenerator(allocationSize = 50) │ │
│ │ DB: CREATE SEQUENCE user_seq INCREMENT BY 1; │ │
│ │ → 경고 없이 동작 (내부적으로 보정) │ │
│ │ │ │
│ │ Hibernate 6: │ │
│ │ @SequenceGenerator(allocationSize = 50) │ │
│ │ DB: CREATE SEQUENCE user_seq INCREMENT BY 1; │ │
│ │ → SchemaManagementException 발생! │ │
│ │ "Sequence 'user_seq' has increment size 1 │ │
│ │ but allocationSize is 50" │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ 해결 방법: │
│ ├── A) DB 시퀀스 수정: ALTER SEQUENCE user_seq INCREMENT BY 50 │
│ ├── B) allocationSize 수정: @SequenceGenerator(allocSize = 1) │
│ └── C) 검증 비활성화 (비권장): │
│ hibernate.id.sequence.increment_size_mismatch_strategy │
│ = LOG / FIX / NONE │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ ③ new_generator_mappings 설정 제거 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Hibernate 5: │ │
│ │ hibernate.id.new_generator_mappings = true/false │ │
│ │ → false: 레거시 SequenceHiLoGenerator 사용 │ │
│ │ → true: enhanced SequenceStyleGenerator 사용 │ │
│ │ │ │
│ │ Hibernate 6: │ │
│ │ 설정 자체가 제거됨 │ │
│ │ → 항상 enhanced generator만 사용 │ │
│ │ → 레거시 Hi/Lo 사용 불가 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ ④ Spring Boot 3 + Hibernate 6 마이그레이션 체크리스트 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ □ hibernate_sequence → 엔티티별 시퀀스 생성 │ │
│ │ □ INCREMENT BY와 allocationSize 일치 확인 │ │
│ │ □ new_generator_mappings 설정 제거 │ │
│ │ □ Hi/Lo 사용 시 pooled 또는 pooled-lo로 전환 │ │
│ │ □ 시퀀스 현재 값 = MAX(id) + allocationSize 확인 │ │
│ │ □ 테스트 환경에서 ID 생성 검증 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Flyway 마이그레이션 스크립트 예시
-- V10__hibernate6_sequence_migration.sql
-- Hibernate 5 → 6 시퀀스 마이그레이션
-- 1. 기존 글로벌 시퀀스에서 현재 값 확인
SELECT last_value FROM hibernate_sequence;
-- 2. 엔티티별 시퀀스 생성 (현재 최대 ID + allocationSize 기준)
CREATE SEQUENCE user_SEQ
START WITH (SELECT MAX(id) + 50 FROM users)
INCREMENT BY 50
NO MINVALUE NO MAXVALUE CACHE 1;
CREATE SEQUENCE order_SEQ
START WITH (SELECT MAX(id) + 50 FROM orders)
INCREMENT BY 50
NO MINVALUE NO MAXVALUE CACHE 1;
-- 3. 글로벌 시퀀스 제거 (확인 후)
-- DROP SEQUENCE hibernate_sequence;
6. 성능과 Multi-JVM 안전성
성능 벤치마크
┌─────────────────────────────────────────────────────────────────┐
│ 실제 성능 측정 사례 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [사례 1] allocationSize 효과 (10,000건 INSERT) │
│ │
│ allocationSize=1 : DB 호출 10,000회 → ~10초 │
│ allocationSize=50 : DB 호출 200회 → ~0.5초 │
│ allocationSize=100: DB 호출 100회 → ~0.3초 │
│ │
│ → allocationSize=50으로도 20배 성능 향상 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [사례 2] Batch Insert + Optimizer 조합 │
│ │
│ 1,000건 INSERT 기준: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 구성 │ DB 왕복 │ 대비 │ │
│ ├───────────────────────────┼──────────┼─────────────┤ │
│ │ allocSize=1, batch=1 │ 2,000회 │ 기준(100%) │ │
│ │ allocSize=1, batch=100 │ 1,010회 │ 50% │ │
│ │ allocSize=100, batch=1 │ 1,010회 │ 50% │ │
│ │ allocSize=100, batch=100 │ 20회 │ 1% (99%↓) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ → allocSize과 batch_size를 함께 최적화해야 최대 효과 │
│ → 권장: 둘을 같은 값으로 설정 (예: 둘 다 100) │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [사례 3] Hypersistence Batch Sequence Generator │
│ │
│ PostgreSQL generate_series를 활용한 대안: │
│ SELECT nextval('seq') FROM generate_series(1, 5); │
│ → 5개 ID를 한 번의 SQL로 조회 │
│ → DB 시퀀스 INCREMENT BY 1 유지 가능 │
│ → allocationSize 불일치 문제 원천 차단 │
│ │
└─────────────────────────────────────────────────────────────────┘
Multi-JVM 클러스터 안전성
┌─────────────────────────────────────────────────────────────────┐
│ Multi-JVM 환경에서 Optimizer 동작 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [pooled-lo, allocationSize=50] │
│ │
│ JVM-1이 nextval() 호출 → 1 │
│ JVM-1의 ID 풀: [1, 50] │
│ │
│ JVM-2가 nextval() 호출 → 51 │
│ JVM-2의 ID 풀: [51, 100] │
│ │
│ JVM-3가 nextval() 호출 → 101 │
│ JVM-3의 ID 풀: [101, 150] │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ JVM-1 │ │ JVM-2 │ │ JVM-3 │ │
│ │ 1~50 │ │ 51~100 │ │ 101~150 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ DB Sequence │ │
│ │ 현재값: 151 │ │
│ │ (다음 호출 시) │ │
│ └──────────────────┘ │
│ │
│ → 각 JVM은 겹치지 않는 ID 범위를 확보 │
│ → DB 시퀀스의 원자성(atomicity)이 충돌 방지 │
│ → 추가 동기화 메커니즘 불필요 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ ⚠️ ID 갭 발생 패턴: │
│ │
│ JVM-1이 ID 1~30까지만 사용하고 종료 │
│ → 31~50은 영원히 사용되지 않음 (갭) │
│ → 이는 정상 동작이며 문제가 아님 │
│ → PK는 비즈니스 의미를 가지면 안 됨 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 외부 시스템 동시 접근 시: │
│ │
│ 외부 배치 작업이 nextval() 호출 → 151 │
│ → JVM-1~3의 범위(1~150)와 충돌 없음 │
│ → pooled/pooled-lo의 핵심 장점 │
│ → Hi/Lo였다면 충돌 발생했을 것 │
│ │
└─────────────────────────────────────────────────────────────────┘
JDBC Batch 최적화 설정
# application.yml - 최적 성능 설정
spring:
jpa:
properties:
hibernate:
# Batch Insert 활성화
jdbc.batch_size: 100
# 같은 엔티티 타입끼리 모아서 배치
order_inserts: true
order_updates: true
# Pooled-Lo Optimizer 사용
id.optimizer.pooled.preferred: pooled-lo
# Batch versioned data (낙관적 락 사용 시)
jdbc.batch_versioned_data: true
┌─────────────────────────────────────────────────────────────────┐
│ IDENTITY vs SEQUENCE: Batch 동작 차이 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [IDENTITY - Batch 불가능] │
│ │
│ em.persist(user1) → 즉시 INSERT (ID 필요) │
│ em.persist(user2) → 즉시 INSERT (ID 필요) │
│ em.persist(user3) → 즉시 INSERT (ID 필요) │
│ em.flush() → 이미 다 실행됨 │
│ │
│ → Write-Behind 전략 무력화 │
│ → 트랜잭션 끝까지 INSERT 지연 불가 │
│ │
│ [SEQUENCE + Pooled-Lo - Batch 최적화] │
│ │
│ em.persist(user1) → nextval() 1번 → 50개 ID 확보 │
│ em.persist(user2) → 메모리에서 ID 할당 (DB 호출 없음) │
│ em.persist(user3) → 메모리에서 ID 할당 (DB 호출 없음) │
│ ... │
│ em.flush() → BATCH INSERT 한 번에 실행! │
│ │
│ → Hibernate의 Write-Behind 완전 활용 │
│ → 대량 INSERT 성능 극대화 │
│ │
└─────────────────────────────────────────────────────────────────┘
7. 대안 기술: DB Sequence를 넘어서
UUID v7 (RFC 9562, 2024)
┌─────────────────────────────────────────────────────────────────┐
│ UUID v7 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ UUID v4 (기존): 완전 랜덤 │
│ 550e8400-e29b-41d4-a716-446655440000 │
│ → B-tree 인덱스 성능 최악 (랜덤 삽입 = 페이지 스플릿) │
│ │
│ UUID v7 (신규): 시간 기반 정렬 가능 │
│ 018e4c6b-3c00-7000-8000-000000000001 │
│ ├── 48-bit: Unix 밀리초 타임스탬프 │
│ ├── 4-bit: 버전 (7) │
│ ├── 12-bit: 랜덤/카운터 │
│ ├── 2-bit: variant │
│ └── 62-bit: 랜덤 │
│ │
│ 장점: │
│ ├── 시간순 정렬 → B-tree 인덱스 순차 삽입 (성능 우수) │
│ ├── DB 시퀀스 불필요 → 분산 환경에서 충돌 없음 │
│ ├── 애플리케이션에서 생성 → DB 왕복 0회 │
│ └── 128-bit → 사실상 충돌 확률 0 │
│ │
│ 단점: │
│ ├── 16 bytes (BIGINT 8 bytes 대비 2배 저장 공간) │
│ ├── JOIN/FK에서 비교 연산 비용 증가 │
│ ├── 인간이 읽기 어려움 (디버깅 시 불편) │
│ └── 일부 DB에서 UUID 타입 미지원 │
│ │
│ Java 구현: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ // Java 17+ (외부 라이브러리) │ │
│ │ // com.fasterxml.uuid:java-uuid-generator │ │
│ │ UUID id = Generators.timeBasedEpochGenerator() │ │
│ │ .generate(); // UUID v7 │ │
│ │ │ │
│ │ // JPA 매핑 │ │
│ │ @Id │ │
│ │ @GeneratedValue(generator = "uuid7") │ │
│ │ @GenericGenerator(name = "uuid7", │ │
│ │ strategy = "com.example.UUIDv7Generator") │ │
│ │ @Column(columnDefinition = "uuid") │ │
│ │ private UUID id; │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
TSID (Time-Sorted Unique Identifier)
┌─────────────────────────────────────────────────────────────────┐
│ TSID │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Twitter Snowflake ID를 기반으로 한 64-bit ID │
│ → BIGINT 컬럼에 저장 가능 (UUID 대비 50% 공간 절약) │
│ │
│ 구조 (64-bit): │
│ ┌──────────────────────────────────────────────┐ │
│ │ [42-bit 타임스탬프] [22-bit 랜덤/노드] │ │
│ │ │ │
│ │ 42-bit: ~139년 커버 (2^42 밀리초) │ │
│ │ 22-bit: 노드 ID + 카운터 │ │
│ │ → 같은 밀리초 내 4,194,304개 고유 ID │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ ├── 8 bytes (BIGINT) → UUID 대비 50% 절약 │
│ ├── 시간순 정렬 가능 → B-tree 인덱스 순차 삽입 │
│ ├── DB 시퀀스 불필요 → 분산 환경 완벽 지원 │
│ ├── Long 타입 → 기존 BIGINT PK와 호환 │
│ └── 초당 수백만 개 생성 가능 │
│ │
│ 단점: │
│ ├── 69년 후 타임스탬프 오버플로 (2^42 ms ≈ 139년) │
│ ├── 외부 라이브러리 의존 (hypersistence-tsid) │
│ └── 서버 시계 동기화 필요 (NTP) │
│ │
│ Java 구현 (Hypersistence Utils): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ @Id │ │
│ │ @Tsid │ │
│ │ private Long id; │ │
│ │ │ │
│ │ // 또는 문자열로 │ │
│ │ @Id │ │
│ │ @Tsid │ │
│ │ private String id; // "0AWE5R050R08G" │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Snowflake ID (Twitter, 2010)
┌─────────────────────────────────────────────────────────────────┐
│ Snowflake ID │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Twitter가 분산 시스템에서 DB 없이 고유 ID를 생성하기 위해 설계 │
│ │
│ 64-bit 구조: │
│ ┌───┬───────────────────┬─────────┬──────────────┐ │
│ │ 0 │ 41-bit timestamp │ 10-bit │ 12-bit │ │
│ │ │ (밀리초) │ 머신ID │ 시퀀스번호 │ │
│ └───┴───────────────────┴─────────┴──────────────┘ │
│ │
│ 1-bit: 부호 (항상 0, 양수) │
│ 41-bit: 커스텀 에포크 기준 밀리초 (~69년) │
│ 10-bit: 5-bit 데이터센터 + 5-bit 워커 (1024대 머신) │
│ 12-bit: 밀리초 내 시퀀스 (4096/ms = 초당 400만 ID) │
│ │
│ 변형 사례: │
│ ├── Discord: 에포크 기준 변경 (Discord 서비스 시작일) │
│ ├── Instagram: 13-bit 샤드 ID (더 많은 머신 풀) │
│ └── Baidu: uid-generator (Java 구현) │
│ │
│ vs Hibernate Sequence Optimizer: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ │ Sequence Optimizer │ Snowflake ID │ │
│ │ DB 의존 │ 시퀀스 필요 │ DB 불필요 │ │
│ │ 분산 안전 │ 시퀀스 원자성 │ 머신ID 분리 │ │
│ │ 정렬 │ 순차 증가 │ 시간순 정렬 │ │
│ │ 저장 크기 │ 8 bytes │ 8 bytes │ │
│ │ 시계 의존 │ 불필요 │ NTP 필수 │ │
│ │ 생성 속도 │ DB 왕복 필요 │ 메모리 내 즉시 │ │
│ │ 적합 환경 │ 단일/소규모 클러스터│ 대규모 분산 시스템 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
전략 선택 가이드
┌─────────────────────────────────────────────────────────────────┐
│ ID 생성 전략 선택 플로차트 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Q1: 분산 시스템인가? (마이크로서비스, 멀티 리전) │
│ │ │
│ ├── YES → Q2: DB 시퀀스를 쓸 수 있는가? │
│ │ │ │
│ │ ├── YES → pooled-lo + allocationSize=100 │
│ │ │ (검증된 방식, 외부 호환 가능) │
│ │ │ │
│ │ └── NO → Q3: ID 크기 제약이 있는가? │
│ │ │ │
│ │ ├── 8 bytes 제한 → TSID 또는 Snowflake │
│ │ └── 제한 없음 → UUID v7 │
│ │ │
│ └── NO → Q4: 대량 INSERT가 있는가? │
│ │ │
│ ├── YES → SEQUENCE + pooled-lo + batch_size 정렬 │
│ │ │
│ └── NO → Q5: MySQL만 사용하는가? │
│ │ │
│ ├── YES → IDENTITY (MySQL은 시퀀스 미지원) │
│ │ 단, batch INSERT 성능 포기 │
│ │ │
│ └── NO → SEQUENCE + pooled-lo (기본 권장) │
│ │
└─────────────────────────────────────────────────────────────────┘
8. 실전 설정 가이드
Spring Boot + PostgreSQL 권장 설정
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
jpa:
hibernate:
ddl-auto: validate # 운영: validate, 개발: update
properties:
hibernate:
# ===== ID 생성 최적화 =====
# Pooled-Lo를 기본 Optimizer로 사용
id.optimizer.pooled.preferred: pooled-lo
# ===== Batch 최적화 =====
jdbc.batch_size: 50 # allocationSize과 일치 권장
order_inserts: true
order_updates: true
jdbc.batch_versioned_data: true
# ===== Dialect =====
dialect: org.hibernate.dialect.PostgreSQLDialect
엔티티 설정 패턴
// ===== 패턴 1: 기본 SEQUENCE (가장 일반적) =====
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "user_gen")
@SequenceGenerator(name = "user_gen",
sequenceName = "user_seq",
allocationSize = 50)
private Long id;
}
// DB: CREATE SEQUENCE user_seq INCREMENT BY 50;
// ===== 패턴 2: 대량 INSERT 최적화 =====
@Entity
public class EventLog {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "event_gen")
@SequenceGenerator(name = "event_gen",
sequenceName = "event_log_seq",
allocationSize = 200) // 대량 INSERT 대비
private Long id;
}
// DB: CREATE SEQUENCE event_log_seq INCREMENT BY 200;
// application.yml: jdbc.batch_size: 200
// ===== 패턴 3: 외부 연동 (보수적) =====
@Entity
public class Payment {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "payment_gen")
@SequenceGenerator(name = "payment_gen",
sequenceName = "payment_seq",
allocationSize = 1) // 외부 시스템과 완전 호환
private Long id;
}
// DB: CREATE SEQUENCE payment_seq INCREMENT BY 1;
// → 성능 낮지만 ID 갭 없음, 외부 시스템 완벽 호환
// ===== 패턴 4: TSID (분산 환경) =====
@Entity
public class Order {
@Id
@Tsid
private Long id; // DB 시퀀스 불필요, 애플리케이션에서 생성
}
트러블슈팅 가이드
┌─────────────────────────────────────────────────────────────────┐
│ 자주 발생하는 문제 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 문제 1: SchemaManagementException │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ "Schema-validation: sequence [user_seq] defined │ │
│ │ increment [1] does not match allocationSize [50]" │ │
│ └──────────────────────────────────────────────────────┘ │
│ 원인: DB 시퀀스 INCREMENT BY ≠ JPA allocationSize │
│ 해결: ALTER SEQUENCE user_seq INCREMENT BY 50; │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 문제 2: 음수 ID 생성 │
│ 원인: pooled optimizer에서 시퀀스를 낮은 값으로 수동 리셋 │
│ 해결: │
│ ├── A) pooled-lo로 전환 (근본 해결) │
│ │ hibernate.id.optimizer.pooled.preferred: pooled-lo │
│ └── B) 시퀀스 리셋 시 충분히 큰 값으로 설정 │
│ ALTER SEQUENCE user_seq RESTART WITH │
│ (SELECT MAX(id) + 50 FROM users); │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 문제 3: Hibernate 5→6 마이그레이션 후 ID 충돌 │
│ 원인: hibernate_sequence에서 엔티티별 시퀀스 전환 시 │
│ 새 시퀀스 시작 값이 기존 MAX(id)보다 낮음 │
│ 해결: │
│ CREATE SEQUENCE user_SEQ │
│ START WITH (SELECT MAX(id) + 50 FROM users) │
│ INCREMENT BY 50; │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 문제 4: 재시작 후 ID 갭 발생 │
│ 원인: 메모리에 남아있던 미사용 ID가 소멸 │
│ 해결: 이것은 정상 동작이다! │
│ ├── PK에 비즈니스 의미를 부여하지 말 것 │
│ ├── "주문번호"가 필요하면 별도 컬럼 사용 │
│ └── allocationSize를 줄이면 갭이 작아짐 (성능 트레이드오프) │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 문제 5: Batch INSERT가 동작하지 않음 │
│ 확인 포인트: │
│ ├── GenerationType.IDENTITY 사용 중? → SEQUENCE로 전환 │
│ ├── jdbc.batch_size 설정했는가? │
│ ├── order_inserts: true 설정했는가? │
│ └── hibernate.show_sql=true로 실제 쿼리 확인 │
│ → Batch 시 "Executing batch size: N" 로그 확인 │
│ │
└─────────────────────────────────────────────────────────────────┘
9. 전문가 의견과 권장사항
Hibernate 핵심 개발자들의 견해
┌─────────────────────────────────────────────────────────────────┐
│ 전문가 권장사항 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Vlad Mihalcea (Hibernate Developer Advocate): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ "IDENTITY를 절대 사용하지 마라. │ │
│ │ SEQUENCE + pooled optimizer가 최선이다. │ │
│ │ IDENTITY는 Hibernate의 Batch INSERT를 │ │
│ │ 완전히 무력화시킨다." │ │
│ │ │ │
│ │ "allocationSize과 batch_size를 일치시키면 │ │
│ │ DB 왕복을 98% 줄일 수 있다." │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Steve Ebersole (Hibernate ORM Lead): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ "pooled-lo should really be the default. │ │
│ │ But changing it would break backward compatibility. │ │
│ │ It's safe to switch from pooled to pooled-lo — │ │
│ │ it just creates harmless gaps." │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Thorben Janssen (Hibernate Expert): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ "GenerationType.SEQUENCE가 가장 유연하다. │ │
│ │ Hibernate가 INSERT를 지연시키고 │ │
│ │ JDBC Batching을 활용할 수 있게 해준다." │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
종합 판단
┌─────────────────────────────────────────────────────────────────┐
│ 종합 판단 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. pooled-lo를 기본으로 사용하라 │
│ → pooled의 음수 ID 버그는 실제 운영에서 발견하기 어렵다 │
│ → 발견 시점은 이미 데이터가 오염된 후 │
│ → 예방이 훨씬 저렴하다 │
│ │
│ 2. allocationSize은 신중하게 선택하라 │
│ → 50 (JPA 기본값): 대부분의 경우 적절 │
│ → 100~200: 대량 INSERT가 빈번한 로그/이벤트 테이블 │
│ → 1: ID 갭이 절대 허용되지 않는 특수 경우만 │
│ → 너무 크면: 재시작 시 큰 갭, 너무 작으면: DB 호출 증가 │
│ │
│ 3. IDENTITY 전략을 피하되, MySQL은 예외 │
│ → MySQL 8.0도 시퀀스를 지원하지 않음 │
│ → MySQL에서는 IDENTITY가 유일한 선택 │
│ → 대량 INSERT가 필요하면 MySQL 대신 PostgreSQL 고려 │
│ │
│ 4. 분산 환경에서는 TSID를 적극 고려하라 │
│ → DB 시퀀스 = 중앙 집중 → 분산의 적 │
│ → TSID = 8 bytes + 시간순 + 분산 안전 │
│ → UUID v7 = 16 bytes이지만 더 표준적 │
│ │
│ 5. 테스트 환경을 소홀히 하지 마라 │
│ → H2 인메모리로 테스트하면 시퀀스 문제를 놓침 │
│ → TestContainers로 실제 DB(PostgreSQL)에서 검증 │
│ → 시퀀스 INCREMENT BY 불일치는 H2에서 안 잡힘 │
│ │
│ 6. Spring Boot의 역사적 함정을 인지하라 │
│ → Spring Boot 1.4: new_generator_mappings=false 오버라이드 │
│ → Spring Boot 2.x: 이 설정이 남아있을 수 있음 │
│ → Spring Boot 3.x + Hibernate 6: 설정 자체 제거 │
│ → 레거시 프로젝트 마이그레이션 시 반드시 확인 │
│ │
└─────────────────────────────────────────────────────────────────┘
10. 한눈에 보는 요약
┌─────────────────────────────────────────────────────────────────┐
│ 최종 요약 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ GenerationType 선택 │
│ → SEQUENCE (최우선) > IDENTITY (MySQL) > TABLE (사용금지) │
│ │
│ Optimizer 선택 │
│ → pooled-lo (최우선) > pooled > none > hi/lo (사용금지) │
│ │
│ 핵심 설정 3가지 │
│ 1) hibernate.id.optimizer.pooled.preferred: pooled-lo │
│ 2) allocationSize = batch_size (예: 둘 다 50) │
│ 3) DB INCREMENT BY = allocationSize (반드시 일치) │
│ │
│ 분산 환경 대안 │
│ → TSID (8 bytes, 시간순, Long 호환) │
│ → UUID v7 (16 bytes, 표준, 충돌 불가) │
│ → Snowflake (대규모 분산, 커스텀 필요) │
│ │
│ Hibernate 6 마이그레이션 필수 작업 │
│ → hibernate_sequence → 엔티티별 시퀀스 │
│ → INCREMENT BY와 allocationSize 일치 확인 │
│ → TestContainers로 실제 DB 검증 │
│ │
│ 절대 하지 말 것 │
│ X IDENTITY + Batch INSERT 기대 │
│ X Hi/Lo + 외부 시스템 접근 │
│ X pooled + 수동 시퀀스 리셋 │
│ X allocationSize ≠ INCREMENT BY │
│ X H2에서만 시퀀스 테스트 │
│ │
└─────────────────────────────────────────────────────────────────┘
11. 역사적 기원과 용어 어원
Surrogate Key의 학술적 기원
┌─────────────────────────────────────────────────────────────────┐
│ Surrogate Key 학술적 기원 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [1976] Hall, Owlett, Todd — "Relations and Entities" │
│ │
│ 최초의 학술적 정의: │
│ "시스템이 생성하는 값으로 실제 엔티티를 표현한다" │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Entity ──(시스템 생성 값)──→ Surrogate Key │ │
│ │ │ │
│ │ 사용자 지정 키가 아닌, 시스템이 부여한 대리자 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [1979] E.F. Codd — ACM TODS 논문 │
│ "Extending the Database Relational Model to Capture │
│ More Meaning" │
│ │
│ 사용자 제어 키(User-Controlled Key)의 3가지 문제: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 문제 1: 변경 가능성 │ │
│ │ → 사용자 키는 언제든 변경될 수 있다 │ │
│ │ → 변경 시 모든 참조를 갱신해야 함 │ │
│ │ │ │
│ │ 문제 2: 관계 간 불일치 │ │
│ │ → 서로 다른 관계에서 동일 엔티티를 식별 불가 │ │
│ │ │ │
│ │ 문제 3: 키 할당 전후 존재 문제 │ │
│ │ → 엔티티가 키 할당 전에 존재할 수 있는가? │ │
│ │ → 키 없는 엔티티는 DB에서 표현 불가 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ → 해결책: 시스템이 할당하는 surrogate 제안 │
│ → 오늘날 모든 Auto-ID 전략의 이론적 근거 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 한국어 용어: │
│ 대리키(代理鍵) = Surrogate Key │
│ 자연키(自然鍵) = Natural Key │
│ │
└─────────────────────────────────────────────────────────────────┘
Database Sequence의 탄생
┌─────────────────────────────────────────────────────────────────┐
│ Database Sequence의 역사 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [1988] Oracle 6 — SEQUENCE 최초 도입 │
│ → Transaction Processing Option에서 등장 │
│ → DB 수준에서 고유 번호를 원자적으로 생성 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [MySQL] AUTO_INCREMENT │
│ → 초기 버전부터 지원 │
│ → IDENTITY는 MySQL 3.23에서 synonym으로 추가 │
│ → MySQL은 2024년 현재도 독립 SEQUENCE 객체 미지원 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [2016] SQL:2016 표준 │
│ → GENERATED ALWAYS AS IDENTITY 공식화 │
│ → 시퀀스 기반 자동 ID 생성의 표준 문법 확립 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [2017] PostgreSQL 10 │
│ → SQL 표준 IDENTITY 컬럼 지원 │
│ → 기존 SERIAL 대비 표준 준수 + 관리 편의 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 타임라인: │
│ │
│ 1988 초기 2016 2017 │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ Oracle 6 MySQL SQL:2016 PostgreSQL 10 │
│ SEQUENCE AUTO_INC IDENTITY IDENTITY 컬럼 │
│ │
└─────────────────────────────────────────────────────────────────┘
Hi-Lo 알고리즘의 기원
┌─────────────────────────────────────────────────────────────────┐
│ Hi-Lo 알고리즘의 역사 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [~1997] Scott Ambler — ORM 논문 │
│ → "DB 카운터 워드 + 메모리 카운터 워드" 복합 키 생성 기술 │
│ → Hi-Lo의 개념적 선조 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [2001] Gavin King — Hibernate 1.0 출시 │
│ → SequenceHiLoGenerator 포함 │
│ → ORM에서 DB 왕복을 줄이는 최초의 실용적 구현 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ "Hi"와 "Lo"의 의미: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Hi = DB에서 온 고차(high-order) 컴포넌트 │ │
│ │ Lo = 메모리의 저차(low-order) 컴포넌트 │ │
│ │ │ │
│ │ 최종 ID = Hi × maxLo + Lo │ │
│ │ │ │
│ │ ┌───────────────┬───────────────┐ │ │
│ │ │ Hi (DB) │ Lo (메모리) │ │ │
│ │ │ 고차 비트 │ 저차 비트 │ │ │
│ │ └───────────────┴───────────────┘ │ │
│ │ │ │
│ │ → 2차원 복합 구조에서 유래한 이름 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [2004] "Hibernate in Action" │
│ → Christian Bauer & Gavin King 공저 │
│ → Hi-Lo를 광범위하게 문서화 │
│ → 이 책으로 Hi-Lo가 Java 생태계에 널리 알려짐 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [2007] Steve Ebersole — pooled optimizer 도입 │
│ → Hibernate 3.2.3 │
│ → Hi-Lo의 "외부 시스템 호환 불가" 문제를 해결 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ Tom Anderson의 구분: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Hi-Lo: │ │
│ │ → 키 공간을 2차원(곱셈)으로 취급 │ │
│ │ → ID = Hi × maxLo + Lo │ │
│ │ → 시퀀스 값으로 원래 ID 역추적 불가 │ │
│ │ │ │
│ │ Linear Block Allocation (pooled): │ │
│ │ → 키 공간을 1차원 수직선으로 취급 │ │
│ │ → 시퀀스 값 = 블록 경계 │ │
│ │ → 외부에서 nextval() 호출해도 안전 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
UUID의 역사
┌─────────────────────────────────────────────────────────────────┐
│ UUID/GUID의 역사 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [~1988] Apollo Network Computing System 1.5 │
│ → UUID류 식별자 최초 등장 │
│ → 분산 네트워크에서 고유 식별 필요에서 탄생 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [1990s] OSF DCE (Distributed Computing Environment) │
│ → UUID 표준화 │
│ → Microsoft가 "GUID" (Globally Unique Identifier)로 채택 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [2002] Jimmy Nilsson — COMB GUID │
│ → UUID에 타임스탬프를 삽입하는 기법 │
│ → UUID v7의 선조 (20년 앞서감) │
│ → 랜덤 UUID의 B-tree 분산 문제를 최초로 해결 시도 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [2005] RFC 4122 (Paul Leach, Rich Salz) │
│ → UUID v1~v5 IETF 표준화 │
│ → v1: 타임스탬프+MAC, v4: 랜덤 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [2016] Alizain Feerasta — ULID 스펙 발표 │
│ → 시간순 정렬 가능 + 문자열 정렬 호환 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [2021] Brad Peabody & Kyzer Davis — IETF 초안 제출 │
│ → 16개 이상의 독립적 시간순 ID 구현이 표준 필요성 입증 │
│ → uuid6, uuid7, uuid8 제안 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [2024] RFC 9562 — UUID v6, v7, v8 공식 표준화 │
│ → v7: 밀리초 타임스탬프 + 랜덤 (가장 권장) │
│ → COMB GUID에서 시작된 22년의 여정이 표준으로 결실 │
│ │
│ 타임라인: │
│ │
│ 1988 1990s 2002 2005 2016 2021 2024 │
│ │ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ ▼ ▼ │
│ Apollo OSF COMB RFC ULID IETF RFC │
│ NCS DCE GUID 4122 초안 9562 │
│ │
└─────────────────────────────────────────────────────────────────┘
Snowflake ID의 탄생 (Twitter, 2010)
┌─────────────────────────────────────────────────────────────────┐
│ Snowflake ID의 탄생 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 배경: MySQL → Cassandra 마이그레이션 │
│ → 분산 환경에서 고유 ID를 생성해야 함 │
│ → 기존 AUTO_INCREMENT는 단일 노드 전용 │
│ │
│ 요구사항: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. 64비트 (BIGINT 호환) │ │
│ │ 2. 대략 시간순 (k-sorted) │ │
│ │ 3. 초당 수만 개 생성 가능 │ │
│ │ 4. 데이터센터 간 고가용성 │ │
│ │ 5. DB에 의존하지 않는 독립적 생성 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 영향을 받은 시스템들: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 연도 │ 시스템 │ 특징 │ │
│ ├──────┼─────────────────────┼───────────────────────┤ │
│ │ 2011 │ Instagram │ PL/pgSQL로 구현 │ │
│ │ 2015 │ Discord │ epoch=2015-01-01 │ │
│ │ - │ Baidu UidGenerator │ 커스텀 비트 레이아웃 │ │
│ │ - │ Meituan Leaf │ 이중 버퍼 전략 │ │
│ │ - │ Sony Sonyflake │ 10비트 머신ID │ │
│ └──────┴─────────────────────┴───────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
한국 커뮤니티 용어
┌─────────────────────────────────────────────────────────────────┐
│ 한국어-영어-한자 용어 대조표 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┬──────────────────────┬────────────────┐ │
│ │ 한국어 │ 영어 │ 한자 │ │
│ ├──────────────┼──────────────────────┼────────────────┤ │
│ │ 기본키 │ Primary Key │ 基本鍵 │ │
│ │ 대리키 │ Surrogate Key │ 代理鍵 │ │
│ │ 자연키 │ Natural Key │ 自然鍵 │ │
│ │ 복합키 │ Composite Key │ 複合鍵 │ │
│ │ 채번 전략 │ ID Generation Strategy│ 採番戰略 │ │
│ │ 시퀀스 │ Sequence │ (외래어) │ │
│ │ 자동 증가 │ Auto Increment │ 自動增加 │ │
│ │ 분산 ID 생성 │ Distributed ID Gen. │ 分散 ID 生成 │ │
│ └──────────────┴──────────────────────┴────────────────┘ │
│ │
│ 참고: "채번(採番)"은 일본어 "採番(さいばん)"에서 유래한 │
│ 한국 IT 업계 관용어로, "번호를 채취한다"는 의미이다. │
│ │
└─────────────────────────────────────────────────────────────────┘
12. DB 레벨 CACHE vs 애플리케이션 레벨 Pre-allocation
두 최적화의 차이
┌─────────────────────────────────────────────────────────────────┐
│ DB CACHE vs App Pre-allocation │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ DB CACHE (시퀀스 캐시) │ │
│ │ → DB 서버 메모리에 시퀀스 값을 미리 할당 │ │
│ │ → 디스크 I/O 절감 │ │
│ │ → 네트워크 왕복은 여전히 필요 │ │
│ │ │ │
│ │ App Pre-allocation (Hi-Lo / Pooled) │ │
│ │ → 앱 서버 메모리에 ID 범위를 할당 │ │
│ │ → 네트워크 왕복 절감 │ │
│ │ → DB 디스크 I/O는 여전히 발생 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 핵심: 둘은 다른 레이어이므로 동시 사용 가능하고 권장됨 │
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ App Server │ │ DB Server │ │
│ │ ┌──────────┐ │ 네트워크│ ┌──────────┐ │ │
│ │ │ Pooled │──┼────────┼→│ CACHE │ │ │
│ │ │ (ID 범위)│ │ 왕복 │ │(SGA/메모리)│ │ │
│ │ └──────────┘ │ ←절감→ │ └─────┬────┘ │ │
│ └───────────────┘ │ │←절감→ │ │
│ │ ┌────┴─────┐ │ │
│ │ │ 디스크 │ │ │
│ │ │ (SEQ$) │ │ │
│ │ └──────────┘ │ │
│ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Oracle SEQUENCE CACHE
┌─────────────────────────────────────────────────────────────────┐
│ Oracle SEQUENCE CACHE 상세 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ NOCACHE: │
│ → 매 nextval() 호출마다 SEQ$ 딕셔너리 테이블에 쓰기 │
│ → 디스크 I/O가 병목 │
│ │
│ CACHE N: │
│ → N개를 SGA(System Global Area) 메모리에 미리 할당 │
│ → N번째마다 디스크 동기화 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 벤치마크 (10,000건 INSERT): │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 설정 │ 소요 시간 │ 배수 │ │
│ ├─────────────────────┼────────────┼───────────────┤ │
│ │ NOCACHE │ ~58초 │ 기준 (1x) │ │
│ │ CACHE 20 (기본) │ ~30초 │ 1.9x 빠름 │ │
│ │ CACHE 1000 │ ~18초 │ 3.2x 빠름 │ │
│ │ CACHE 50000 │ ~16초 │ 3.6x 빠름 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ RAC (Real Application Clusters) 환경: │
│ → CACHE + NOORDER 권장 │
│ → ORDER 사용 시 노드 간 직렬화(cross-instance ping)로 │
│ 성능 심각하게 저하 │
│ │
│ 권장 DDL: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ CREATE SEQUENCE my_seq CACHE 1000 NOORDER; │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
PostgreSQL SEQUENCE
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL SEQUENCE CACHE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ CACHE 파라미터: │
│ → 기본값 1 = 캐시 없음 (매번 WAL 쓰기) │
│ → 세션 단위 캐시: 각 세션이 CACHE 크기만큼 선점 │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Session A: CACHE 20 → ID 1~20 선점 │ │
│ │ Session B: CACHE 20 → ID 21~40 선점 │ │
│ │ Session A: ID 1, 2, 3... 순차 사용 │ │
│ │ Session B: ID 21, 22, 23... 순차 사용 │ │
│ │ │ │
│ │ → 전체적으로 보면 1, 21, 2, 22... 순서 섞임 │ │
│ │ → 각 세션 내에서는 순차 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 권장: 고빈도 INSERT 테이블은 CACHE 20~100 │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ALTER SEQUENCE my_table_id_seq CACHE 50; │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
MySQL AUTO_INCREMENT
┌─────────────────────────────────────────────────────────────────┐
│ MySQL AUTO_INCREMENT 락 모드 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ DB 레벨 캐시 개념 없음 (AUTO_INCREMENT 자체가 테이블 수준) │
│ │
│ innodb_autoinc_lock_mode 3가지: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 모드 │ 이름 │ 동작 │ │
│ ├──────┼───────────────┼────────────────────────────┤ │
│ │ 0 │ Traditional │ 문장 끝까지 테이블 락 유지 │ │
│ │ │ │ → 가장 안전하지만 느림 │ │
│ │ │ │ │ │
│ │ 1 │ Consecutive │ 벌크 INSERT만 테이블 락 │ │
│ │ │ (5.x 기본) │ → 단순 INSERT는 뮤텍스 │ │
│ │ │ │ │ │
│ │ 2 │ Interleaved │ 테이블 락 없음, 뮤텍스만 │ │
│ │ │ (8.0+ 기본) │ → 가장 빠르나 갭 가능 │ │
│ └──────┴───────────────┴────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ MySQL 8.0 개선: │
│ → AUTO_INCREMENT 카운터를 redo log에 영속화 │
│ → 서버 재시작 시 카운터 유실 버그 수정 │
│ → 5.x에서는 재시작 시 MAX(id)+1로 재계산 → ID 재사용 가능 │
│ │
└─────────────────────────────────────────────────────────────────┘
조합 매트릭스
┌─────────────────────────────────────────────────────────────────┐
│ DB CACHE × App Pre-allocation 조합 효과 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┬──────────────┬─────────────────────────┐ │
│ │ DB 기능 │ 앱 기능 │ 결과 │ │
│ ├──────────────┼──────────────┼─────────────────────────┤ │
│ │ CACHE 없음 │ Pooled 없음 │ 매 INSERT마다 │ │
│ │ │ │ 디스크+네트워크 왕복 │ │
│ │ │ │ → 최악 │ │
│ ├──────────────┼──────────────┼─────────────────────────┤ │
│ │ CACHE 있음 │ Pooled 없음 │ 디스크 절감 │ │
│ │ │ │ 네트워크는 매번 │ │
│ │ │ │ → 보통 │ │
│ ├──────────────┼──────────────┼─────────────────────────┤ │
│ │ CACHE 없음 │ Pooled 있음 │ 네트워크 절감 │ │
│ │ │ │ 디스크는 매번 │ │
│ │ │ │ → 보통 │ │
│ ├──────────────┼──────────────┼─────────────────────────┤ │
│ │ CACHE 있음 │ Pooled 있음 │ 디스크+네트워크 │ │
│ │ │ │ 모두 절감 │ │
│ │ │ │ → 최적 ★ │ │
│ └──────────────┴──────────────┴─────────────────────────┘ │
│ │
│ → 운영 환경에서는 반드시 둘 다 활성화할 것 │
│ │
└─────────────────────────────────────────────────────────────────┘
13. DB별 Auto-ID 메커니즘 상세 비교
PostgreSQL: SERIAL vs IDENTITY vs SEQUENCE
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL Auto-ID 메커니즘 비교 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┬──────────────┬───────────────┬────────────┐ │
│ │ 특성 │ SERIAL │ GENERATED AS │ SEQUENCE │ │
│ │ │ │ IDENTITY │ (독립) │ │
│ ├──────────────┼──────────────┼───────────────┼────────────┤ │
│ │ SQL 표준 │ X │ O (SQL:2003) │ O │ │
│ │ │ (PG 전용) │ │ │ │
│ ├──────────────┼──────────────┼───────────────┼────────────┤ │
│ │ 지원 시작 │ PG 1.x │ PG 10 (2017) │ 초기부터 │ │
│ ├──────────────┼──────────────┼───────────────┼────────────┤ │
│ │ 컬럼 삭제 시 │ 시퀀스 │ 시퀀스 함께 │ 수동 정리 │ │
│ │ │ 고아됨 │ 삭제 │ │ │
│ ├──────────────┼──────────────┼───────────────┼────────────┤ │
│ │ ALWAYS 강제 │ X │ O (GENERATED │ N/A │ │
│ │ │ │ ALWAYS) │ │ │
│ ├──────────────┼──────────────┼───────────────┼────────────┤ │
│ │ 2024 권장 │ 신규 코드 │ ★ 권장 │ 멀티 테이블│ │
│ │ │ 사용 금지 │ │ 공유 시 │ │
│ └──────────────┴──────────────┴───────────────┴────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 권장 DDL: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ CREATE TABLE users ( │ │
│ │ id BIGINT GENERATED ALWAYS AS IDENTITY, │ │
│ │ name TEXT NOT NULL │ │
│ │ ); │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ BIGINT 사용 이유: │
│ → 4바이트 추가 비용으로 오버플로우 영구 방지 │
│ → INT (4B): 최대 ~21억 → 대규모 서비스에서 도달 가능 │
│ → BIGINT (8B): 최대 ~922경 → 사실상 무한 │
│ │
└─────────────────────────────────────────────────────────────────┘
MySQL AUTO_INCREMENT 특성
┌─────────────────────────────────────────────────────────────────┐
│ MySQL AUTO_INCREMENT 특성 상세 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Gap(갭) 발생 원인: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. 트랜잭션 롤백 │ │
│ │ → AUTO_INCREMENT 값은 롤백되지 않음 │ │
│ │ │ │
│ │ 2. Mixed-mode INSERT │ │
│ │ → INSERT INTO t (id, name) VALUES (100, 'a') │ │
│ │ → 다음 AUTO_INC = 101 (강제 점프) │ │
│ │ │ │
│ │ 3. 벌크 INSERT │ │
│ │ → lock_mode=1에서 다중 행 INSERT 시 여분 할당 │ │
│ │ │ │
│ │ 4. 서버 재시작 (MySQL 5.x) │ │
│ │ → MAX(id)+1로 재계산 │ │
│ │ → DELETE 후 재시작하면 ID 재사용 가능 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ MySQL 8.0 이전 vs 이후: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ MySQL 5.x: │ │
│ │ → 재시작 시 MAX(id)+1에서 카운터 재계산 │ │
│ │ → DELETE 후 재시작 = ID 재사용 가능 (위험!) │ │
│ │ │ │
│ │ MySQL 8.0: │ │
│ │ → redo log에 카운터 영속화 │ │
│ │ → 재시작해도 카운터 유지 │ │
│ │ → ID 재사용 버그 완전 수정 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 분산 한계: │
│ → 본질적으로 단일 노드 (AUTO_INCREMENT = 중앙 집중) │
│ → Galera Cluster에서는 우회 방법 사용: │
│ auto_increment_increment = N (노드 수) │
│ auto_increment_offset = 노드 번호 │
│ → 예: 3노드 → 노드1: 1,4,7... 노드2: 2,5,8... 노드3: 3,6,9 │
│ │
└─────────────────────────────────────────────────────────────────┘
Oracle SEQUENCE 고급
┌─────────────────────────────────────────────────────────────────┐
│ Oracle SEQUENCE 고급 기능 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [2013] Oracle 12c — IDENTITY 컬럼 지원 │
│ → 내부적으로 시퀀스를 자동 생성 │
│ → GENERATED ALWAYS AS IDENTITY │
│ → PostgreSQL/SQL 표준과 동일 문법 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [2018] Oracle 18c — Scalable Sequences │
│ → 고동시성 환경용 │
│ → 시퀀스 번호 앞에 인스턴스 번호를 붙여 경합 제거 │
│ → RAC에서 시퀀스 핫 블록 문제 해결 │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ CREATE SEQUENCE my_seq │ │
│ │ SCALE EXTEND │ │
│ │ CACHE 1000 │ │
│ │ NOORDER; │ │
│ │ │ │
│ │ → SCALE: 인스턴스별 접두사 추가 │ │
│ │ → EXTEND: 자릿수 확장 (오버플로우 방지) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
14. 대안 ID 전략 심화: 비트 레이아웃과 성능
UUID v1 비트 레이아웃 (128비트)
┌─────────────────────────────────────────────────────────────────┐
│ UUID v1 비트 레이아웃 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 128비트 구조: │
│ │
│ ┌────────────┬──────────┬──────┬────────────┐ │
│ │ time_low │ time_mid │ ver │ time_high │ │
│ │ (32비트) │ (16비트) │=0001 │ (12비트) │ │
│ │ │ │(4비트)│ │ │
│ ├────────────┴──────────┴──────┴────────────┤ │
│ │ var=10 │ clock_seq │ node/MAC │ │
│ │ (2비트)│ (14비트) │ (48비트) │ │
│ └───────────────────────────────────────────┘ │
│ │
│ 타임스탬프: 1582년 10월 15일부터 100나노초 단위 │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 문제 1: time_low가 먼저 오므로 │ │
│ │ 가장 빠르게 변하는 비트가 앞에 위치 │ │
│ │ → B-tree 인덱스에서 정렬 불가 │ │
│ │ │ │
│ │ 문제 2: MAC 주소 노출 │ │
│ │ → 보안/개인정보 위험 │ │
│ │ → 서버 하드웨어 정보 유출 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ → UUID v6이 이 문제를 해결 (타임스탬프 비트 재배치) │
│ │
└─────────────────────────────────────────────────────────────────┘
UUID v4 비트 레이아웃 (128비트)
┌─────────────────────────────────────────────────────────────────┐
│ UUID v4 비트 레이아웃 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 128비트 구조: │
│ │
│ ┌──────────────────────┬──────┬────────────┐ │
│ │ random_a │ ver │ random_b │ │
│ │ (48비트) │=0100 │ (12비트) │ │
│ │ │(4비트)│ │ │
│ ├──────────────────────┴──────┴────────────┤ │
│ │ var=10 │ random_c │ │
│ │ (2비트)│ (62비트) │ │
│ └──────────────────────────────────────────┘ │
│ │
│ 유효 랜덤 비트: 122비트 │
│ → 6비트는 버전(4) + 변형(2)에 사용 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ B-tree 영향: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 100만 레코드 기준 페이지 분할: │ │
│ │ │ │
│ │ 순차 ID (BIGINT) : ~10-20회 │ │
│ │ UUID v4 (랜덤) : ~5,000-10,000+회 │ │
│ │ │ │
│ │ → 약 500배 차이! │ │
│ │ │ │
│ │ 원인: 완전 랜덤이므로 B-tree 리프 노드 전체에 │ │
│ │ 균일하게 분산 → 지속적 페이지 분할 + 캐시 미스 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 결론: PK로 사용 금지. 세션 토큰/API 키에만 적합 │
│ │
└─────────────────────────────────────────────────────────────────┘
UUID v7 비트 레이아웃 (128비트) — 2024 표준
┌─────────────────────────────────────────────────────────────────┐
│ UUID v7 비트 레이아웃 (RFC 9562) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 128비트 구조: │
│ │
│ ┌──────────────────────┬──────┬────────────┐ │
│ │ unix_ts_ms │ ver │ rand_a │ │
│ │ (48비트) │=0111 │ (12비트) │ │
│ │ │(4비트)│ │ │
│ ├──────────────────────┴──────┴────────────┤ │
│ │ var=10 │ rand_b │ │
│ │ (2비트)│ (62비트) │ │
│ └──────────────────────────────────────────┘ │
│ │
│ 48비트 밀리초 타임스탬프 → 서기 10889년까지 사용 가능 │
│ 74비트 랜덤 → 같은 밀리초 내 충돌 확률 극소 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ v4 대비 장점: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. 시간순 정렬 가능 → B-tree 친화적 │ │
│ │ 2. 타임스탬프 추출 가능 → created_at 대체 가능 │ │
│ │ 3. 분산 환경에서 조율 불필요 │ │
│ │ 4. MAC 주소 미노출 → v1 보안 문제 해결 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ PostgreSQL 18: 네이티브 uuidv7() 함수 지원 예정 │
│ │
│ 저장 시 주의: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ O BINARY(16) 또는 UUID 타입 → 16바이트 │ │
│ │ X CHAR(36) → 36바이트 (2.25배) │ │
│ │ │ │
│ │ → CHAR(36)은 저장 공간 2.25배 낭비 │ │
│ │ → 인덱스 크기도 2.25배 증가 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
ULID 구조 (128비트, Crockford Base32 26자)
┌─────────────────────────────────────────────────────────────────┐
│ ULID 구조 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 128비트 구조: │
│ │
│ ┌──────────────────────┬───────────────────────────┐ │
│ │ Timestamp │ Randomness │ │
│ │ (48비트, ms) │ (80비트) │ │
│ └──────────────────────┴───────────────────────────┘ │
│ │
│ Crockford Base32 인코딩: 26자 │
│ → I, L, O, U 제외 (시각적 혼동 방지) │
│ → 0123456789ABCDEFGHJKMNPQRSTVWXYZ │
│ │
│ 예시: 01ARZ3NDEKTSV4RRFFQ69G5FAV │
│ ├──────────┤├──────────────┤ │
│ Timestamp Randomness │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ Monotonic ULID: │
│ → 같은 밀리초 내에서 랜덤 부분 +1 증가 │
│ → 밀리초 내 순서 보장 │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 같은 ms 내 생성 시: │ │
│ │ 01ARZ3NDEK_TSAAAAAAAA (1번째) │ │
│ │ 01ARZ3NDEK_TSAAAAAAAB (2번째, 랜덤+1) │ │
│ │ 01ARZ3NDEK_TSAAAAAAAC (3번째, 랜덤+2) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ UUID v7 대비 장점: │
│ → 문자열 정렬이 시간순과 일치 │
│ → UUID v7은 바이트 순서 이해 필요 (하이픈 위치 때문) │
│ → Crockford Base32로 10자 절약 (36자 → 26자) │
│ │
└─────────────────────────────────────────────────────────────────┘
Snowflake ID 비트 레이아웃 (64비트)
┌─────────────────────────────────────────────────────────────────┐
│ Snowflake ID 비트 레이아웃 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 64비트 구조: │
│ │
│ ┌────┬──────────────┬──────────┬────────┬──────────┐ │
│ │sign│ timestamp │datacenter│ worker │ sequence │ │
│ │ =0 │ (41비트,ms) │ (5비트) │(5비트) │ (12비트) │ │
│ │(1) │ │ │ │ │ │
│ └────┴──────────────┴──────────┴────────┴──────────┘ │
│ │
│ Twitter epoch: 2010-11-04T01:42:54.657Z │
│ │
│ 용량 계산: │
│ → 워커당: 4,096 ID/ms (12비트 시퀀스) │
│ → 1,024 노드: 32 데이터센터 × 32 워커 │
│ → 시스템 전체: ~400만 ID/ms │
│ → 41비트 타임스탬프: ~69년 (2010+69 = 2079년까지) │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 변형 구현들: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Instagram 변형 (2011): │ │
│ │ ┌────────────┬───────────┬──────────┐ │ │
│ │ │ timestamp │ shard_id │ sequence │ │ │
│ │ │ (41비트) │ (13비트) │ (10비트) │ │ │
│ │ └────────────┴───────────┴──────────┘ │ │
│ │ → PL/pgSQL로 구현 (DB 레벨) │ │
│ │ → 8,192 샤드 × 1,024 ID/ms │ │
│ │ │ │
│ │ Discord 변형 (2015): │ │
│ │ → epoch = 2015-01-01 │ │
│ │ → 워커+프로세스 ID로 노드 구분 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
TSID 구조 (64비트, Crockford Base32 13자)
┌─────────────────────────────────────────────────────────────────┐
│ TSID 구조 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 64비트 구조: │
│ │
│ ┌──────────────────┬──────────┬──────────────┐ │
│ │ timestamp │ node │ counter │ │
│ │ (42비트, ms) │ (10비트) │ (12비트) │ │
│ └──────────────────┴──────────┴──────────────┘ │
│ │
│ 기본 epoch: 2020-01-01 UTC │
│ → 42비트: ~139년 (2020+139 = 2159년까지) │
│ │
│ Crockford Base32 인코딩: 13자 │
│ → 예: 0AWE5HZP3SKTK │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ Java 라이브러리: │
│ → tsid-creator (원본) │
│ → hypersistence-tsid (Vlad Mihalcea) │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ ⚠️ JavaScript 주의: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ JavaScript의 정수 한계: 53비트 (Number.MAX_SAFE) │ │
│ │ TSID: 64비트 │ │
│ │ │ │
│ │ → 53비트 초과 시 정밀도 손실! │ │
│ │ → JSON 응답에서 Long → String 직렬화 필수 │ │
│ │ │ │
│ │ @JsonSerialize(using = ToStringSerializer.class) │ │
│ │ private Long id; │ │
│ │ │ │
│ │ JSON: { "id": "388400145978769408" } // 문자열 │ │
│ │ 아님: { "id": 388400145978769408 } // 정밀도↓ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
NanoID
┌─────────────────────────────────────────────────────────────────┐
│ NanoID │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 기본 설정: │
│ → 21자, A-Za-z0-9_- (64문자 알파벳) │
│ → ~126비트 엔트로피 │
│ → crypto.getRandomValues() 사용 (Math.random() 아님) │
│ │
│ 예시: V1StGXR8_Z5jdHi6B-myT │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ UUID v4 대비: │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ UUID v4: 550e8400-e29b-41d4-a716-446655440000 │ │
│ │ 36자, 하이픈 포함, URL 인코딩 불필요 │ │
│ │ │ │
│ │ NanoID: V1StGXR8_Z5jdHi6B-myT │ │
│ │ 21자, URL-safe, 42% 짧음 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 적합한 용도: │
│ O URL slug, 세션 ID, API 키, 초대 코드 │
│ X DB PK (비정렬 → B-tree 페이지 분할 유발) │
│ │
└─────────────────────────────────────────────────────────────────┘
성능 비교 벤치마크
┌─────────────────────────────────────────────────────────────────┐
│ ID 전략별 성능 비교 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┬──────────────┬───────────┬──────────────┐ │
│ │ ID 타입 │ B-tree 페이지│ 인덱스 │ INSERT │ │
│ │ │ 분할/100만건 │ 크기 │ 처리량 │ │
│ ├─────────────────┼──────────────┼───────────┼──────────────┤ │
│ │ BIGINT 순차 │ ~20 │ 최소 (8B) │ 최고 │ │
│ │ │ │ │ │ │
│ │ UUID v7 / ULID │ ~100-200 │ 2x (16B) │ 매우 좋음 │ │
│ │ │ │ │ │ │
│ │ Snowflake / TSID │ ~20-50 │ 최소 (8B) │ 최고 │ │
│ │ │ │ │ │ │
│ │ UUID v4 │ ~5,000-10,000│ 2x+ (16B)│ 2-10x 느림 │ │
│ └─────────────────┴──────────────┴───────────┴──────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 시각화: │
│ │
│ 페이지 분할 수 (적을수록 좋음) │
│ │
│ BIGINT 순차 ▓▓ (~20) │
│ Snowflake ▓▓▓ (~20-50) │
│ UUID v7 ▓▓▓▓▓▓▓▓▓ (~100-200) │
│ UUID v4 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ (~5,000-10,000) │
│ │
│ 결론: │
│ → 단일 DB: BIGINT 순차 (SEQUENCE + pooled-lo) │
│ → 분산 + 8바이트: Snowflake / TSID │
│ → 분산 + 표준: UUID v7 │
│ → UUID v4: PK 사용 금지 (세션 토큰 등에만) │
│ │
└─────────────────────────────────────────────────────────────────┘
15. 참고자료
[핵심 참고자료]
1. Vlad Mihalcea - "The best way to use the @GeneratedValue annotation"
https://vladmihalcea.com/hibernate-identity-sequence-and-table-sequence-generator/
2. Vlad Mihalcea - "Hibernate pooled and pooled-lo identifier generators"
https://vladmihalcea.com/hibernate-hidden-gem-the-pooled-lo-optimizer/
3. Thorben Janssen - "JPA GenerationType.SEQUENCE"
https://thorben-janssen.com/jpa-generate-primary-keys/
4. Hibernate ORM GitHub - PooledOptimizer.java
https://github.com/hibernate/hibernate-orm
5. JPA 3.2 Specification - Chapter 4: Entity Identity
https://jakarta.ee/specifications/persistence/3.2/
6. Hibernate JIRA HHH-10683 - Pooled Optimizer Negative ID Bug
https://hibernate.atlassian.net/browse/HHH-10683
7. Philippe Marschall - Batch Sequence Generator (Hypersistence Utils)
https://github.com/vladmihalcea/hypersistence-utils
8. RFC 9562 - Universally Unique IDentifiers (UUIDs) v7
https://www.rfc-editor.org/rfc/rfc9562
9. Hall, Owlett, Todd (1976) - "Relations and Entities"
https://www.researchgate.net/publication/221325612_Relations_and_Entities
10. E.F. Codd (1979) - "Extending the Database Relational Model to Capture More Meaning"
https://dl.acm.org/doi/10.1145/320107.320109
11. Steve Ebersole (2007) - "New 3.2.3 Hibernate Identifier Generators"
https://in.relation.to/2007/04/10/new-323-hibernate-identifier-generators/
12. Tom Anderson - "Linear Block Allocator — a superior alternative to Hi/Lo"
https://literatejava.com/hibernate/linear-block-allocator-a-superior-alternative-to-hilo/
13. Oracle FAQ - Sequence
https://www.orafaq.com/wiki/Sequence
14. PostgreSQL Identity Columns (official docs)
https://www.postgresql.org/docs/current/ddl-identity-columns.html
15. MySQL AUTO_INCREMENT Handling in InnoDB
https://dev.mysql.com/doc/en/innodb-auto-increment-handling.html
16. Twitter/X Engineering - "Announcing Snowflake" (2010)
https://blog.x.com/engineering/en_us/a/2010/announcing-snowflake
17. Instagram Engineering - "Sharding & IDs at Instagram"
https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c
18. ULID Specification
https://github.com/ulid/spec
19. NanoID
https://github.com/ai/nanoid
20. Vlad Mihalcea - "How to migrate the hilo optimizer to pooled strategy"
https://vladmihalcea.com/migrate-hilo-hibernate-pooled/
[추가 참고자료]
21. Twitter Snowflake ID - 분산 ID 생성 패턴
22. TSID Creator - Time-Sorted Unique Identifier Library
23. Spring Boot JPA Configuration Guide
24. Quarkus Hibernate ORM Guide (pooled-lo 기본 설정)