공부/JPA

JPA 기본키 생성 전략과 선택 기준

d02 2023. 7. 16. 22:43

JPA 엔티티 매핑 중 @Id로 기본 키 매핑을 할 때, 해당 값을 넣어주는 방식으로 여러 가지 선택지가 있다.

 

@Id만 사용하고 다른 설정을 해주지 않으면, 기본 키 값을 직접 할당해야 한다. 하지만 정말 불편할 것이다.

따라서 아래와 같이, @GeneratedValue 어노테이션을 통해 기본키 값을 자동 생성할 수 있다.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

 

이러한 자동 생성 전략으로도 여러 가지가 있는데, 이 중 무엇을 사용할지 정확히 알고 있지 않다는 것을 깨달아서 정리해보려고 한다.

 

아래와 같이 하나의 트랜잭션 단위를 가지는 테스트 코드를 실행하여

User 엔티티를 저장할 때, 각 전략에 따라 어떤 일이 벌어지는지 확인하며 비교해보도록 하자.

DB는 MySQL 모드의 H2를 사용하였다.

@Test
void generateValue() {
    System.out.println("-----------------------------");
    System.out.println("-----------persist-----------");
    entityManager.persist(new User("dy1007", "1234", "doy", "doy@gmail.com"));
    System.out.println("-----------commit------------");
}

 

1. IDENTITY

기본 키 생성을 데이터베이스에 위임한다. (ex. MySQL의 AUTO_INCREMENT)

JPA는 일반적으로 성능 상 이득을 위해, 트랜잭션이 커밋되는 시점에 모아놓은 SQL을 한 번에 실행한다. (쓰기 지연)

하지만 IDENTITY 전략을 사용할 경우

DB에 먼저 값을 저장해 자동 생성된 기본 키 값을 받아와야 하기 때문에, 객체를 영속화하는 시점에 즉시 insert 쿼리를 실행한다.

즉 DB에 저장해야 기본 키를 생성할 수 있는 방식이다.

 

 많은 DB에서 지원, 주로 MySQL, SQL Server, PostgreSQL, DB2, Derby, Sybase에서 사용

 

테스트 코드를 실행하면, 콘솔 출력 내용이 아래와 같다.

실제로 하나의 트랜잭션이 커밋되기(테스트 코드니까 롤백됐지만) 이전에 insert 쿼리를 날린 것을 확인할 수 있다.

-----------------------------
----------persist------------
Hibernate: 
    insert 
    into
        user
        (created_at, updated_at, email, name, password, user_id) 
    values
        (?, ?, ?, ?, ?, ?)
-----------commit------------

 

2. SEQUENCE

데이터베이스 시퀀스라는, 유일한 값을 순서대로 생성하는 오브젝트를 사용해 기본 키를 생성한다. (ex. Oracle의 sequence)

아래와 같이 별도로 SequenceGenerator 매핑 설정을 해주어야 한다.

 

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@SequenceGenerator(name = "USER_SEQ_GENERATOR", allocationSize = 1) // allocationSize의 기본 값은 50
private Long id;

 

@SequenceGenerator의 속성 중

sequenceName 속성으로 엔티티에 따라 서로 다른 시퀀스를 지정할 수 있고(시퀀스 분리 가능)

allocationSize 속성으로 시퀀스 한 번 호출에 증가하는 수를 정해서, 성능 최적화를 할 수 있다.

한 번에 시퀀스 값을 많이 증가시켜 놓고, 그만큼을 메모리에 할당해두고 쓴다. 

예를 들어 1~50까지의 시퀀스 값을 미리 선점해두면, 51이 되어서야 시퀀스 값을 다시 100으로 증가시키면 되기 때문에 시퀀스 호출 횟수를 줄일 수 있다.

또 여러 곳에서 시퀀스 호출을 하더라도 미리 선점해둔 값을 사용하므로, 충돌을 피할 수 있다.

IDENTITY와 달리 쓰기 지연을 지원하는 것도 장점이다.

 

✅ Oracle, PostgreSQL, DB2, H2에서 지원 

 MySQL은 Sequence 전략을 지원하지 않는다!

 

테스트 코드를 실행하면, 콘솔 출력 내용이 아래와 같다.

이 경우에는 MySQL이 해당 전략을 지원하지 않기 때문에 기본 H2 모드로 바꾸어 실행해보았다.

persist 호출 후, 커밋하기 전에 시퀀스에서 값을 호출함을 확인할 수 있다.

-----------------------------
---------persist-------------
Hibernate: 
    call next value for hibernate_sequence
-----------commit------------

 

그런데, 시퀀스를 지원하지 않는 데이터베이스에서 시퀀스 전략을 선택하면 어떻게 될까?

