Backend/JPA

기본 키 매핑

olsohee 2024. 1. 9. 13:15

JPA의 기본 키 매핑

JPA는 엔티티들을 영속성 컨텍스트에서 관리하는데, 영속성 컨텍스트에서 관리되려면 엔티티를 구분할 수 있는 식별자가 반드시 필요하다. 식별자가 되는 필드는 엔티티 클래스에서 @Id 어노테이션을 통해 지정할 수 있다. 

 

JPA가 제공하는 기본 키 생성 전략은 다음과 같다.

  • 직접 할당: 기본 키를 애플리케이션에서 직접 할당한다.
  • 자동 생성
    • IDENTITY: 기본 키 생성을 데이터베이스에 위임한다.
    • SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
    • TABLE: 키 생성 테이블을 사용한다.

자동 생성 전략이 이렇게 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문이다. 예를 들어 오라클은 시퀀스를 제공하지만 MySQL은 시퀀스를 제공하지 않고 그 대신에 기본 키 값을 자동으로 채워주는 AUTO_INCREMENT 기능을 제공한다. 따라서 SEQUENCE나 IDENTITY 전략은 사용하는 데이터베이스에 의존한다. 반면 TABLE 전략은 키 생성용 테이블을 만들어두고 시퀀스처럼 사용하는 방법이기 때문에 모든 데이터베이스에서 사용할 수 있다.

 

기본 키를 직접 할당하려면 @Id만 사용하면 되고, 자동 생성 전략을 사용하려면 @Id에 @GeneratedValue를 추가하고 원하는 키 생성 전략을 선택하면 된다.

IDENTITY 전략

IDENTITY 전략은 기본 키 생성을 데이터베이스에 위임하는 전략이다. MySQL의 경우 AUTO_INCREMENT가 적용된다.

따라서 데이터베이스에 엔티티를 insert 한 후에 기본 키 값을 조회할 수 있다. 주의할 점은 이때 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다는 것이다. 엔티티가 영속 상태가 되려면 반드시 식별자가 필요하다. 그런데 IDENTITY 전략은 엔티티를 데이터베이스에 저장한 후에 식별자를 알 수 있다. 따라서 em.persist()를 호출하는 즉시 insert sql이 데이터베이스에 반영된다. 

Board board = new Board();
em.persist(board); // 이 시점에 바로 insert sql이 데이터베이스에 반영된다.
System.out.println(board.getId());

SEQUENCE 전략

데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트이다. SEQUENCE 전략은 데이터베이스 시퀀스를 사용해서 기본 키를 생성한다. 따라서 이 전략은 시퀀스를 지원하는 오라클, PostgreSQL, DB2, H2 등의 데이터베이스에서 사용 가능하다.

 

시퀀스를 사용하려면 우선 다음과 같이 시퀀스를 생성해야 한다.

create table board (
    id bigint not null primary key,
    data varchar(255)
)

create sequence board_seq start with 1 increament by 1

그리고 다음과 같이 사용할 데이터베이스 시퀀스를 매핑해야 한다. @SequenceGenerator를 통해 시퀀스 생성기를 등록하고, sequenceName 속성을 통해 데이터베이스 시퀀스를 지정하면 JPA는 시퀀스 생성기를 지정한 데이터베이스 시퀀스와 매핑한다.

 

그리고 키 생성 전략을 GenerationType.SEQUENCE로 설정하고 등록한 시퀀스 생성기를 지정하면, 해당 시퀀스 생성기를 통해 기본 키가 할당된다.

@Entity
@SequenceGenerator (
    name = "BOARD_SEQ_GENERATOR", //시퀀스 생성기 이름
    sequenceName = "BOARD_SEQ", //매핑할 데이터베이스 시퀀스
    initialValue = 1, 
    allocationSize = 1)
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, //SEQUENCE 전략 선택
    				generator = "BOARD_SEQ_GENERATOR") //시퀀스 생성기 지정
    private Long id;
}

SEQUENCE 전략과 IDENTITY 전략의 동작 방식의 차이

