본문 바로가기
Java, Kotlin, Spring/JPA

JPA - 기본키 생성 전략

by Wordbe 2021. 9. 26.
728x90

JPA - 기본키 생성 전략

관계형 데이터베이스에서 테이블은 각각의 행을 구별하는 기본키가 필요하다.

기본키를 만들 때는 주민등록번호, 주문번호 같은 '자연키' 를 사용할 수도 있고,

논리적 의미가 하나도 없는 하나씩 증가하는 순번 같은 '대체키'를 사용할 수도 있다.

기본키로 어떤 것을 선택하면 좋을까?

주문번호 관련 생성 정책이 변경되거나 주민등록번호를 더 이상 저장하면 안된다는 정책이 생기는 등 자연키는 기본키로 정할 수 있는 올바른 전략이 아닐 수도 있다. 상황에 따라 다르겠지만 대체키를 권장한다.

대체키를 사용하고자 할 때는 Integer 타입 (약 10자리수, 21억 정도까지 표현) 대신 Long 타입 (약 19자리, 9백경 정도까지 표현)을 사용하여 표현 범위를 늘리는 것이 좋다.


@GeneratedValue(strategy =)

그렇다면 이 기본키는 어떻게 생성할까?

JPA 엔티티에서 @GeneratedValue 어노테이션을 기본키 필드 위에 붙이면, 기본키(대체키)를 자동으로 생성해주는 전략을 사용할 수 있다.

전략은 크게 4가지 인데 하나씩 살펴보자.


IDENTITY

@Entity
public class Badge { 
    @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;
}

기본키 생성을 hibernate 가 아니라, 데이터베이스가 하도록 위임한다.

데이터를 저장한 후 commit 시점에 최적의 상황을 고려하여 쿼리를 실행시키는 것이 아니라, persist() 시점에 insert 문이 바로 실행되고 DB 에서 기본키 값을 생성한다. 그리고 이 키 값을 가져와서 JPA 1차 캐시에 (key, entity) 키, 값을 저장한다.

따라서 IDENTITY 옵션 사용시 write-behind (쓰기지연) 기능을 무시한다. write-behind 는 한 트랜잭션 안에서 이루어지는 성능 최적화이므로 이 부분은 이루어지지 않아도 양보가 가능하다. 단, bulk insert 가 안되는 문제가 있을 수 있다. (다른 해결책이 있음)

  • MySQL, PostgreSQL, SQL Server, DB2 등에서 주로 사용한다.


테스트를 위해 docker로 간단하게 MySQL 환경을 마련해보자.

# docker hub 로부터 자동으로 mysql 이미지를 가져와서 도커 컨테이너를 실행한다.
$ docker run -d -p 3306:3306 --name wordbe-mysql -e MYSQL_ROOT_PASSWORD=1234 -e MYSQL_DATABASE=mytest -e MYSQL_USER=wordbe -e MYSQL_PASSWORD=1234 mysql

# mysql 서버와 인터렉트모드
$ docker exec -i -t wordbe-mysql bash

스프링부트에서는 mysql 을 연결할 수 있도록 드라이버를 먼저 설치한다.

dependencies {
    implementation 'mysql:mysql-connector-java'
}

그리고 스프링부트 설정에 mysql 정보를 추가한다.

application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mytest
    username: wordbe
    password: 1234

테스트코드를 입력한 뒤, 실행시켜보면

@Test
void GeneratedValue_IDENTITY_전략() {
  Badge badge = new Badge("칭찬왕");
  badgeRepository.save(badge);
}
create table badge (
  id bigint not null auto_increment,
  name varchar(255),
  primary key (id)
) engine=InnoDB

insert into badge (name) values ('칭찬왕');

테이블은 MySQL 의 Native Generator 를 통해 기본키가 생성되게 된다. insert 문이 실행되는데 id는 데이터베이스에서 자동으로 입력되어 들어간다.


SEQUENCE

Hibernate 가 만들어주는 시퀀스를 사용한다.

데이터베이스(Oracle 등)가 제공해주는 시퀀스를 사용할 수 있다. @SequenceGenerator 등록이 필요하다.

@Entity
@SequenceGenerator(
    name = "BADGE_SEQ_GENERATOR",
    sequnceName = "BADGE_SEQ",
    initialValue = 1,
    allocationSize = 50)
public class Badge { 
    @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "BADGE_SEQ_GENERATOR")
  private Long id;

  private String name;
}

