핵심개념이해 3
- Entity 상태
- Entity 상태란
- Entity 상태별 의미 및 Entity 상태 변화 흐름
- JPA 특징
- 1차 캐시
- dirty check
- write behind
- Cascade
Entity 상태
Entity 상태란
정확한 표현은 JPA 에서 Entity를 관리할 때 관리의 기준으로 삼는 Entity의 상태를 의미한다.
Entity 상태별 의미 및 Entity 상태 변화 흐름
출처 : 강의자료
*Transient : 임시로 체류중인 상태로 JPA가 관심을 두지 않는 상태(관리 X)
*Persistent : JPA가 해당 Entity 에 대해 관심을 두고 관리중인 상태(관리 O)
*Detached : JPA가 더이상 해당 Entity 에 대해 관심을 두지 않지 않는 상태(관리 X)
*Removed : JPA가 관리하긴 하지만 삭제하기로 한 상태(Removed 상태라고 해서 아직 delete 쿼리가 수행된 것이 아님)(관리 X)
JPA가 해당 Entity에 대해 관리중 = 1차 캐시, Dirty Check, Write Behind 가 발생한다는 의미
JPA 특징
1차 캐시
1차 캐시라고 굳이 1차를 달아준 이유는 2차 캐시가 존재하기 때문이다. 1차 캐시와 2차 캐시의 차이는 나중에 다루고, 일단 1차 캐시만 정리한다.
출처 : https://www.tecbar.net/hibernate-caching/
1차 캐시는 Session에서 관리되는 것으로 근본적인 목적은 일반적인 캐시의 목적과 동일한 성능 향상이다. 그림에서 볼 수 있듯이 조회시 PK를 기준으로 캐시에 있으면 Database 에 커넥션을 만들지 않고 캐시에서 바로 가져올 수 있도록 한다. 예제를 보자.
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
Account account = new Account();
account.setName("fistkim");
entityManager.persist(account);
Session session = entityManager.unwrap(Session.class);
Account account1 = session.get(Account.class, 1L);
System.out.println(account1.getName());
Account account2 = session.get(Account.class, 1L);
System.out.println(account2.getName());
Account account3 = session.get(Account.class, 1L);
System.out.println(account3.getName());
}
위 로직만 보면 조회가 3번 이뤄져야한다. 하지만 로그를 보면 결국 조회가 한 번도 이뤄지지 않은 것을 알 수 있다.
Hibernate:
insert
into
account
(name)
values
(?)
fistkim
fistkim
fistkim
조회 요청시 PK를 기준으로 먼저 1차 캐시를 탐색하게 되는데, 1L 에 해당하는 해당 entity 는 이미 entityManager.persist(account); 에서 1차 캐시에 저장이 된 상태이기 때문에 DB 에 직접 조회할 필요가 없다. 그렇기에 select 쿼리가 발생하지 않은 것이다.
그렇다면 2L로 조회하게 되면 1차 캐시에 해당 entity 가 없어서 select 조회가 발생해야하는데 진짜로 그런지 한번 보자.
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
Account account = new Account();
account.setName("fistkim");
entityManager.persist(account);
Session session = entityManager.unwrap(Session.class);
Account account1 = session.get(Account.class, 2L);
Account account2 = session.get(Account.class, 2L);
Account account3 = session.get(Account.class, 2L);
}
Hibernate:
insert
into
account
(name)
values
(?)
Hibernate:
select
account0_.id as id1_0_0_,
account0_.name as name2_0_0_
from
account account0_
where
account0_.id=?
Hibernate:
select
account0_.id as id1_0_0_,
account0_.name as name2_0_0_
from
account account0_
where
account0_.id=?
Hibernate:
select
account0_.id as id1_0_0_,
account0_.name as name2_0_0_
from
account account0_
where
account0_.id=?
실제로 조회가 되고 있음을 확인할 수 있다.
dirty check
JPA는 entity의 조회 시점에 스냅샷을 만들어 두고 이를 트랜잭션이 끝나는 시점에 비교하여 변경된 것(dirty)이 있으면 이를 영속화 해준다.
상태가 변경된 것(dirty)을 트랜잭션이 끝나는 시점에 알아서 확인(check) 해서 반영해주는 것이 곧 dirty check 인 것이다.
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
Account account = new Account();
account.setName("fistkim");
entityManager.persist(account);
account.setName("fistkim1");
}
위 코드에서 update 요청을 하지 않아도 아래와 같이 Update 쿼리가 수행된다.
Hibernate:
insert
into
account
(name)
values
(?)
Hibernate:
update
account
set
name=?
where
id=?
왜냐하면 처음에 fistkim 상태로 persist가 되었기 때문에 트랜잭션이 종료되는 시점에 dirty check를 해보니 fistkim 이어야 할 name이 fistkim1로 변경된 것이 확인되었기 때문이다.
그렇다면 변경을 많이 발생시키고 최종적으로는 결국 초기 상태 그대로(결국 dirty 가 없게)로 만들면 어떻게 될까?
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
Account account = new Account();
account.setName("fistkim");
entityManager.persist(account);
account.setName("fistkim1");
account.setName("fistkim2");
account.setName("fistkim3");
account.setName("fistkim");
}
Hibernate:
insert
into
account
(name)
values
(?)
fistkim -> fistkim1 -> fistkim2 -> fistkim3 -> fistkim 으로 결국 트랜젝션이 종료되는 시점의 상태가 최초에 persist 된 상태 그대로 이므로 dirty check 에서 걸리는 것이 없어서 update 문이 발생하지 않은 것을 알 수 있다.
Write behind
위에서 1차 캐시와 dirty check 에서 확인한 바와 같이, JPA 는 트랜젝션이 끝나는 순간(= commit 이 수행되는 순간)에 쿼리 요청을 수행한다. 이를 Write behind 라고 하는데 위에서 다룬 1차 캐시와 밀접한 연관이 있다.
The persistence context, also known as the first level cache, acts as a buffer between the current entity state transitions and the database.
In caching theory, the write-behind synchronization requires that all changes happen against the cache, whose responsibility is to eventually synchronize with the backing store.
위 글에도 나와있다시피 단순히 ‘당장 쿼리 수행을 요청하지 않고 commit이 될때 한번에 한다’의 개념이 아니라, 쿼리 수행 요청을 지연함으로써 최적으로 꼭 필요한 쿼리만을 최종 판단하여 수행 요청한다는 개념으로 이해하는 것이 좋을 것 같다.
Cascade
Cascade는 사전적으로 ‘폭포’, ‘폭포처럼 흐르다’라는 뜻이다. JPA 에서는 이러한 맥락에서 Cascade가 위에서 설명한 ‘Entity의 상태’를 전파하는 ‘상태 전파 전략 또는 설정’의 개념으로 사용된다.
@Override
@Transactional
public void run(ApplicationArguments args) throws Exception {
Post post = new Post();
post.setName("첫번째 포스팅");
entityManager.persist(post);
Comment comment = new Comment();
comment.setContents("첫번째 댓글 내용");
post.addComment(comment);
}
//@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
@OneToMany(mappedBy = "post")
private Set<Comment> comments = new HashSet<>();
public void addComment(Comment comment) {
comment.setPost(this);
this.comments.add(comment);
}
위와 같이 포스팅에 댓글을 달아 준다고 했을 때, cascade 옵션을 설정해주지 않으면 실제로 comment 테이블에는 insert 가 발생하지 않는다.