JPA는 엔티티들을 영속성 컨텍스트에서 관리하는데, 엔티티를 구분할 수 있는 식별자가 필요하다. 식별자가 되는 필드는 엔티티 클래스에서 @Id 어노테이션을 통해 지정할 수 있다. 그리고 엔티티가 영속성 컨텍스트에 들어가 JPA에 관리되는 시점에는 반드시 식별자로 지정된 필드에 식별자 값이 할당되어 있어야 한다.
JPA가 제공하는 기본 키 생성 전략은 다음과 같다.
직접 할당: 기본 키를 애플리케이션에서 직접 할당한다.
자동 생성: 대리 키 사용 방식
IDENTITY: 기본 키 생성을 데이터베이스에 위임한다.
SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.
TABLE: 키 생성 테이블을 사용한다.
기본 키 자동 생성 전략이 이렇게 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문이다. 예를 들어 오라클은 시퀀스를 제공하지만, MYSQL은 시퀀스를 제공하지 않고 그 대신에 기본 키 값을 자동으로 채워주는 AUTO_INCREMENT 기능을 제공한다. 따라서 SEQUENCE나 IDENTITY 전략은 사용하는 데이터베이스에 의존한다. 반면 TABLE 전략은 키 생성용 테이블을 만들어두고 시퀀스처럼 사용하는 방법이기 때문에 모든 데이터베이스에서 사용할 수 있다.
IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 전략이다. 따라서 데이터베이스에 엔티티를 insert 한 후에 기본 키 값을 조회할 수 있다.
Board board = new Board();
em.persist(board); //엔티티 저장
System.out.println(board.getId()); //저장 시점에 데이터베이스가 생성한 기본 키 값을 JPA가 조회한다.
엔티티가 영속 상태가 되려면 식별자가 반드시 필요하다. 그런데 IDENTITY 식별자 생성 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 insert sql이 데이터베이스에 저장된다. 따라서 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.
데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트이다. SEQUENCE 전략은 데이터베이스 시퀀스를 사용해서 기본 키를 생성한다. 따라서 이 전략은 시퀀스를 지원하는 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용 가능하다.
시퀀스를 사용해야 하기 때문에 다음과 같이 우선 시퀀스를 생성해야 한다.
CREATE TABLE BOARD (
ID BIGINT NOT NULL PRIMARY KEY,
DATA VARCHAR(255)
)
CREATE SEQUENCE BOARD_SEQ START WITH 1 INCREMENT BY 1
그리고 다음과 같이 사용할 데이터베이스 시퀀스를 매핑해야 한다. @SequenceGenerator
를 사용해서 시퀀스 생성기를 등록하고 sequenceName
속성으로 데이터베이스의 시퀀스를 지정해주면, JPA는 시퀀스 생성기를 데이터베이스의 시퀀스와 매핑한다.
그리고 키 생성 전략을 GenerationType.SEQUENCE
로 설정하고 generator = "BOARD_SEQ_GENERATOR"
로 등록한 시퀀스 생성기를 설정하면, 이제부터 BOARD_SEQ_GENERATOR 시퀀스 생성기가 id 식별자 값을 할당한다.
@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;
...
}
시퀀스 사용 코드는 다음과 같다.
Board board = new Board();
em.persist(board);
System.out.println(board.getId());
시퀀스 사용 코드는 IDENTITY 전략과 같지만 내부 동작 방식은 다르다.
SEQUNCE 전략은 em.persist()를 호출할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다. 그리고 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다(식별자 할당 -> 영속성 컨텍스트에 저장). 이후 트랜잭션을 커밋하면 플러시가 일어나면서 엔티티를 데이터베이스에 저장한다.
IDENTITY 전략은 em.persist() 호출 즉시 엔티티를 데이터베이스에 저장하고, 식별자를 조회해서 엔티티에 식별자를 할당한다(데이터베이스에 저장 -> 식별자 할당).
@SequenceGenerator 속성은 다음과 같다.
속성 | 기능 | 기본 값 |
---|---|---|
name | 식별자 생성기 이름 | 필수 |
sequenceName | 데이터베이스에 등록된 시퀀스 이름 | hibernate_sequence |
initialValue | 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정 | 1 |
allocationSize | 시퀀스를 한 번 호출할 때 증가하는 수 | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 |
매핑할 DDL은 다음과 같다.
create sequence [sequenceName] start with [initialValue] increment by [allocationSize]
SEQUENCER 전략은 데이터베이스와 2번 통신한다.
식별자를 구하기 위해 데이터베이스 시퀀스를 조회
ex, SELECT BOARD_SEQ.NEXTVAL FROM DUAL
조회한 시퀀스를 기본 키 값으로 사용해 데이터베이스에 엔티티를 저장
ex, INSERT INTO BOARD ...
따라서 JPA는 시퀀스에 접근하는 횟수를 줄이기 위한 최적화 방법으로 allocationSize를 사용한다. 하이버네이트의 경우 기본 값은 50인데, 최초에 데이터베이스에 시퀀스를 호출한 이후 50까지는 메모리에서 현재 시퀀스 값을 저장하고 가상으로 값을 증가시키며 관리하고, 이후 51이 되는 시점에 데이터베이스의 시퀀스를 한 번 더 호출하고 51부터 100까지 가상으로 시퀀스 식별자를 관리한다. 이때 주의할 점은 데이터베이스의 시퀀스 증가 값이 1인 경우, 반드시 allocationSize 또한 1로 맞춰 주어야 한다.
이 최적화 방법은 시퀀스 값을 선점하므로 여러 JVM이 동시에 동작해도 기본 키 값이 충돌하지 않는다. 단, 데이터베이스에 직접 접근해서 데이터를 등록할 때 시퀀스 값이 한 번에 많이 증가한다는 점을 염두해야 한다.
allocationSize 동작 방식은 다음과 같다.
최초 persist() 실행시 데이터베이스 시퀀스를 두 번 호출하여, 첫번째 시퀀스 값을 가상으로 관리할 시작 값, 두번째 시퀀스 값을 가상으로 관리할 범위의 끝 값(MAX)으로 지정한다.
이후에는 persist()를 실행해도 데이터베이스에 시퀀스를 호출하지 않고 메모리에서 가상으로 관리하며 할당한다. persist()를 실행할 때마다 메모리에서 관리하는 가상의 값을 1씩 증가시키며 엔티티에 할당한다.
어느 시점, 엔티티에 식별자를 할당할 값이 관리할 범위의 끝 값(MAX)이 되고, 이후 다시 한 번 persist()를 실행하면 데이터베이스에 시퀀스를 호출한다.
다시 호출한 시퀀스 값을 가상으로 관리할 끝 값(MAX)으로 바꾸고, 시작 값은 끝 값 - (allocationSize - 1) 값으로 바꾼다.
ex, 데이터베이스 시퀀스 증가 값이 50, allocationSize 값이 50인 경우
최초에 persist() 호출시 엔티티의 식별자를 구하기 위해 데이터베이스 시퀀스를 두 번 호출한다. 이때 1과 51이 리턴되는데, 1을 JPA가 메모리에서 관리할 시작 값, 51을 끝 값(MAX)으로 지정한다.
엔티티에 식별자로 51이 할당되기까지 데이터베이스에 시퀀스를 호출하지 않고 JPA가 직접 가상의 시퀀스 값을 할당한다. 그리고 persist()로 엔티티를 저장하다보니 어느 시점, 가상의 시퀀스 끝 값(MAX)인 51이 엔티티의 식별자로 지정된다.
이후 다시 한 번 persist()를 통해 엔티티를 저장하려는 시점에 데이터베이스에 시퀀스를 호출한다. 이때 101이 리턴되고 이를 JPA가 가상으로 관리하는 시퀀스의 끝 값(MAX)으로 지정한다.
그리고 공식을 적용하여 101-(50-1) = 52를 시작 값을 지정한다. 그리고 52를 현재 persist()하려는 엔티티의 식별자로 할당한다.
이후에는 엔티티의 식별자 값이 MAX 값이 될 때까지 데이터베이스에 시퀀스를 조회하지 않고 가상의 시퀀스 값을 엔티티에 할당한다.
ex, 데이터베이스 시퀀스 증가 값이 1, allocationSize 값이 50인 경우
이 경우 식별자 관련 예외가 발생하므로 데이터베이스 시퀀스 증가 값이 1이면 allocationSize도 1이어야 한다.
최초에 persist() 호출시 엔티티의 식별자를 구하기 위해 데이터베이스 시퀀스를 두 번 호출한다. 이때 1과 2가 리턴되는데, 1을 JPA가 메모리에서 관리할 시작 값, 2를 끝 값(MAX)으로 지정한다.
persist()로 엔티티를 계속 저장하다 보니 어느 시점, 가장 최근에 persist() 실행시 할당했던 식별자 값이 가상의 시퀀스의 끝 값(MAX)인 2이다.
이후 persist()를 통해 엔티티를 저장하려는 시점에 데이터베이스에 시퀀스를 호출한다. 이때 3이 리턴되도 이를 JPA가 가상으로 관리하는 시퀀스의 끝 값(MAX)으로 지정한다.
그리고 공식을 적용하여 3-(50-1) = -46을 시작 값으로 지정한다. 그리고 -46을 현재 persist()하려는 엔티티의 식별자로 할당한다.
이후에는 엔티티의 식별자 값이 MAX 값이 될 때까지 데이터베이스에 시퀀스를 조회하지 않고 가상의 시퀀스 값을 엔티티에 할당한다. 식별자에 -46, -45, -44가 순차적으로 할당되어 persist()되고 어느 시점, 가상으로 관리하는 시퀀스에서 할당할 식별자 값이 1이 되는 순간 persist()를 실행하면 엔티티 식별자 중복 에러가 발생한다. 따라서 데이터베이스 시퀀스 증가 값이 1이면 allocationSize도 1로 지정해야 한다.
ex, 데이터베이스 시퀀스 증가 값이 1, allocationSize 값이 50인 경우
이 경우는 persist()를 실행할 때마다 데이터베이스 시퀀스로부터 값을 얻어 식별자로 할당하게 된다.
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;
...
}
사용 코드는 다음과 같다.
Board board = new Board();
em.persist(board);
System.out.println(board.getId());
그리고 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 속성은 다음과 같다.
속성 | 기능 | 기본 값 |
---|---|---|
name | 식별자 생성기 이름 | 필수 |
table | 키 생성 테이블명 | hibernate_sequences |
pkColumnName | 시퀀스 컬럼명 | sequence_name |
valueColumnName | 시퀀스 값 칼럼명 | next_val |
pkColumnValue | 키로 사용할 값 이름 | 엔티티 이름 |
initialValue | 초기 값, 마지막으로 생성된 값이 기준 | 0 |
allocationSize | 시퀀스를 한 번 호출할 때 증가하는 수 | 50 |
catalog, schema | 데이터베이스 catalog, schema 이름 | |
uniqueConstraints | 유니크 제약 조건을 지정할 수 있음 |
TABLE 전략은 값을 조회하면서 select 쿼리를 사용하고, 다음 값으로 증가시키기 위해 update 쿼리를 사용한다. 이 전략은 SEQUENCE 전략과 비교해서 데이터베이스와 한 번 더 통신한다는 단점이 있다. TABLE 전략을 최적화하려면 @TableGenerator.allocatinoSize
를 사용하면 된다.
AUTO 전략은 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택한다. 예를 들어 오라클을 선택하면 SEQUENCE를, MySQL을 선택하면 IDENTITY를 사용한다.
@GeneratedValue.strategy의 기본 값은 AUTO이다. 따라서 @GeneratedValue
와 @GeneratedValue(strategy = GenerationType.AUTO)
는 같다.
AUTO 전략의 장점은 데이터베이스를 변경해도 코드를 수정할 필요가 없다는 것이다. 특히 키 생성 전략이 아직 확정되지 않은 개발 초기 단계나 프로토타입 개발 시 편리하게 사용할 수 있다.
AUTO를 사용할 때 SEQUENCE나 TABLE 전략이 선택되면 시퀀스나 키 생성용 테이블을 미리 만들어 두어야 한다. 만약 스키마 자동 생성 기능을 사용한다면, 하이버네이트가 기본 값을 사용해서 적절한 시퀀스나 키 생성용 테이블을 만들어준다.