영호

[JPA] 다양한 기본 키 자동 생성 전략 본문

JPA

[JPA] 다양한 기본 키 자동 생성 전략

0h0 2023. 7. 2. 19:01

들어가면서,

관계형 데이터베이스를 사용하면서 각각의 데이터를 식별하는 기본키는 중요한 개념이다. 데이터베이스마다 기본 키를 생성해주는 방식은 다양하다. 그래서 JPA에서도 다양한 방식에 대응하기 위해 4가지 정도의 기본 키 생성전략이 존재하는데 각각의 특징에 대해서 정리하려고 한다.

 

AUTO


해당 전략의 경우 id로 사용하려는 필드의 타입, 사용하는 데이터베이스에 따라 전략을 '알아서' 선택해주는 방식이다.

 

우선, id로 사용하려는 필드 타입의 경우 2가지가 있다. UUID, Numeric(Integer, Long)이 있다. UUID를 사용하는 경우 JPA는 UUIDGenerator를 이용해 기본 키를 생성해준다.

그리고, Numeric의 경우 SequenceStyleGenerator에 의해 기본 키가 생성된다. 

SequenceStyleGenerator는 구현되어 있는 클래스다. 만약 사용하고 있는 데이터 베이스가 시퀀스를 지원하면 시퀀스 방식으로 기본 키를 생성해주고, 시퀀스를 지원하지 않으면 테이블 방식으로 기본 키를 생성해준다. (시퀀스, 테이블 방식은 아래에서 알아볼 예정이다.)

UUID, 시퀀스 지원 유무에 따라 어떤 쿼리가 나가는지 알아보자.

AUTO는 기본 값이기 때문에 @GeneratedValue만 사용하면 AUTO전략을 이용한다.

@Entity
public class Person {

    @Id
    @GeneratedValue
    private UUID id;

    public UUID getId() {
        return id;
    }
}

UUID를 사용하는 경우

Hibernate: 
    
    create table person (
       id binary(255) not null,
        primary key (id)
    ) engine=InnoDB

id의 타입이 binary인 것을 확인할 수 있다.

Numeric + 시퀀스 지원하지 않는 DB(MySQL)

Hibernate: 
    
    create table hibernate_sequence (
       next_val bigint
    ) engine=InnoDB
Hibernate: 
    
    insert into hibernate_sequence values ( 1 )
Hibernate: 
    
    create table person (
       id bigint not null,
        primary key (id)
    ) engine=InnoDB

시퀀스를 지원하지 않기 때문에 테이블 방식이 선택되면서 hibernate_sequence테이블이 생성되는 것을 확인할 수 있다.

Numeric + 시퀀스 지원 DB(h2)

Hibernate: 
    
    create table person (
       id bigint not null,
        primary key (id)
    )

별도의 시퀀스 테이블이 생성되지 않는 것을 확인할 수 있다.

IDENTITY


기본 키 생성을 데이터베이스에 맡기는 전략이다. 예를 들어, MySQL의 auto_increment가 있다. 주로, MySQL, PostgreSQL등에서 사용한다.

 

이 전략은 기본 키 생성을 데이터베이스에 맡기기 때문에, persist()시점에 실제 insert쿼리가 발생하게 된다. 그래서 쓰기지연이 동작하지 않는다는 점이 있다. 

사용방법

@Entity
public class Person {

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

    public Long getId() {
        return id;
    }
}
Hibernate: 
    
    create table person (
       id bigint not null auto_increment,
        primary key (id)
    ) engine=InnoDB
  • @GeneratedValue의 strategy옵션을 IDENTITY로 명시하면 된다. 위 코드를 실행시키면 아래와 같은 쿼리가 발생한다.
  • MySQL을 사용한 예제인데, id컬럼을 보면 auto_increment가 붙은 것을 볼 수 있다.

기본 키 생성시점

persist()시점에 기본 키가 생성되는지 코드로 확인해보고 싶어서 확인해봤다.

@Test
public void test() {

    Person person = new Person();
    System.out.println("persist -----");
    em.persist(person);
    System.out.println("----");

    System.out.println("flush-----");
    em.flush();
    System.out.println("------");
}
persist -----
Hibernate: 
    insert 
    into
        person
        
    values
        ( )
----
flush-----
  • 실제로 flush()가 아닌 persist시점에 insert쿼리가 나가는 것을 확인할 수 있다.

SEQUENCE


데이터베이스 시퀀스는 식별자 값들을 순서대로 생성하는 데이터베이스 오브젝트인데, SEQUENCE전략은 해당 시퀀스를 사용해 기본키를 생성하는 전략이다. 해당 전략은 시퀀스를 지원하는 DB인 h2, 오라클 등에서 사용한다.

 

AUTO와의 공통점

AUTO전략의 Numeric타입의 id를 사용할 때와 마찬가지로 SequenceStyleGenerator를 이용해 기본 키를 생성해준다.

AUTO와의 차이점

AUTO와의 차이점은 UUID를 지원해주지 않는다는 것이다.