SEQUENCE 전략은 IDENTITY 전략과 내부 동작 방식이 다르다.

  • SEQUENCE 전략: em.persist()를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다. 그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다(식별자 할당 ➡️ 영속성 컨텍스트에 저장). 그리고 이후 트랜잭션을 커밋하면 플러시가 일어나면서 엔티티를 데이터베이스에 저장한다.
  • IDENTITY 전략: em.persist() 호출 즉시 엔티티를 데이터베이스에 저장하고, 식별자를 조회해서 엔티티에 식별자를 할당한다(데이터베이스에 저장 ➡️ 식별자 할당). 

@SequenceGenerator

@SequenceGenerator 속성은 다음과 같다.

속성 기능 기본 값
name 식별자 생성기 이름 필수
sequenceName 데이터베이스에 등록된 시퀀스 이름 hibernate_sequence
initialValue 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정 1
allocationSize 시퀀스를 한 번 호출할 때 증가하는 수 50
catalog, schema 데이터베이스 catalog, schema 이름  

매핑되는 DDL은 다음과 같다.

create sequence [sequenceName] start with [initialValue] increment by [allocationSize]

SEQUENCE 전략과 최적화

SEQUENCE 전략은 데이터베이스와 2번 통신한다.

  1. 식별자를 구하기 위해 데이터베이스 시퀀스를 조회 (ex, SELECT BOARD_SEQ.NEXTVAL FROM DUAL)
  2. 조회한 시퀀스를 기본 키 값으로 사용해 엔티티를 데이터베이스에 저장 (ex, INSERT INTO BOARD ...)

따라서 JPA는 시퀀스에 접근하는 횟수를 줄여 데이터베이스와의 통신 횟수를 줄이기 위해 allocationSize를 사용한다. 하이버네이트의 경우 기본 값은 50이다. 그러면 최초에 데이터베이스에 시퀀스를 호출한 이후 1 ~ 50까지는 메모리에서 시퀀스 값을 조회해서 사용하고, 이후 51이 되는 시점에 데이터베이스 시퀀스를 한 번 더 호출하고 51부터 100까지 가상으로 시퀀스 식별자를 관리한다. 이 최적화 방법은 시퀀스 값을 선점하므로 여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는다. 

동작 방식

  1. 최초 persist() 실행 시 데이터베이스 시퀀스를 두 번 호출하여, 첫 번째 시퀀스 값을 가상으로 관리할 시작 값, 두 번째 시퀀스 값을 가상으로 관리할 범위의 끝 값(MAX)으로 지정한다.
  2. 이후에는 persist()를 실행해도 데이터베이스에 시퀀스를 호출하지 않고, 메모리에서 관리하는 가상의 값을 1씩 증가시키며 엔티티에 할당한다.
  3. 어느 시점, 엔티티에 할당할 식별자 값이 끝 값(MAX)이 된다.
  4. 이후에는 persist()를 실행하면 다시 데이터베이스 시퀀스를 호출한다.
  5. 다시 호출한 시퀀스 값을 가상으로 관리할 범위의 끝 값(MAX)으로 지정하고, 시작 값은 끝 값 - (allocationSize - 1)으로 지정한다.

ex, 데이터베이스 시퀀스 증가 값이 50, allocationSize 값이 50인 경우

  1. 최초 persist() 실행 시 데이터베이스 시퀀스를 두 번 호출하여, 첫 번째 시퀀스 값인 1을 가상으로 관리할 시작 값, 두 번째 시퀀스 값인 51을 가상으로 관리할 범위의 끝 값(MAX)으로 지정한다.
  2. 이후에는 persist()를 실행해도 데이터베이스에 시퀀스를 호출하지 않고, 메모리에서 관리하는 가상의 값을 1씩 증가시키며 엔티티에 할당한다.
  3. 어느 시점, 엔티티에 할당할 식별자 값이 끝 값인 51이 된다.
  4. 이후에는 persist()를 실행하면 다시 데이터베이스 시퀀스를 호출한다.
  5. 다시 호출한 시퀀스 값인 101을 가상으로 관리할 범위의 끝 값(MAX)으로 지정하고, 시작 값은 끝 값 - (allocationSize - 1)인 52(101 - (50 - 1))로 지정한다.
  6. 이후에는 엔티티의 식별자 값이 끝 값인 101이 될 때까지 데이터베이스 시퀀스를 조회하지 않고 가상의 시퀀스 값을 엔티티에 할당한다.

