하이버네이트는 어떻게 자동 키 생성 전략을 결정하는가

데이터베이스 테이블의 기본 키Primary key를 선정할 때 크게 2가지 선택지가 있는데 하나는 자연 키Natural key이고 또 하나는 대체 키Surrogate key이다. 자연 키는 전화번호, 이메일처럼 비즈니스적으로 의미 있는 키를 말하며, 대체 키는 비즈니스와 상관없이 임의로 만들어진 키를 말한다.

JPAJava Persistence API는 데이터베이스 테이블 대체 키를 기본 키로 자동 생성하는 기능을 지원한다. 사용법은 엔티티Entity 클래스에 Id 어노테이션과 함께 결합하여 GeneratedValue 어노테이션을 추가한다.

1
2
3
4
5
6
7
8
9
10
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
class Car {
   @Id
   @GeneratedValue // @GeneratedValue(strategy = GenerationType.AUTO)와 같다.
   private Long id;
   //....
}

GeneratedValue 어노테이션에는 자동 생성 전략을 4개로 지정해 줄 수 있는데 지정해 주지 않으면 기본값은 'AUTO'이다.

생성 전략

설명
AUTO(default) JPA 구현체가 자동으로 생성 전략을 결정한다.
IDENTITY 기본키 생성을 데이터베이스에 위임한다. 예를 들어 MySQL의 경우 AUTO_INCREMENT를 사용하여 기본키를 생성한다.
SEQUENCE 데이터베이스의 특별한 오브젝트 시퀀스를 사용하여 기본키를 생성한다.
TABLE 데이터베이스에 키 생성 전용 테이블을 하나 만들고 이를 사용하여 기본키를 생성한다.

출처 : 자바 ORM 표준 JPA 프로그래밍 4.6 기본키 매핑

GeneratedValue-AUTO를 사용하는 경우 자동 키 생성 전략은 어떻게 결정하는 것일까?

이 글은 JPA 구현체 중 하나인 하이버네이트Hibernate 5에서 하이버네이트가 자동 키 생성 전략을 어떤 과정으로 결정하는지에 대해 설명한다.

하이버네이트 Interpreting AUTO

하이버네이트 문서 중 2.6.7. Interpreting AUTO 절을 아래 그림으로 나타냈다.

그림. 하이버네이트 Interpreting AUTO

그림. 하이버네이트 Interpreting AUTO

하이버네이트는 가장 먼저 @GeneratedValue가 사용된 데이터 타입(Java)을 확인하고 UUID라면 UUID Generator가 된다.[1]

1
2
3
4
5
6
7
8
import java.util.UUID;
@Entity
public class Product {
  @Id
  @GeneratedValue
  private UUID id;
  // ...
}

데이터 타입이 숫자 타입(Integer, Long)이면 하이버네이트 프로퍼티Property인 'hibernate.id.new_generator_mappings'[2]에 설정된 값(기본값은 TRUE)을 확인한다. 값이 'FALSE' 인 경우 Native Generator가 된다. Native Generator는 다시 하이버네이트에 설정된 방언Dialect[3](아래 예시에서는 MySQL5Dialect)으로 결정한다.[4]

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 하이버네이트 persistence.xml 예시 -->
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
    <persistence-unit name="test" transaction-type="RESOURCE_LOCAL">
        <properties>
            <!-- ... -->
            <property name="hibernate.id.new_generator_mappings" value="false" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect" />
        </properties>
    </persistence-unit>
</persistence>

'hibernate.id.new_generator_mappings' 값이 'TRUE'인 경우 SequenceStyleGenerator를 사용하게 되는데 데이터베이스가 Sequence를 지원하는 경우 Sequence Generator를 지원하지 않는다면 Table Generator가 된다.

코드 예시

코드는 하이버네이트 5.2와 데이터베이스로는 MySQL로 작성하였다. Maven POMProject Object Model은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    //...  
    <dependencies>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.2.8.Final</version>
        </dependency>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.37</version>
        </dependency>
        //...
    </dependencies>