데이터 저장시 (persist()) 데이터베이스에서 시퀀스를 조회해서 JPA 1차 캐시에 (key, entity) 키, 값을 저장한다.

데이터베이스에 시퀀스를 조회하러 DB I/O 요청 cost가 생긴다. 하지만, initialValue 와 allocationSize 를 조정함으로써 메모리에 미리 순번을 저장해 놓을 수 있다. 따라서 대량의 insert 쿼리가 주로 발생하는 엔티티라면 이 값을 설정해주면 좋을 것이다.

initialValue 는 기본값이 1, allocationSize 는 기본값이 50이다. 즉 1부터 한번에 50개의 순번을 가져온다. 한 번에 더 많이 가져오려면 allocationSize = 100 등으로 이 값을 늘리면 될 것이다.


TABLE

키 생성용 테이블을 별도로 사용하여 (마치 시퀀스처럼), 키를 생성한다.

모든 데이터베이스에서 적용가능하지만, 성능이 좋지 않으므로 실제 운영에서 잘 사용되지 않는다.

@Entity
@TableGenerator(
    name = "BADGE_KEY_GENERATOR",
    table = "BADGE_KEYS",
  pkColumnValue = "BADGE_KEY"
    initialValue = 1,
    allocationSize = 50)
public class Badge { 
    @Id
  @GeneratedValue(strategy = GenerationType.TABLE, generator = "BADGE_KEY_GENERATOR")
  private Long id;

  private String name;
}

AUTO

@Entity
public class Badge { 
    @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  private String name;
}

이 전략옵션은 말그대로 '자동'으로 적절한 전략을 알아서 매핑시킨다. @GeneratedValue 라고만 쓰면 기본으로 AUTO 옵션을 사용한다.

Hibernate 는 적절한 옵션을 알아내기 위해 아래 논리대로 탐색한다.

  1. 기본키 타입이 UUID 일 때 UUID Generator가 된다.

  2. Number 타입이면, hibernate.id.new_generator_mapping 옵션(기본값 true)을 확인한다.

    2-1. false 이면 Native Generator 를 사용한다. (MySQL 의 경우 auto_increment)

    2-2. true 이면, Sequence Generator 를 사용한다.

    ​ 2-2-1. 이 때 데이터베이스가 Sequence 를 지원하지 않는다면 TABLE Generator 를 사용한다.

만약, MySQL 을 사용하고, 아무 옵션도 주지 않았다면, AUTO 로 적용되어 SEQUENCE 로 기본키를 자동으로 생성하게 될 것이다.



  • 위 예제에서는 Number 타입(Long) 이고, hibernate.id.new_generator_mapping = true (기본값) 이다.
  • 데이터베이스는 H2 를 이용했으므로 Sequence 전략을 따른다.
  • 테스트 결과는 아래와 같다.
@DataJpaTest
@Rollback(value = false)
class BadgeTest {
    @Autowired
    BadgeRepository badgeRepository;

    @Test
    void GeneratedValue_AUTO_전략() {
        Badge badge = new Badge("칭찬왕");
        badgeRepository.save(badge);
    }
}
create table badge (
  id bigint generated by default as identity,
  name varchar(255),
  primary key (id)
)

...

call next value for hibernate_sequence
2021-09-25 17:22:10.889 DEBUG 62185 --- [           main] org.hibernate.SQL                        : 
    insert 
    into
        badge
        (name, id) 
    values
        (?, ?)
2021-09-25 17:22:10.891 TRACE 62185 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [칭찬왕]
2021-09-25 17:22:10.891 TRACE 62185 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]

자연키 생성

마지막으로 기본키를 사용자가 원하는 전략으로 만들어 주고 싶을 경우 아래와 같이 사용하면 된다.

@Entity
public class Badge { 
    @Id
  private Long id;

  private String name;

  @PrePersist
  public void generateId() {
    if (this.id != null) {
      throw new InvalidOperationException("id가 있는 경우엔 새로운 id를 생성할 수 없습니다.");
    }
    this.id = RandomStringUtils.randomAlphanumeric(12);
  }
}

영문대소문자와 숫자가섞인 랜덤한 12자리를 ID로 만들도록 임의로 설정하였다.

이미 id 가 있는 경우의 엔티티를 save 에서 새로운 아이디를 발급할 수 있는 상황이 혹시나 있을 수 있으므로 예외처리해준다.




728x90

댓글