DEV ℧ Developer Diary

[JPA] 기본키 매핑

해당 포스트는 인프런 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 듣고 정리한 글입니다.

기본키 매핑

JPA에서 PK값인 기본키를 매핑하는것은 두가지 방법이 있다.

@Id

@GeneratedValue

두가지의 방법을 알아보고자 한다.

기본키 매핑방법

  • @Id : Sequence와 같은 자동으로 매핑되는 키를 사용하지 않는 경우, 직접 할당할때는 @Id만 사용한다.
  • @GeneratedValue : Sequence와 같은 자동 증가값을 키로 매핑 할 경우 사용한다.
    • IDENTITY : 데이터 베이스에 위임한다.
    • SEQUENCE : 데이터 베이스의 Sequence를 사용한다. ORACLE의 경우 @SequenceGenerator이 필요하다.
    • TABLE : 키 생성용 테이블을 생성해서 매핑한다. 모든 DB에서 사용한다. @TableGenerator이 필요하다.
    • AUTO : 설정한 DB 방언에 따라 자동으로 지정한다, 기본값이다.

@Id

자동으로 증가되는 값이 아닌 직접적으로 지정하고자 할때 @Id만 사용한다.

/* String의 PK 설정 */
@Id
private String id;
/* 임의의 PK값 설정 */
User user = new User();
user.setId("PK_A");
user.setUsername("만득이");
user.setRoleType(RoleType.USER);
entityManager.persist(user);

result1

IDENTITY 전략

  • 기본 키 생성을 데이터 베이스에 위임한다.
  • 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다.
    • (예 : MySQL의 AUTO_INCREMENT)

기본키 값을 넣어주지 않아도 자동으로 증가하는 값을 생성한다.

/* IDENTITY 전략 사용 */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/* 기본키 값을 넣어주지 않는다. */
User user = new User();
user.setUsername("만득이");
user.setRoleType(RoleType.USER);
entityManager.persist(user);
/* insert com.dhaudgkr.jpastart.column.User */
insert
into
    TB_USER
    (id, age, createdDate, description, lastModifiedDate, roleType, name)
values
    (default, ?, ?, ?, ?, ?, ?)

result2

IDENTITY의 특징

JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL을 실행 하지만, AUTO_INCREMENT는 데이터베이스에 INSERT SQL을 실행한 뒤 값을 알 수 있다보니 IDENTITY만 유일하게 entityManager.persist() 시점에 즉시 INSERT SQL을 싱행하고 DB에서 식별자를 조회한다.

영속성 컨텍스트에 엔티티를 넣기 위해서는 PK 값이 필요한데 해당 값을 DB에 직접적으로 접근하기전에는 알 수 없기 때문이다.

User user = new User();
user.setUsername("만득이");
user.setRoleType(RoleType.USER);
log.debug("persist BEFORE");
entityManager.persist(user);
log.debug("persist AFTER");

entityTransaction.commit();
/* persist()를 실행할때 INSERT SQL을 날린다. */
02:00:48.784 [main] DEBUG com.dhaudgkr.jpastart.column.ColumnMain - persist BEFORE
    /* insert com.dhaudgkr.jpastart.column.User
        */ insert
        into
            TB_USER
            (id, age, createdDate, description, lastModifiedDate, roleType, name)
        values
            (default, ?, ?, ?, ?, ?, ?)
02:00:48.817 [main] DEBUG com.dhaudgkr.jpastart.column.ColumnMain - persist AFTER

SEQUENCE 전략

  • 데이터베이스 시퀀스를 생성하여 기본키의 값을 순서대로 생성한다.
  • 주로 오라클에서 쓰이며, PostgreSQL, DB2, H2 데이터베이스에서 사용한다.

IDENTITY 전략과 결과는 동일하지만, 시퀀스를 매핑하여 값을 생성한다.

/* SEQUENCE 전략 사용 */
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
/* TABLE 생성시 sequence도 같이 생성한다. */
create sequence hibernate_sequence start with 1 increment by 1

create table TB_USER (
   id bigint not null,
   age integer,
   createdDate timestamp,
   description clob,
   lastModifiedDate timestamp,
   roleType integer,
   name varchar(255),
   primary key (id)
)
User user = new User();
user.setUsername("만득이");
user.setRoleType(RoleType.USER);
entityManager.persist(user);
/* 생성한 sequence에서 값 증가 */
call next value for hibernate_sequence

/* insert com.dhaudgkr.jpastart.column.User
*/ insert
into
    TB_USER
    (age, createdDate, description, lastModifiedDate, roleType, name, id)
values
    (?, ?, ?, ?, ?, ?, ?)

이하 결과는 IDENTITY와 동일하다.

추가로 hibernate에서 기본적으로 생성하는 시퀀스가 아닌 시퀀스를 매핑하거나 따로 생성하려면 추가로 @SequenceGenerator를 선언하면 된다.

@Entity
@SequenceGenerator(name = "user_seq_generator", sequenceName = "user_seq")
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq_generator")
  private Long id;
}
create sequence user_seq start with 1 increment by 50

@SequenceGenerator

  • name : 식별자 생성기 이름 (필수값)
  • sequenceName : 데이터베이스에 등록되어 있는 시퀀스 이름 (기본값 : hibernate_sequence)
  • initialValue : DDL 생성 시에만 사용된다, 시퀀스 DDL을 생성할 때 처음 시작하는 수를 지정한다. (기본값 : 1)
  • allocationSize : 시퀀스 한번 호출에 증가하는 수 (성능 최적화를 위해 사용된다.) 데이터베이스 시퀀스 값이 하나씩 증가하도록 성정되어 있으면 이 값을 반드시 1로 설정해야 한다. (기본값 : 50 주의)
  • catalog, schema : 각각 데이터베이스 catalog, schema의 이름