ex, 데이터베이스 시퀀스 증가 값이 1, allocationSize 값이 50인 경우

이 경우에는 식별자 관련 예외가 발생한다. 따라서 데이터베이스 시퀀스 증가 값이 1이면 allocationSize도 1이어야 한다.

  1. 최초 persist() 실행 시 데이터베이스 시퀀스를 두 번 호출하여, 첫 번째 시퀀스 값인 1을 가상으로 관리할 시작 값, 두 번째 시퀀스 값인 2를 가상으로 관리할 범위의 끝 값(MAX)으로 지정한다.
  2. 이후에는 persist()를 실행해도 데이터베이스에 시퀀스를 호출하지 않고, 메모리에서 관리하는 가상의 값을 1씩 증가시키며 엔티티에 할당한다.
  3. 어느 시점, 엔티티에 할당할 식별자 값이 끝 값인 2가 된다.
  4. 이후에는 persist()를 실행하면 다시 데이터베이스 시퀀스를 호출한다.
  5. 다시 호출한 시퀀스 값인 3을 가상으로 관리할 범위의 끝 값(MAX)으로 지정하고, 시작 값은 끝 값 - (allocationSize - 1)인 -46(3 - (50 - 1))으로 지정한다.
  6. 이후에는 엔티티의 식별자 값이 끝 값인 3이 될 때까지 데이터베이스 시퀀스를 조회하지 않고 가상의 시퀀스 값을 엔티티에 할당한다. 즉 엔티티 식별자에 -46, -45, -44가 순차적으로 할당되고 1이 되는 시점에 1을 식별자로 갖는 엔티티를 영속성 컨텍스트에 persist()하는 순간 식별자 중복 에러가 발생한다. 

ex, 데이터베이스 시퀀스 증가 값이 1, allocationSize 값이 1인 경우

이 경우에는 persist()를 실행할 때마다 데이터베이스 시퀀스를 조회하여 식별자로 할당하게 된다.

TABLE 전략

TABLE 전략은 키 생성 전용 테이블을 만들고 여기에 이름과 값으로 사용할 칼럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다. 이 전략은 테이블을 사용하므로 시퀀스를 지원하지 않는 데이터베이스도 사용 가능하므로, 모든 데이터베이스에 적용할 수 있다는 장점이 있다. 그러나 시퀀스 오브젝트와 달리 최적화가 되어있지 않은 테이블이기 때문에 성능 이슈가 있을 수 있다. TABLE 전략은 시퀀스 대신에 테이블을 사용한다는 것만 제외하면 SEQUENCE 전략과 내부 동작방식이 같다.

 

TABLE 전략을 사용하려면 우선 다음과 같이 키 생성 용도로 사용할 테이블을 만들어야 한다. 아래 코드의 경우 sequence_name 칼럼을 시퀀스 이름으로 사용하고, next_val 칼럼을 시퀀스 값으로 사용한다.

create table MY_SEQUENCES (
    sequence_name varchar(255) not null,
    next_val bigint,
    primary key (sequence_name)
)

그리고 @TableGenerator를 사용해서 테이블 키 생성기를 등록한다.

@Entity
@TableGenerator (
    name = "BOARD_SEQ_GENERATOR" //테이블 키 생성기 이름
    table = "MY_SEQUENCES" //매핑할 키 생성용 테이블
    pkColumnValue = "BOARD_SEQ", 
    allocationSize = 1)
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, //TABLE 전략 선택
    				generator = "BOARD_SEQ_GENERATOR") //테이블 키 생성기 지정
    private Long id;
}

그리고 MY_SEQUENCES 테이블 결과를 보면 다음과 같다.

sequence_name next_val
BOARD_SEQ 2
MEMBER_SEQ 10
PRODUCT_SEQ 50
... ...

MY_SEQUENCES 테이블에 @TableGenerator.pkColumnValue에서 지정한 BOARD_SEQ가 컬럼명으로 추가되었다. 이제 키 생성기를 사용할 때마다 next_val 컬럼 값이 증가한다. 참고로 MY_SEQUENCES 테이블에 값이 없으면 JPA가 값을 insert하면서 초기화하므로 값을 미리 넣어둘 필요는 없다.

@TableGenerator

@TableGenerator 속성은 다음과 같다.

