JPA 게시글은 대부분 인프런의 김영한님의 강의인 '자바 ORM 표준 JPA 프로그래밍' 기반으로 내용을 정리했습니다.
기본 키 매핑
기본키의 경우엔 직접 할당과 자동생성 방법이 있다. 직접 할당은 여태 써온 @Id 애노테이션을 사용하면 된다.
직접 할당
- @Id
// Member.class
@Id
private Long id;
// 실행 클래스
...생략
Member member = new Member();
member.setId(1000L); // 기본키 직접 할당
member.setUsername("B");
...생략
이렇게 개발자가 직접 PK 값을 할당해주는 경우 @Id만 사용하면 된다.
자동 생성(@GeneratedValue)
자동 생성의 경우 IDENTITY, SEQUENCE, TABLE, AUTO 속성이 있다.
IDENTITY 전략
- 기본키 생성을 데이터베이스에 위임한다.
- 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다.
- JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL을 실행한다.
- AUTO_INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있다.
- IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행하고 DB에서 식별자를 조회한다.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 실행 클래스
...생략
Member member = new Member();
member.setUsername("B");
em.persist(member);
...생략
// 출력 로그
create table Member (
id bigint generated by default as identity,
name varchar(255) not null,
primary key (id)
)
/* insert hellojpa.Member
*/ insert
into
Member
(id, name)
values
(null, ?)
객체 생성 시 id 값을 따로 세팅하지 않아도 자동으로 insert문에 id 데이터가 저장되어 INSERT SQL이 전달된다.
IDENTITY은 위에 적혀있기도 하지만 중요한 이야기니 다시 얘기해보자면 id 값이 JPA에서 저장되는게 아니라, JPA INSERT SQL을 날릴 때 id가 null 값으로 되어있으면 DB에서 id 값을 세팅해준다. 즉 id 값을 알 수 있는 시점은 DB에 저장이 되고나서야 알 수 있다는 것이다. 이게 왜 문제가 될까? JPA에는 영속성 컨텍스트가 존재한다. 데이터가 영속성 컨텍스트에서 관리가 되려면 무.조.건 id 값이 존재해야한다. 그래서 이 문제를 해결하기 위해 특별히 IDENTITY 전략은 기존 매커니즘과 다르게 동작한다.
일단 transaction.commit() 시점이 아니라 em.persist()가 실행되는 시점에 INSERT SQL문이 전달된다. 그리고 JPA가 DB에 저장되어 있는 id 값을 다시 SELECT해서 가져와 영속성 컨텍스트에 id값을 넣어 관리한다.
결론은 그래서 IDENTITY 전략에서는 SQL을 모아서 쿼리를 한 번에 날리는 버퍼링 기능을 사용할 수 없다.
SEQUENCE 전략
- 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트이다.
- 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용한다.
- 참고로 필드 데이터 타입은 int에는 0이 포함되어있기 때문에 별로고 Integer를 사용하는게 더 낫다. 근데 또 Integer보다 Long을 사용하는게 더 낫다.
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
// 출력 로그
Hibernate:
call next value for hibernate_sequence //시퀀스 먼저 호출 후 INSERT SQL 실행
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
참고로 시퀀스에 관한 별도 설정이 없으면 기본 시퀀스인 hibernate_sequence를 사용한다. 만약 테이블마다 시퀀스를 따로 관리하고 싶다면 @SequenceGenerator를 사용해서 매핑을 하면된다.
- @SequenceGenerator 사용
hibernate.hbm2ddl.auto 값을 create로 설정한다.
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ", // 매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
...생략
// 출력 로그
Hibernate:
call next value for MEMBER_SEQ
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
- @SequenceGenerator 속성
- name : 식별자 생성기 이름. 필수값이다.
- sequenceName : 데이터베이스에 등록되어 있는 시퀀스 이름. 기본값은 hibernate_sequence
- initialValue : DDL 생성 시에만 사용된다. 시퀀스 DDL을 생성할 때 처음으로 시작하는 수를 지정한다. 기본값은 1이다.
- allocationSize : 시퀀스 한 번 호출에 증가하는 수(성능 최적화에 사용)이다. 데이터베이스 시퀀스 값이 하나씩 증가하도록 설정되어 있으면 이 값을 반드시 1로 설정해야 한다. 기본값은 50이다.
- catalog, schema : 데이터베이스 catalog, schema 이름이다.
SEQUENCE 전략의 경우에도 IDENTITY 전략과 똑같이 DB에 가기 전까지는 id 값을 알 수 없다. 대신 IDENTITY 전략과 조금 다른 점이 있는데 SEQUENCE 전략은 em.persist()가 실행 될 때 시퀀스를 호출해 id 값을 가져와 객체에 id를 세팅하고나서 영속성 컨텍스트에 저장한다. 즉 INSERT SQL을 보내기 전에 id 값을 세팅한다. 그러기 때문에 IDENTITY 전략에서 사용하지 못하는 버퍼링 기능을SEQUENCE 전략에서는 사용할 수가 있다.
그럼 성능에는 아무 영향이 없을까?
뭔가 SEQUENCE 전략이나 IDENTITY 전략은 INSERT SQL를 보내기 전이나 후에 굳이 DB에 계속 네트워크를 날리면 성능이 떨어지지 않을까?라는 생각을 할 수 있다. 그래서 그 해결 방법으로 allocationSize라는 속성을 JPA에서 제공한다.
allocationSize는 시퀀스를 한 번 호출할 때 지정한 값만큼 미리 데이터베이스에 미리 적재시켜놓고 메모리에서는 하나하나씩 데이터를 쌓아간다. 그러다가 데이터베이스에 미리 적재시켜 놓은 개수와 메모리에 쌓인 개수가 같아지면 또 Next Call을 한 번 호출해서 데이터베이스에는 50~100번까지의 50개의 데이터를 미리 적재시켜 놓고 메모리에서는 51~100번까지 데이터를 담도록한다.
또 allocationSize 기능이 너무 똑똑한 나머지 여러 WAS에서 동시에 실행시켜도 동시성 이슈 없이 잘 진행이 된다.
// 객체 클래스
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ",
initialValue = 1, allocationSize = 50) // 50으로 설정
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
... 생략
// 실행 클래스
... 생략
Member member1 = new Member();
member1.setUsername("A");
Member member2 = new Member();
member2.setUsername("B");
Member member3 = new Member();
member3.setUsername("C");
// DB 시퀀스 : 1 | 필드 id : 1
// DB 시퀀스 : 51 | 필드 id : 2
// DB 시퀀스 : 51 | 필드 id : 3
em.persist(member1); // 1, 51
em.persist(member2); // 메모리
em.persist(member3); // 메모리
tx.commit();
... 생략
allocationSize의 적정 값은 50~100정도를 권장한다. 값이 너무 크면 웹 애플리케이션이 다시 구동될 때 전에 사용하지 못한 크기 만큼이 낭비되므로 주의해야한다.
TABLE 전략
키 전용 생성 테이블을 하나 만든다. 데이터베이스 시퀀스를 흉내내는 전략이다. 장점으로는 모든 데이터베이스에 적용이 가능하고 단점은 테이블을 직접 접근해서 성능 상의 이슈가 날 수도 있고, 또한 테이블에 락이 걸릴 수 있다는 단점을 가지고 있다.
hibernate.hbm2ddl.auto의 속성을 create로 변경한다.
// 객체 클래스
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = "MEMBER_SEQ", allocationSize = 1)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
그럼 MY_SEQUENCES 테이블이 생성되고 테이블 안에는 내가 만든 시퀀스와, 그 시퀀스의 다음 값을 저장하고 있다.
TABEL 전략은 잘 사용하지 않는다.
- TableGenerator - 속성
- name : 식별자 생성기 이름. 필수값이다
- table : 키 생성 테이블명 기본값은 hibernate_sequences이다.
- pkColumnName : 시퀀스 컬럼명. 기본값은 sequence_name이다.
- valueColumnNa : 시퀀스 값 컬럼명. 기본값은 next_val이다.
- pkColumnValue : 키로 사용할 값 이름. 기본값은 엔티티 이름이다.
- initialValue : 초기 값, 마지막으로 생성된 값이 기준이다.
- allocationSize : 시퀀스 한 번 호출에 증가하는 수 (성능 최적화에 사용). 기본값은 50이다.
- catalog, schema : 데이터베이스 catalog, schema 이름을 주면 된다.
- uniqueConstraints(DDL) : 유니크 제약 조건을 걸 수 있다.
권장하는 식별자 전략
- 기본키 제약 조건 : NOT NULL, 유일, 변하면 안된다.
- 미래까지 이 조건을 만족하는 자연키는 찾기 어려우니 대리키(대체키)를 사용하자.
- 대리키 : 비즈니스와 전혀 상관없는 키 (uuid나 auto_increment 등)
- ex) 주민등록번호 기본키로 적절하지 않다.
- 권장 : Long형 + 대체키 + 키 생성전략 사용