SEQUENCE의 특징

SEQUENCE도 마찬가지로 결국 값을 증가시키는 시퀀스가 DB에서 관리되는 것이다 보니 PK값을 영속성에 넣어주기 전에 알 수 가 없다. 하지만 영속성 컨텍스트에 넣어주기 위해서는 PK값이 필수로 필요하기 때문에, 시퀀스를 미리 조회해서 PK값을 넣어준다.

User user = new User();
user.setUsername("만득이");
user.setRoleType(RoleType.USER);
log.debug("persist BEFORE");
entityManager.persist(user);
log.debug("persist AFTER");

entityTransaction.commit();
02:04:17.696 [main] DEBUG com.dhaudgkr.jpastart.column.ColumnMain - persist BEFORE
    call next value for user_seq
02:04:17.736 [main] DEBUG com.dhaudgkr.jpastart.column.ColumnMain - persist AFTER

TABLE 전략

  • 키 생성 전용 테이블을 하나 만들어 데이터베이스 시퀀스를 흉내낸다.
  • 장점은 모든 데이터 베이스에 적용이 가능하다.
  • 단점은 테이블을 또 하나 생성하여 값을 증가시키다보니 DB Lock 과같은 성능 이슈가 있을 수 있다.

@TableGenerator 를 이용하여 키 매핑용 테이블을 만들고 Id값과 매핑하여 값을 생성한다.

/* 키 매핑용 테이블을 생성하여 Id와 매핑 */
@Entity
@TableGenerator(
        name = "USER_SEQ_GENERATOR",
        table = "MY_SEQUENCES",
        pkColumnValue = "USER_SEQ", allocationSize = 1)
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.TABLE, generator = "USER_SEQ_GENERATOR")
  private Long id;
}
/* 키 매핑용 테이블 생성 */
create table MY_SEQUENCES (
   sequence_name varchar(255) not null,
   next_val bigint,
   primary key (sequence_name)
)

create table TB_USER (
   id bigint not null,
   age integer,
   createdDate timestamp,
   description clob,
   lastModifiedDate timestamp,
   roleType integer,
   name varchar(255),
   primary key (id)
)
User user = new User();
user.setUsername("만득이");
user.setRoleType(RoleType.USER);
entityManager.persist(user);
/* 키 매핑용 테이블에서 값을 조회하고 숫자를 증가시킨다. */
select
    tbl.next_val
from
    MY_SEQUENCES tbl
where
    tbl.sequence_name=? for update

update
    MY_SEQUENCES
set
    next_val=?
where
    next_val=?
    and sequence_name=?

/* insert com.dhaudgkr.jpastart.column.User
*/ insert
into
    TB_USER
    (age, createdDate, description, lastModifiedDate, roleType, name, id)
values
    (?, ?, ?, ?, ?, ?, ?)

시퀀스용 테이블 생성 및 결과

result2

@TableGenerator

  • name : 식별자 생성기 이름 (필수)
  • table : 키 생성 테이블명 (기본값 : hibernate_sequences)
  • pkColumnName : 시퀀스 컬럼명 (기본값 : sequence_name)
  • valueColumnName : 시퀀스 값 컬럼명 (기본값 : next_val)
  • pkColumnValue : 키로 사용할 값 이름 (기본값 : 엔티티 이름)
  • initialValue : 초기 값, 마지막으로 생성된 값이 기준이다. (기본값 : 0)
  • allocationSize : 시퀀스 한번 호출에 증가하는 수 (성능 최적화에 사용됨) (기본값 : 50 주의)
  • catalog, schema : 각각 데이터베이스 catalog, schema 이름
  • uniqueConstraints(DDL) : 유니크 제약 조건을 지정할 수 있다.

allocationSize

이에대한 성능개선에 대한 팁으로 @TableGenerator@SequenceGenerator 속성 중 allocationSize 속성이 있는데 해당 속성에 값을 주면 그 값만큼 미리 시퀀스나 테이블에서 값을 받아와 Memory에 저장한다. 그래서 다음 엔티티에는 Memory에 저장된 값을 부여한다.

성능면으로는 좋을 수 있으나, 서버가 내려가 Memory가 초기화 된다면 이미 증가된 Sequence의 값은 되돌릴 수 없으니 중간에 구멍이 생길 위험이 있다.

권장 하는 식별자 전략?

세개의 식별자 전략을 사용해야 할까. 보통 TABLE 생성 전략은 성능이슈로 인해 운영에서 잘 사용하지 않는다.

기본키 제약 조건을 지켜야 하는데,

  • null이 아닐것
  • 유일할것
  • 변하면 안될것 해당 세가지의 조건을 지켜야 한다.

예를 들면 주민등록번호도 기본키로 적절하지 않다. why? JOIN시 주민등록번호를 외래키로 잡게 되는데 제약 조건에 의해 주민등록번호가 모든 테이블에 퍼져있게됨..

권장 : Long형 + 대체키 + 키 생성전략 생성

IDENTITY와 같은 Auto_Increment나 SEQUENCE를 DB의 종류에 따라서 선택해서 사용하거나 때에 따라 UUID, 혹은 컨벤션에 따라 랜덤 값을 넣어주는 것이 좋다.