속성 기능 기본 값
name 식별자 생성기 이름 필수
table 키 생성 테이블명 hibernate_sequences
pkColumnName 시퀀스 컬럼명 sequence_name
valueColumnName 시퀀스 값 칼럼명 next_val
pkColumnValue 키로 사용할 값 이름 엔티티 이름
initialValue 초기 값, 마지막으로 생성된 값이 기준 0
allocationSize 시퀀스를 한 번 호출할 때 증가하는 수 50
catalog, schema 데이터베이스 catalog, schema 이름  
uniqueConstraints 유니크 제약 조건을 지정할 수 있음  

TABLE 전략과 최적화

TABLE 전략은 값을 조회하면서 select 쿼리를 사용하고, 다음 값으로 증가시키기 위해 update 쿼리를 사용한다. 이 전략은 SEQUENCE 전략과 비교해서 데이터베이스와 한 번 더 통신한다는 단점이 있다. TABLE 전략을 최적화하려면 @TableGenerator.allocatinoSize를 사용하면 된다.

AUTO 전략

AUTO 전략은 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택한다. 예를 들어 오라클을 선택하면 SEQUENCE를, MySQL을 선택하면 IDENTITY를 사용한다.

 

@GeneratedValue.strategy의 기본 값은 AUTO이다. 따라서 @GeneratedValue와 @GeneratedValue(strategy = GenerationType.AUTO)는 같다.

 

AUTO 전략의 장점은 데이터베이스를 변경해도 코드를 수정할 필요가 없다는 것이다. 특히 키 생성 전략이 아직 확정되지 않은 개발 초기 단계나 프로토타입 개발 시 편리하게 사용할 수 있다.

 

AUTO를 사용할 때 SEQUENCE나 TABLE 전략이 선택되면 시퀀스나 키 생성용 테이블을 미리 만들어 두어야 한다. 만약 스키마 자동 생성 기능을 사용한다면, 하이버네이트가 기본 값을 사용해서 적절한 시퀀스나 키 생성용 테이블을 만들어준다.

기본 키 생성 전략 중 어떤 것을 사용해야 할까?

지금까지 JPA가 제공하는 기본 키 생성 전략을 알아봤다. 그렇다면 이들 중 어떤 것을 사용해야 할까?

 

IDENTITY 전략

  • IDENTITY 전략은 엔티티가 데이터베이스에 저장된 후에 영속성 컨텍스트에 저장되기 때문에 persist() 실행 즉시 insert sql이 실행된다. 즉 쓰기 지연의 이점을 누릴 수 없다.

SEQUENCE 전략

  • SEQUENCE 전략은 데이터베이스의 시퀀스를 호출해서 해당 시퀀스 값을 엔티티의 기본 값으로 설정한다. 따라서 쓰기 지연의 이점을 누릴 수 있다.
  • 그러나 결국에는 시퀀스 값을 조회하기 위해 데이터베이스에 시퀀스 값 조회 sql이 실행된다. 따라서 이때의 데이터베이스 통신 횟수를 줄이기 위해 JPA는 allocationSize를 제공하여, allocationSize 값만큼 시퀀스 값을 가져와서 캐싱하는 방법을 제공한다. 이 방법은 각 스레드는 시퀀스 값을 메모리에 캐싱해두고, 캐싱된 값을 사용하기 때문에 멀티 스레드의 상황에서 안전하다.

개인적인 생각으로는, 시퀀스를 제공하지 않는 데이터베이스의 경우에는 IDENTITY 전략을 사용하고, 시퀀스를 제공하는 데이터베이스인 경우에는 SEQUENCE 전략을 사용하면 될 거 같다. 그리고 시퀀스를 제공하지 않는 데이터베이스이지만 쓰기 지연의 이점을 누리고자 할 때는 TABLE 전략을 사용하면 될 것 같다.


Reference

'Backend > JPA' 카테고리의 다른 글

JPQL의 소개와 기본 문법  (1) 2024.01.10
영속성 전이(cascade)와 고아 객체  (0) 2024.01.09
프록시와 지연 로딩  (1) 2024.01.09
양방향 연관관계 매핑  (0) 2024.01.09
엔티티 매니저와 영속성 컨텍스트  (0) 2024.01.05