</project>

하이버네이트 설정 파일인 persistence.xml은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
    <persistence-unit name="test" transaction-type="RESOURCE_LOCAL">
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect" />
            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/hibernate" />
            <property name="javax.persistence.jdbc.user" value="root" />
            <property name="javax.persistence.jdbc.password" value="1111" />
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="hibernate.hbm2ddl.auto" value="create-drop" />
        </properties>
    </persistence-unit>
</persistence>

엔티티 클래스는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Product {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  private String name;
  //...
}

Product 엔티티의 id의 자동 생성 전략은 무엇일까?

먼저 데이터 타입이 숫자 타입이기 때문에 하이버네이트 설정 파일(persistence.xml) 프로퍼티 'hibernate.id.new_generator_mappings' 설정값을 확인하게 되는데 명시적으로 설정해 주지 않았기 때문에 'TRUE'이다. 따라서 SequenceStyleGenerator를 사용하게 되며, MySQL은 시퀀스 오브젝트를 지원하지 않기 때문에 결국 Table Generator가 답이다.

그림. 하이버네이트 Interpreting AUTO - MySQL

그림. 하이버네이트 Interpreting AUTO - MySQL

실제로 하이버네이트 DDLData Definition Language 로그를 통해서 Table Generator를 사용하는 것을 확인할 수 있다. 하이버네이트는 'hibernate_sequence'라는 키 생성 전용 테이블을 생성하고 Product 테이블 생성 시 id 컬럼에는 not null과 기본키 제약조건을 선언한다.[5]

갈무리. 하이버네이트 DDL 로그

갈무리. 하이버네이트 DDL 로그

GitHub

전체 코드는 필자의 GitHub 저장소에서 확인할 수 있다.

주석

[1] 기본적으로 UUID는 테이블 컬럼으로 binary를 매핑된다. 이를 문자열로 변경하고 싶을 때에는 아래처럼 @Type을 추가한다.

1
2
3
4
5
6
7
8
9
import org.hibernate.annotations.Type;
@Entity
public class Product {
  @Id
  @GeneratedValue
  @Type(type="uuid-char")
  private UUID id;
  //...
}

[2] hibernate.id.new_generator_mappings

하이버네이트는 더 효과적이고 JPA 규격에 맞는 새로운 키 생성 전략을 개발했는데 과거 버전과의 호환성을 유치하려고 기본값을 false로 두었다. 기존 하이버 네이트 시스템을 유지보수하는 것이 아니라면 반드시 true로 설정하자. - 자바 ORM 표준 JPA 프로그래밍 132 쪽

[3] RDBMS 마다 SQL을 상이하게 사용하는 부분이 있다. 하이버네이트는 각각의 RDBMS 마다 특수화된 Dialect를 사용하여 호환성을 확보한다.

Represents a dialect of SQL implemented by a particular RDBMS. Subclasses implement Hibernate compatibility with different systems. - https://docs.jboss.org/hibernate/orm/3.5/api/org/hibernate/dialect/Dialect.html

[4] 실제로 하이버네이트는 방언 클래스의 getNativeIdentifierGeneratorStrategy 메소드를 호출하여 방언으로 설정한 생성 전략을 사용한다.

[5] hibernate_sequence 테이블을 어떤 식으로 사용하는지는 아래 코드를 실행한 SQL 로그로 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ProductTest {
  EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
  @Test
  public void test() {
    Product product = new Product("My Bottle");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = null;
    try {
      tx = em.getTransaction();
      tx.begin();
      em.persist(product);
      tx.commit();
    } catch (Exception e) {
      e.printStackTrace();
      if (tx != null) {
        tx.rollback();
      }
      throw new RuntimeException(e);
    } finally {
      em.close();
    }
  }
}
갈무리. 하이버네이트 SQL 로그

갈무리. 하이버네이트 SQL 로그

참고 자료


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.