JPA 기본 키 생성 전략: IDENTITY, SEQUENCE, TABLE, AUTO를 언제 선택할까
빠른 답
IDENTITY는 DB가INSERT시점에 ID를 만들기 때문에persist()직후INSERT가 실행될 수 있고, JDBC batch insert에는 불리합니다.SEQUENCE는INSERT전에 시퀀스에서 ID를 먼저 확보하므로 쓰기 지연과 batch insert를 활용하기 좋습니다.allocationSize설정이 성능에 큰 영향을 줍니다.TABLE은 여러 DB에서 동작하지만 키 전용 테이블을SELECT하고UPDATE해야 해서 락 경합과 왕복 비용이 생기기 쉽습니다.AUTO는 편하지만 DB 방언, JPA provider, Hibernate 버전에 따라 실제 전략이 달라질 수 있어 운영 DB가 정해져 있다면 명시 전략이 더 예측하기 쉽습니다.
목차
한눈에 비교
시간 흐름으로 이해하기
선택 기준 매트릭스
왜 ID 생성 전략이 헷갈리는가
JPA에서 기본 키를 다루는 방법은 크게 직접 할당과 자동 생성으로 나뉩니다. 직접 할당은 @Id만 두고 애플리케이션이 ID 값을 직접 넣는 방식입니다. 외부 시스템에서 이미 식별자를 받아오거나, 도메인 규칙상 별도 식별자를 써야 할 때 사용할 수 있습니다.
자동 생성은 @Id와 @GeneratedValue를 함께 사용합니다. strategy 값으로 숫자형 ID에서는 주로 IDENTITY, SEQUENCE, TABLE, AUTO를 선택합니다. 원문에 있는 stretagy는 현재 API 기준으로 strategy가 맞습니다.
헷갈리는 지점은 “ID를 누가 만드느냐”보다 “ID를 언제 알 수 있느냐”에 있습니다. JPA의 영속성 컨텍스트는 엔티티를 식별자로 관리합니다. 그래서 DB에 행을 넣어봐야 ID를 알 수 있는 전략과, INSERT 전에 ID를 미리 받을 수 있는 전략은 persist() 이후 SQL 흐름이 달라집니다.
예제 쿼리와 실행 계획
아래는 PostgreSQL 스타일로 세 전략의 DB 객체를 단순화한 예시입니다. 실제 DDL은 Hibernate dialect, DB 버전, schema generation 설정에 따라 달라질 수 있지만, 실행 비용을 이해하는 데는 충분합니다.
CREATE TABLE post_identity (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
title varchar(200) NOT NULL
);
CREATE SEQUENCE post_seq START WITH 1 INCREMENT BY 50;
CREATE TABLE post_sequence (
id bigint PRIMARY KEY,
title varchar(200) NOT NULL
);
CREATE TABLE id_generator (
segment_name varchar(64) PRIMARY KEY,
next_val bigint NOT NULL
);
INSERT INTO id_generator(segment_name, next_val)
VALUES ('post_table', 1);
EXPLAIN
SELECT next_val
FROM id_generator
WHERE segment_name = 'post_table'
FOR UPDATE;
IDENTITY는 엔티티 테이블에 바로 INSERT하면서 DB가 ID를 만듭니다. 엔티티 테이블의 기본 키에는 보통 자동으로 인덱스가 만들어지고, 이 인덱스는 이후 findById, 외래 키 조인, 중복 방지에 쓰입니다. 다만 ID 생성 자체가 INSERT와 묶여 있으므로 JPA provider가 여러 INSERT를 모아 보내기 어렵습니다.
SEQUENCE는 엔티티 테이블을 스캔하지 않고 시퀀스 객체에서 다음 값을 얻습니다. 실행 계획 관점에서는 엔티티 테이블의 인덱스를 읽는 작업이 아니라, DB 내부의 시퀀스 값을 증가시키는 작업에 가깝습니다. 그래서 쓰기 부하가 있는 환경에서도 allocationSize를 맞춰 두면 DB 왕복을 줄이기 쉽습니다.
TABLE은 별도 키 테이블을 조회하고 갱신합니다. 위 EXPLAIN의 결과는 대략 다음과 같은 형태가 됩니다.
LockRows
-> Index Scan using id_generator_pkey on id_generator
Index Cond: ((segment_name)::text = 'post_table'::text)
이 결과에서 볼 부분은 Index Scan과 LockRows입니다. segment_name이 기본 키라면 필요한 한 행만 찾을 수 있지만, 동시에 같은 엔티티 ID를 발급받으려는 트랜잭션이 많으면 그 한 행에 락이 몰립니다. TABLE 전략이 이식성은 좋지만 쓰기량이 많은 서비스의 기본 선택지로는 부담스러운 이유가 여기에 있습니다.
persist와 INSERT 시점
IDENTITY에서는 새 엔티티의 ID가 DB INSERT 이후에야 결정됩니다. Hibernate는 generated key를 얻기 위해 JDBC의 Statement#getGeneratedKeys를 사용할 수 있고, 이 경우 SQL 로그에는 별도 SELECT 없이 INSERT가 먼저 보입니다.
반대로 SEQUENCE에서는 persist() 시점에 먼저 select nextval(...) 같은 쿼리로 ID를 확보할 수 있습니다. 그 뒤 엔티티는 ID를 가진 상태로 영속성 컨텍스트에 들어가며, 실제 INSERT는 flush 또는 commit 시점까지 미뤄질 수 있습니다. 쓰기 지연과 batch insert를 활용하기 좋은 구조입니다.
TABLE도 INSERT 전에 ID를 확보한다는 점에서는 SEQUENCE와 비슷합니다. 하지만 시퀀스 객체 대신 테이블 행을 읽고 갱신해야 하므로 SELECT ... FOR UPDATE와 UPDATE가 따라옵니다. 이 차이는 대량 저장이나 동시 저장에서 DB 락 대기 시간으로 드러날 수 있습니다.
설정 예시
운영 DB가 정해져 있다면 AUTO로 숨기기보다 사용하는 전략을 코드에 드러내는 편이 설정 리뷰와 장애 분석에 도움이 됩니다. 아래 예시는 SEQUENCE 전략을 명시한 엔티티입니다.
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_seq_gen")
@SequenceGenerator(
name = "post_seq_gen",
sequenceName = "post_seq",
allocationSize = 50
)
private Long id;
private String title;
protected Post() {
}
public Post(String title) {
this.title = title;
}
}
allocationSize는 JPA provider가 한 번에 확보해 둘 ID 범위의 크기입니다. Jakarta Persistence 3.2의 SequenceGenerator 기준 기본값은 50입니다. DB 시퀀스의 INCREMENT BY와 애플리케이션의 allocationSize가 어긋나면 Hibernate 설정에 따라 검증 오류가 나거나 예상보다 큰 ID gap이 보일 수 있습니다.
ID gap은 반드시 장애가 아닙니다. 트랜잭션 롤백, 애플리케이션 재시작, 시퀀스 preallocation 때문에 숫자가 건너뛰는 일은 흔합니다. 기본 키는 순번보다 식별자에 가깝게 보는 편이 안전합니다. 영수증 번호, 주문 표시 번호처럼 gap이 민감한 값은 기본 키와 분리해서 별도 규칙으로 관리하는 쪽이 낫습니다.
SQL 로그로 확인하기
전략을 바꿨다면 애플리케이션 코드만 보지 말고 SQL 로그를 확인해야 합니다. Hibernate 6.x 이상에서는 바인딩 로그 카테고리가 Hibernate 5.x 시절과 다릅니다. Spring Boot 환경이라면 다음처럼 SQL과 바인딩 값을 켤 수 있습니다.
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.orm.jdbc.bind=trace
같은 엔티티 3건을 저장할 때 전략별 로그는 다음처럼 달라집니다.
$ ./gradlew test --tests IdGenerationLogTest
## SEQUENCE 예시
Hibernate: select nextval('post_seq')
Hibernate: insert into post (title, id) values (?, ?)
Hibernate: insert into post (title, id) values (?, ?)
Hibernate: insert into post (title, id) values (?, ?)
## IDENTITY 예시
Hibernate: insert into post_identity (title) values (?)
Hibernate: insert into post_identity (title) values (?)
Hibernate: insert into post_identity (title) values (?)
## TABLE 예시
Hibernate: select next_val from id_generator where segment_name=? for update
Hibernate: update id_generator set next_val=? where next_val=? and segment_name=?
Hibernate: insert into post_table (title, id) values (?, ?)
이 로그에서 SEQUENCE는 먼저 ID를 확보한 뒤 INSERT가 이어집니다. allocationSize=50이면 매번 nextval을 호출하지 않고 확보한 범위 안에서 ID를 사용할 수 있습니다. 반면 IDENTITY는 INSERT 자체가 ID 생성 행위라서 hibernate.jdbc.batch_size를 켜도 해당 엔티티의 insert batching 효과가 제한됩니다.
TABLE은 로그만 봐도 DB 왕복이 늘어납니다. 특히 for update와 update id_generator가 반복된다면 키 테이블이 병목이 될 가능성을 의심할 수 있습니다. 이때는 애플리케이션 서버 CPU보다 DB 락 대기, 트랜잭션 시간, connection pool 대기를 같이 확인해야 합니다.
버전과 마이그레이션 포인트
현재 Jakarta Persistence 문서는 javax.persistence가 아니라 jakarta.persistence 패키지를 기준으로 합니다. Jakarta Persistence 3.0에서 패키지 이름이 javax.*에서 jakarta.*로 옮겨졌기 때문에, Spring Boot 2.x나 Java EE 시절 예제를 그대로 가져오면 import부터 맞지 않을 수 있습니다.
또 하나의 차이는 GenerationType.UUID입니다. 오래된 JPA 2.x 설명에서는 AUTO, IDENTITY, SEQUENCE, TABLE 네 가지만 나오는 경우가 많지만, Jakarta Persistence 3.1 이후 API에는 UUID가 포함되어 있습니다. 이 글은 숫자형 기본 키 전략을 중심으로 다루지만, UUID 기본 키를 검토한다면 DB 인덱스 크기, 정렬성, 저장 타입까지 함께 봐야 합니다.
Hibernate도 버전에 따라 identity insert 지연과 로그 카테고리 같은 세부 동작이 달라졌습니다. Hibernate current user guide는 identity 컬럼을 사용하는 엔티티에서는 식별자를 알기 전에 행 삽입이 필요하고, 이 때문에 insert batching에 제약이 생긴다고 설명합니다. 운영 환경에서 버전을 올릴 때는 ID 전략 자체뿐 아니라 batch 설정, SQL 로그, DDL 생성 결과를 함께 확인하는 편이 안전합니다.
공식 기준은 다음 문서에서 확인할 수 있습니다.
- Jakarta Persistence 3.2
GenerationType: https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/generationtype - Jakarta Persistence 3.2
GeneratedValue: https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/generatedvalue - Hibernate ORM User Guide, Identifiers: https://docs.hibernate.org/orm/current/userguide/html_single/#identifiers
원문 참고
https://www.maeil-mail.kr/question/69
댓글
댓글 쓰기