다시 MySQL 모드의 H2로 설정하고 이 테스트 코드를 실행하면, 아래와 같았다.

시퀀스 전략을 사용하지만 MySQL이 시퀀스를 지원하지 않기 때문에

hibernate_sequence라는 테이블을 생성해 기본 키 값을 생성함을 확인할 수 있다.

마치 데이터베이스 시퀀스를 흉내내는 것이다. 이는 아래 설명할 Table 전략과 같은 방식이다.

Hibernate: 
    
    create table hibernate_sequence (
       next_val bigint
    ) engine=InnoDB
Hibernate: 
    
    insert into hibernate_sequence values ( 1 )
-----------------------------
----------persist------------
Hibernate: 
    select
        next_val as id_val 
    from
        hibernate_sequence for update
            
Hibernate: 
    update
        hibernate_sequence 
    set
        next_val= ? 
    where
        next_val=?
-----------commit------------

 

3. TABLE

키 생성 전용 테이블을 하나 만들어서, 데이터베이스 시퀀스를 흉내내는 전략이다.

모든 데이터베이스에서 적용 가능한 대신, 성능적 단점 때문에 잘 사용하지 않는다.

또 SEQUENCE 전략과 달리, DB와 한번 더 통신해야 한다.

 

키 생성 전용 테이블을 생성한 뒤, 

create table MY_SEQUENCES (
    sequence_name varchar(255) not null, -- 매핑할 시퀀스 이름
    next_val bigint, -- 시퀀스 값
    primary key ( sequence_name )
)

@TableGenerator 어노테이션으로 매핑해준다.

@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "USER_SEQ_GENERATOR")
@TableGenerator(name = "USER_SEQ_GENERATOR",
        table = "MY_SEQUENCES",
        pkColumnName = "USER_SEQ", allocationSize = 1)
private Long id;

 

내부 동작 방식은 SEQUENCE 전략과 동일하다. 마찬가지로 initialValue, allocationSize 를 설정할 수 있다.

 

테스트 코드를 실행하면, 콘솔 출력 내용이 아래와 같다.

앞서 MySQL에서 시퀀스 전략을 선택했을 때와 같은 방식임을 확인할 수 있다.

Hibernate: 
    
    create table my_sequences (
       USER_SEQ varchar(255) not null,
        next_val bigint,
        primary key (USER_SEQ)
    ) engine=InnoDB
Hibernate: 
    
    insert into my_sequences(USER_SEQ, next_val) values ('user',0)
Hibernate:
-----------------------------
---------persist-------------
Hibernate: 
    select
        tbl.next_val 
    from
        my_sequences tbl 
    where
        tbl.USER_SEQ=? for update
            
Hibernate: 
    update
        my_sequences 
    set
        next_val=?  
    where
        next_val=? 
        and USER_SEQ=?
-----------commit------------

 

4. AUTO

말 그대로, 선택한 DB 방언에 따라 위 세 가지 전략 중 한 가지를 자동으로 선택하는 전략이다.

DB를 변경해도 코드 수정을 하지 않아도 되기 때문에 좋아보일 수 있다.

 

하지만 Hibernate 버전에 따라 같은 DB 방언임에도 선택하는 전략이 달라질 수 있으므로 완전히 신뢰하기 어렵다!
(ex. Hibernate 5부터 MySQL의 자동 선택 전략은 Identity가 아닌 Table이라고 한다.)

 

 

그래서 뭘 쓰지?

위와 같은 특징들을 고려하여 기본키 전략을 선택하면 되겠다.

 

지금 나의 결론은

시퀀스를 지원하는 DB를 쓴다면 성능 최적화를 하기 좋은 SEQUENCE 전략을,

하지만 MySQL을 쓴다면 IDENTITY 전략을 선택하는 것이다.

 

MySQL을 쓰면서 SEQUENCE나 AUTO 전략을 선택한다면, 결국 Table 전략으로 동작하게 된다.

(만약 AUTO 전략이 바뀐다 해도, 전략의 제어권이 외부에 있는 것은 좋지 않은 것 같다)

하지만 Table 전략은 장점보다 단점이 더 커보인다.

 

IDENTITY는 쓰기 지연을 지원하지 않아 매번 insert 쿼리를 날려야 하지만,

Table 전략도 어차피 매번 시퀀스 테이블을 조회하고 업데이트해야 한다. 

때문에 IDENTITY 전략을 사용하는 게 이점이 더 크다고 생각한다.

 

 

참고 자료

https://www.baeldung.com/jpa-strategies-when-set-primary-key

인프런 김영한님 강의 <자바 ORM 표준 JPA 프로그래밍 - 기본편>

반응형