SEQUENCE전략을 사용하면서 기본 키 타입을 UUID로 하면 엔티티 저장 시 IdentifierGenerationException예외가 발생한다. 

사용법

IDENTITY와 마찬가지로 @GeneratedValue의 옵션을 SEQUENCE로 설정하면 된다.

@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
}
Hibernate: 
    
    create table person (
       id bigint not null,
        primary key (id)
    )

주의점

SEQUENCE전략을 이대로 사용한다면, 하나의 시퀀스를 다른 테이블이 공유하게 된다. 만약 Person을 저장하고, Item이라는 다른 엔티티를 저장할 경우 각각의 id값이 1,1이 아닌 1,2가 된다.

 

테이블마다 시퀀스를 다르게 사용하고 싶다면 @SequenceGenerator를 이용하면 된다. 구체적인 사용법은 다루지 않겠다.

궁금한 부분

그렇다면, 시퀀스를 지원하지 않는 DB에서 SEQUENCE전략을 사용하면 예외가 발생하는지 궁금해서 직접 해본 결과 예외 대신 hibernate_sequence라는 테이블이 생성된다. 이후 select쿼리를 통해 기본 키 값을 조회한 뒤, insert쿼리가 나가는 것을 확인했다.

 

공식문서에서 SEQUENCE전략은 위에서 말했던 SequenceStyleGenerator를 사용한다. 만약, 시퀀스를 이용하지 않는 DB를 사용하는데 SEQUENCE전략을 사용하면 내부적으로 시퀀스 역할을 하는 테이블을 만들어서 사용하기 때문에 예외가 발생하지 않는다. 이로 인해, hibernate는 DB간 이식성을 높였다고 설명한다.

2.7.9. Using sequences
For implementing database sequence-based identifier value generation Hibernate makes use of its org.hibernate.id.enhanced.SequenceStyleGenerator id generator. It is important to note that SequenceStyleGenerator is capable of working against databases that do not support sequences by transparently switching to a table as the underlying backing, which gives Hibernate a huge degree of portability across databases while still maintaining consistent id generation behavior (versus say choosing between SEQUENCE and IDENTITY).

아래는 시퀀스를 지원하지 않는 MySQL에서 SEQUENCE를 사용했을 때의 쿼리다.

Hibernate: 
    
    create table hibernate_sequence (
       next_val bigint
    ) engine=InnoDB
Hibernate: 
    
    insert into hibernate_sequence values ( 1 )
Hibernate: 
    
    create table person (
       id bigint not null,
        primary key (id)
    ) engine=InnoDB
    
Hibernate: // insert전에 select로 기본 키 생성
select
    next_val as id_val 
from
    hibernate_sequence for update
            
Hibernate: 
    update
        hibernate_sequence 
    set
        next_val= ? 
    where
        next_val=?

Hibernate: 
    insert 
    into
        person
        (id) 
    values
        (?)
  • 쿼리를 보면 hibernate_sequence라는 시퀀스 역할을 테이블이 생성된다.
  • 이후, insert하기 전에 해당 테이블에 select쿼리를 통해 기본 키를 생성한 뒤 insert하는 것을 확인할 수 있다.

TABLE


해당 전략은 키 생성 테이블을 만들고 이를 통해 데이터베이스 시퀀스를 흉내내는 전략이다. 이 방법도 마찬가지로 @GeneratedValue의 strategy옵션을 TABLE로 설정하면 된다.

Hibernate: 
    
    create table hibernate_sequences (
       sequence_name varchar(255) not null,
        next_val bigint,
        primary key (sequence_name)
    )
    
Hibernate: 
    
    insert into hibernate_sequences(sequence_name, next_val) values ('default',0)
    
Hibernate: 
    
    create table person (
       id bigint not null,
        primary key (id)
    )

이 전략도 마찬가지로 @TableGenerator를 이용할 수 있다.

 

결론(내 생각)


처음에는 AUTO로 하면 장땡이라고 생각했다. 그러나, 공부를 하다 보니 무조건 AUTO를 사용한다면 시퀀스를 지원하지 않는 DB인 MySQL의 경우 auto-increment를 이용할 수 없다. 그래서 사용하려는 DB의 시퀀스 지원 유무에 따라 SEQUENCE, IDENTITY를 선택해서 사용하는 것이 좋아보인다.

왜냐하면, MySQL을 이용하는데 AUTO를 사용할 경우 하나의 시퀀스 테이블을 공유하면서 2개의 다른 엔티티를 저장했을 때 각각 1, 1을 id로 갖는 것이 아닌 1, 2를 가지기 때문이다.

 

만약, 기본 키 타입을 UUID로 하고 싶으면 AUTO를 사용하면 될 것 같다. AUTO를 제외한 전략에서 UUID를 사용할 수 있는지는 더 찾아봐야겠다.

 

그리고, IDENTITY의 경우 기본 키를 생성하기 위해 데이터베이스에 저장해야 되는 점으로 인해 쓰기지연이 동작하지 않는다. 이걸 해결하기 위한 방법은 조금 더 공부를 해봐야겠다.

Comments