둘셋 개발!

[JPA] Primary Key 자동 생성 전략 본문

JPA

[JPA] Primary Key 자동 생성 전략

23 2023. 9. 15. 13:44

엔티티 (JPA가 관리하는 객체)의 기본키를 자동으로 생성하는 전략에는 3가지가 있다.

 

1. IDENTITY
2. SEQUENCE
3. TABLE

 

아래의 내용은 각각의 전략에 대한 설명과 성능향상을 위한 방법과 실제 사용 예시이다.

 

1. IDENTITY

@Entity
public class Member {

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

 

이 전략은 기본키 생성을 DB에게 위임하는 것이다.

엔티티를 영속성 컨텍스트에 persist 하려고 할 때 DB에 저장하고 DB로부터 기본키를 받아 영속성 컨텍스트에 저장한다.

원래는 새로운 엔티티를 영속화하면 insert 쿼리를 쓰기 지연 SQL에 저장하고, 트랜잭션이 커밋되는 시점에 DB에 쿼리를 보내지만,

IDENTITY 전략을 사용하면 쓰기 지연을 사용할 수 없다.

 

이때 나는 기존에 있던 쓰기지연 SQL에 있는 쿼리들도 같이 DB로 날아가는지 궁금해서 테스트를 해봤다.

그 결과 같이 날라가진 않았다.

테스트한 내용을 직접 보고 싶다면 밑에 더 보기를 눌러주세요!

더보기

Member엔티티의 기본키를 IDENTITY 전략으로 했음

tx.begin();

Member memberA = new Member();
em.persist(memberA);
memberA.setUsername("uni"); // 더티체킹으로 update 쿼리가 쓰기지연 SQL에 저장

Member memberB = new Member();
em.persist(memberB);

System.out.println("--------------트랜잭션 커밋 전");
tx.commit();

 

결과

 트랜잭션 커밋을 한 후에 update를 쿼리가 날아간 것을 볼 수 있다.

이때 또 의문이 드는 것은 DB에 저장하는 쿼리를 한 후에 기본키를 알고 싶다면 기본키를 알기 위한 쿼리가 하나 더 나가야 할 것 같은데,

insert쿼리 하나만 나갔다.

이렇게 된 이유는 JDBC3에 추가된 Statement.getGeneratedKeys()를 사용하면 데이터를 저장함과 동시에 기본키도 알 수 있는데

하이버네이트는 이를 사용했다.

 

참고로 IDENTITY 전략은 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다.

 

 

2. SEQUENCE 전략

@Entity
@SequenceGenerator(
        name = "BOARD_SEQ_GENERATOR",
        sequenceName = "BOARD_SEQ",
        initialValue = 1, allocationSize = 1)
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                    generator = "BOARD_SEQ_GENERATOR")
    private Long id;
}

DB의 시퀀스를 사용해서 기본키를 할당하는 방법이다.

Board클래스 위에 @SequenceGenerator을 사용해서 시퀀스를 만들고, id필드 위에 해당 시퀀스를 사용하겠다고 @GeneratedValue의 속성 값으로 선언했다.

 

@SequenceGenerator

- name: 식별자 생성기 이름

- sequenceName: DB에 생성할 시퀀스 이름

- initialValue: 처음 시작하는 숫자

- allocationSize: 시퀀스 한번 호출 시 얼마나 증가할 것인지

 

여기서 눈여겨 봐야할 속성값은 allocationSize이다. 이 속성값으로 성능을 최적화할 수 있다.

일단 위의 예시에는 allocationSize을 1로 설정했는데, 그러면 이것의 의미는 시퀀스를 한 번호출할 때마다 1씩 증가하겠다는 이야기이다.

그렇다면 Board를 저장할 때마다 시퀀스에 접근해서 값을 가져와야 하므로, 하나의 Board를 저장할 때마다 총 2번의 DB에 접근해야 한다. (inser쿼리, 시퀀스 쿼리)

 

다음 예시는 Board를 50번 저장하는 예시이다.

tx.begin();
for (int i=0;i<50;i++) {
    Board board = new Board();
    em.persist(board);
}
tx.commit();

 

결과 로그

Hibernate: 
    call next value for BOARD_SEQ
