기본 콘텐츠로 건너뛰기

JPA 기본 키 생성 전략: IDENTITY, SEQUENCE, TABLE, AUTO를 언제 선택할까

JPA 기본 키 생성 전략: IDENTITY, SEQUENCE, TABLE, AUTO를 언제 선택할까

빠른 답

  • IDENTITY는 DB가 INSERT 시점에 ID를 만들기 때문에 persist() 직후 INSERT가 실행될 수 있고, JDBC batch insert에는 불리합니다.
  • SEQUENCEINSERT 전에 시퀀스에서 ID를 먼저 확보하므로 쓰기 지연과 batch insert를 활용하기 좋습니다. allocationSize 설정이 성능에 큰 영향을 줍니다.
  • TABLE은 여러 DB에서 동작하지만 키 전용 테이블을 SELECT하고 UPDATE해야 해서 락 경합과 왕복 비용이 생기기 쉽습니다.
  • AUTO는 편하지만 DB 방언, JPA provider, Hibernate 버전에 따라 실제 전략이 달라질 수 있어 운영 DB가 정해져 있다면 명시 전략이 더 예측하기 쉽습니다.

한눈에 비교

생성 위치
IDENTITY 는 엔티티 테이블의 identity 컬럼, SEQUENCE 는 DB 시퀀스 객체, TABLE 은 별도 키 테이블, AUTO 는 provider의 판단을 사용합니다.
ID를 아는 시점
IDENTITY 는 행을 실제로 넣은 뒤 ID를 알 수 있고, SEQUENCE 와 TABLE 은 INSERT 전에 ID를 확보할 수 있습니다.
쓰기 지연과 배치 저장
SEQUENCE 는 여러 INSERT 를 flush 시점까지 모으기 쉽지만, IDENTITY 는 ID 생성을 위해 행 삽입이 먼저 필요해 JDBC batch insert 효과가 제한됩니다.
DB 의존성
MySQL 계열은 보통 IDENTITY , PostgreSQL과 Oracle은 SEQUENCE 를 고려하기 쉽습니다. TABLE 은 이식성은 좋지만 성능 비용이 큽니다.
운영 해석 난이도
AUTO 는 코드가 짧지만 실제 DDL과 SQL 로그를 확인해야 합니다. 명시 전략은 장애 분석과 성능 튜닝 때 흐름을 추적하기 쉽습니다.

시간 흐름으로 이해하기

엔티티 생성
애플리케이션이 새 엔티티를 만들고 entityManager.persist(entity) 를 호출합니다.
ID 확보
IDENTITY 는 INSERT 후 ID를 받고, SEQUENCE 는 시퀀스에서 먼저 ID를 가져오며, TABLE 은 키 테이블을 잠그고 값을 갱신합니다.
영속성 컨텍스트 등록
JPA provider는 식별자를 가진 엔티티를 영속성 컨텍스트에서 관리합니다.
flush 또는 commit
아직 DB에 반영되지 않은 변경이 실제 INSERT 로 전송됩니다.
인덱스와 키 생성 상태 갱신
엔티티 테이블의 기본 키 인덱스, 시퀀스 값, 키 테이블 값이 다음 저장을 위한 상태로 바뀝니다.

선택 기준 매트릭스

PostgreSQL 또는 Oracle에서 쓰기량이 많음: SEQUENCE 를 권장합니다. DB 시퀀스를 사용할 수 있고, allocationSize 로 ID 조회 왕복을 줄일 수 있습니다.
MySQL처럼 identity 컬럼 중심의 DB를 사용함: IDENTITY 를 선택할 수 있습니다. 다만 대량 저장이 많다면 batch insert 제약을 먼저 확인해야 합니다.
한 트랜잭션에서 수십, 수백 건을 저장함
SEQUENCE 가 유리합니다. ID를 먼저 확보한 뒤 INSERT 를 모아 보낼 수 있기 때문입니다.
여러 DB를 같은 코드로 지원해야 함
AUTO 가 편합니다. 대신 운영 환경에서는 실제 선택된 전략을 SQL 로그와 DDL로 확인해야 합니다.
시퀀스와 identity를 쓰기 어려운 DB를 지원해야 함: TABLE 을 고려할 수 있습니다. 이 경우 키 테이블의 락, 인덱스, 트랜잭션 경합이 비용으로 따라옵니다.

왜 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 ScanLockRows입니다. 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를 활용하기 좋은 구조입니다.

TABLEINSERT 전에 ID를 확보한다는 점에서는 SEQUENCE와 비슷합니다. 하지만 시퀀스 객체 대신 테이블 행을 읽고 갱신해야 하므로 SELECT ... FOR UPDATEUPDATE가 따라옵니다. 이 차이는 대량 저장이나 동시 저장에서 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를 사용할 수 있습니다. 반면 IDENTITYINSERT 자체가 ID 생성 행위라서 hibernate.jdbc.batch_size를 켜도 해당 엔티티의 insert batching 효과가 제한됩니다.

TABLE은 로그만 봐도 DB 왕복이 늘어납니다. 특히 for updateupdate 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

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...