기본키 생성 전략에 따른 JPA 의 기본 동작 차이.
IDENTITY
IDENTITY 전략에서는 PK (auto_increment) 값이 Insert 쿼리가 DB에 COMMIT 되는 시점에 확정된다.
문제 상황
영속성 컨텍스트를 가진 JPA 입장에서 어떤 트랜잭션 내에서 COMMIT 하기 전에 PK (ID) 를 사용해야 한다면 어떻게 해야할까?
em.persist(entity) 시점에
INSERT 쿼리를 실행한다.
(아직 COMMIT 전)
Member.java
@Entity
@Table(name = "member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Column(name = "member_name", updatable = true, nullable = false)
private String name;
public static Member registerMember(String name) {
var member = new Member();
member.name = name;
return member;
}
}
MemberTest.java
class MemberTest {
private final EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-practice");
private EntityManager em;
@Test
void strategyIdentityTest() {
var tx = getEntityTransaction();
try {
tx.begin();
var member1 = Member.registerMember("제이스");
System.out.println("=============================");
em.persist(member1);
System.out.println("=============================");
assertThat(member1.getId()).isEqualTo(1L);
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
}
}
실행 결과
=============================
15:07:13.566 [Test worker] DEBUG org.hibernate.engine.spi.ActionQueue - Executing identity-insert immediately
15:07:13.577 [Test worker] DEBUG org.hibernate.SQL -
/* insert com.example.jpapractice.domain.member.Member
*/ insert
into
member (member_id, age, create_date, description, last_modified_date, member_name, role_type)
values
(default, ?, ?, ?, ?, ?, ?)
Hibernate:
/* insert com.example.jpapractice.domain.member.Member
*/ insert
into
member (member_id, age, create_date, description, last_modified_date, member_name, role_type)
values
(default, ?, ?, ?, ?, ?, ?)
15:07:13.590 [Test worker] DEBUG org.hibernate.id.IdentifierGeneratorHelper - Natively generated identity: 1
=============================
em.persist(member) 하자마자 INSERT 쿼리가 실행된다.
물론 곧바로 영속성 컨텍스트에 써서 등록한다. (1차 캐싱상태 완료)
IDENTITY PK를 갖는 엔티티 클래스의 em.persist() 내부 동작 순서
- INSERT 쿼리를 일단 실행한다.
(완전히 COMMIT 하진 않는다.) - 생성된 IDENTITY 값을 내부적으로 가져온다.
- 영속성 컨텍스트에 가져온 PK값을 ID로 삼아 객체를 등록한다.
- 트랜잭션을 COMMIT 하여 영구 반영한다.
SEQUENCE
별도로 sequence 라고하는 object 가 DB 내에 생성된다.
em.persist 를 호출하는 시점에 타겟팅할 sequence 와 값만 불러온다.
Member.java
@Entity
@Table(name = "member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "member_id")
private Long id;
@Column(name = "member_name", updatable = true, nullable = false)
private String name;
public static Member registerMember(String name) {
var member = new Member();
member.name = name;
return member;
}
}
생성된 테이블 DDL
테이블 정보와 생성된 시퀀스를 모두 확인할 수 있다.
create table MEMBER
(
MEMBER_ID BIGINT auto_increment
primary key,
AGE INTEGER
);
-- auto-generated definition
create sequence MEMBER_SEQ
increment by 50;
MemberTest.java
class MemberTest {
private final EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-practice");
private EntityManager em;
@Test
void strategySequenceTest() {
var tx = getEntityTransaction();
try {
tx.begin();
var member1 = Member.registerMember("제이스");
System.out.println("=============================");
em.persist(member1);
System.out.println("=============================");
assertThat(member1.getId()).isEqualTo(1L);
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
}
private EntityTransaction getEntityTransaction() {
this.em = emf.createEntityManager();
return em.getTransaction();
}
}
실행 결과
member.getId() 에서 SELECT 를 통해 결정된 시퀀스 값을 끌고온다.
마지막으로 INSERT 쿼리가 실행된다.
=============================
14:53:05.757 [Test worker] DEBUG org.hibernate.SQL -
select
next value for member_seq
Hibernate:
select
next value for member_seq
14:53:05.759 [Test worker] DEBUG org.hibernate.id.enhanced.SequenceStructure - Sequence value obtained: 1
14:53:05.761 [Test worker] DEBUG org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 1, using strategy: org.hibernate.id.enhanced.SequenceStyleGenerator
=============================
/* insert com.example.jpapractice.domain.member.Member
*/ insert
into
member (age, create_date, description, last_modified_date, member_name, role_type, member_id)
values
(?, ?, ?, ?, ?, ?, ?)
em.persist 해도
곧바로 ISNERT 쿼리가 실행되지 않는다.
SEQUENCE PK를 갖는 엔티티 클래스의 em.persist() 내부 동작 순서
- DB에서 지정된 next SEQUENCE 값을 찾아온다.
- 영속성 컨텍스트에 객체를 등록한다.
- INSERT 쿼리를 실행한다.
- COMMIT 한다.
allocationSize 가 50이 Default 인 이유 - 동시성 Issue
⚠️ 추후 내용 추가 예정
allocationSize 만큼 DB에는 미리 올려놓고 메모리에서는 1씩 쓰는 것.
여러 웹서버가 있어도 동시성 이슈 없이 다양한 문제를 해결할 수 있게한다.
✒️ TIP
`em.persist()` 시점에 즉시 INSERT 쿼리를 실행하는 하는 IDENTITY 보다 쿼리를 버퍼링하여 지연 실행하는 SEQUENCE 전략이 좋아보일 수 있으나, 버퍼링 전략에 대한 성능 차이는 미미하다.
JDBC Batch 같은 측면에서 보면 Network I/O 가 줄어들기 때문에 SEQUENCE 가 낫다.
상상해보라. IDENTITY 를 쓰면 총 50개의 INSERT 쿼리를 실행하고자 할 때 50번 각각의 INSERT 실행이 필요하다. (다음 PK 값을 생성하기 위해) 반면에 SEQUENCE 는 JVM 메모리 내에서 가상의 번호로 allocationSize 를 갖고 JDBC Batch 연산이 가능하므로 최적화에 용이하다.
📝 Conclusion
INSERT Batch 처리가 필요하다면 SEQUENCE를 권장하고
그 외에는 IDENTITY 를 사용해도 좋다.
It is important to realize that using IDENTITY columns imposes a runtime behavior where the entity row
must be physically inserted prior to the identifier value being known.
There is yet another important runtime impact of choosing IDENTITY generation: Hibernate will not be able to batch INSERT statements for the entities using the IDENTITY generation.
The importance of this depends on the application-specific use cases. If the application is not usually creating many new instances of a given entity type using the IDENTITY generator, then this limitation will be less important since batching would not have been very helpful anyway.
- Hibernate docs
🔗 Reference
'JVM > JPA' 카테고리의 다른 글
[JPA] 상속관계 매핑 (0) | 2023.01.13 |
---|---|
[JPA] 1:1 연관관계 설정 (0) | 2023.01.12 |
[JPA] JPA 는 왜 등장했을까? (0) | 2023.01.07 |
[JPA] API 생성시 Entity 를 반환하지 말자. (0) | 2022.12.29 |