Hibernate: 
    call next value for BOARD_SEQ
    
    .... (총 50번 찍힘)
    
Hibernate: 
    /* insert hellojpa.entitiy.Board
        */ insert 
        into
            Board
            (id) 
        values
            (?)
Hibernate: 
    /* insert hellojpa.entitiy.Board
        */ insert 
        into
            Board
            (id) 
        values
        	(?)
   .... (이것도 총 50번 찍힘)

 

총 100번의 쿼리가 나간다.

이를 allocationSize를 조정하면 이를 최적화할 수 있다.

예를 들어 현재의 시퀀스 값이 1이고 allocationSize를 50이라고 설정한다음 시퀀스를 한 번 실행한다면

            - 시퀀스 값 += 50

            -  1~50까지 메모리에서 시퀀스 값을 할당

이렇게 하면 시퀀스 값은 51이 되고 애플리케이션에서는 50까지 저장할 때까지는 시퀀스 값을 찾으러 DB에 접근하지 않아도 된다.

 

다음은 위의 예시에서 allocationSize를 50으로 조정한 결과이다. (참고로 default가 50이다)

Hibernate: 
    call next value for BOARD_SEQ
Hibernate: 
    call next value for BOARD_SEQ
    
    
    
Hibernate: 
    /* insert hellojpa.entitiy.Board
        */ insert 
        into
            Board
            (id) 
        values
            (?)
            
   // 총 insert 쿼리가 50개

시퀀스를 총 2번 호출했다. 50번에서 2번으로 줄여진 것이다.

그리고 1번이 아니라 2번 호출하는 이유는 1~50까지 메모리에서 할당해야하는데 처음 시퀀스를 호출할 때 얻은 값이 1이므로 한번더 호출 한 것이다. 51이라는 값을 받아 왔음으로 이제 맘 놓고 1~50까지 기본키값을 메모리에서 할당하게 되는 것이다.

h2에서 현재 값이 101이라는 것을 확인할 수 있다.

두 번 호출했으니까 1 + (50 +50)  = 101 이 맞다.

 

3. Table 전략

@Entity
@TableGenerator(name = "PET_SEQ_GENERATOR",
                table = "MY_SEQUENCES",
                pkColumnValue = "PET_SEQ", allocationSize = 1)
public class Pet {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE,
                    generator = "PET_SEQ_GENERATOR")
    private Long id;

    private String name;
}

테이블 전략은 키 생성 전용 테이블을 만드는 것이다.

위의 코드를 보면 MY_SEQUENCES라는 테이블을 사용한다는 것을 알 수 있다.

물론 MY_SEQUENES 테이블은 따로 만들어줘야 한다.

 

이 경우 Pet을 저장하면 다음과 같은 결과가 나온다.

tx.begin();
        
Pet pet = new Pet();
em.persist(pet);

tx.commit();

 

결과 로그

Hibernate: 
    select
        tbl.next_val 
    from
        MY_SEQUENCES tbl 
    where
        tbl.sequence_name=? for update
            
Hibernate: 
    update
        MY_SEQUENCES 
    set
        next_val=?  
    where
        next_val=? 
        and sequence_name=?
Hibernate: 
    /* insert hellojpa.entitiy.Pet
        */ insert 
        into
            Pet
            (name, id) 
        values
            (?, ?)

현재의 sequence 번호를 알기위해 select 하고 1(allocationSize를 1로 해놨음)을 증가시키기 위해 update를 하고 

데이터를 저장하기 위해 insert를 한다.

sequence전략과 비교했을 때 쿼리가 한번더 나간다는 단점이 있다.

 

만약 50개의 pet을 저장하고자 한다면 50 *3 의 쿼리가 나간다.

이또한 마찬가지로 allocationSize를 늘림으로써 성능 최적화를 해야한다...!

sequence와 같은 방법이기 때문에 구체적인 과정을 생략하겠다.

 

 

마지막으로 AUTO

기본키 전략의 default는 auto인데, 이것은 JPA가 IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택해서 넣어주는 것이다.

 

 

 

 

 

(자바 ORM 표준 JPA 프로그래밍 - 김영한 책을 바탕으로 포스팅 하였습니다)