영속성 상태란?
흔히 JPA 에서는 객체 상태를 영속성 상태라고 하는
기준에 의거하여 Hibernate가 객체를 관리하게 되는데요..
핵심적인 상태는 총 3가지 입니다
- Managed(persist) 상태 : 영속성 컨텍스트에 저장된 상태
- Detached(clear, detach) 상태 : 영속성 컨텍스트에 저장되었다가 분리된 상태 = 준영속 상태
- Removed(removed) 상태 : 삭제된 상태
영속 상태
조금 쉽게 코드로 이야기하자면,
EntityManager에 의해서 관리되는 상태가 바로 영속성 상태입니다
em.persist(team)
persist 메서드는 주로 JPARepository에서 save시 일어나게 되죠.!
준영속 상태
영속성 상태에서 벗어나서 영속성 컨텍스트가 관리하지 않으면 준영속 상태로 돌아오게 됩니다.!
em.persist(team) // 영속
em.detach(team) // 준영속
em.clear() // 영속성 컨텍스트 초기화
삭제 상태
실제로 영속성 컨텍스트에서 삭제하고, 데이터베이스에 삭제쿼리를 날리기 위해 사용됩니다
em.remove(team) // 삭제
특징
- 영속성 컨텍스트에 등록(persist)되었다고, database에 save, update 쿼리가 나가는 것이 아니다
대표적으로 영속성 컨텍스트는 동일한 Transaction 내에서 모든 객체들을 관리하기 때문에,
특정 Transaction이 끝날때
em.flush()
flush하게 되고, 이 시점에 데이터베이스로 영속성 컨텍스트에 등록된 객체를 바탕으로
쿼리를 작성하여 보내게 됩니다.!
- 영속성 컨텍스트에는 id 를 바탕으로 1차 cache 에서 데이터들을 관리하게 된다
영속성 컨텍스트에 객체가 등록될 때, 해당 객체에 id가 있냐 없냐를 기준으로
새롭게 save 할 것인지, 혹은 update 할 것인지 결정하게 됩니다
이 개념은 너무나 중요하기 때문에 아래에서 더 다루도록 할게요.!
- 영속성 컨텍스트는 동일한 쓰레드에서만 유효하다
흔히 말해서 Thread-safe한 구조입니다.
어떤 요청에 의해서 1개의 쓰레드는 수정하고,
1개의 쓰레드에도 수정하면 thread-safe하지 않게되면 너무 꼬여버리겠죠.?
이제 기본적인 것을 알았으니 조금 더 중요한 것들을 알아봅시다
영속성 컨텍스트 활용하기
보통 영속성 컨텍스트가 중요한 이유는 아래와 같은 항목들이 있기 때문이에요
- 1차 cache
- Trasnaction 쓰기 지연
- 변경 감지 및 병합
- 지연 로딩
1차 Cache
1차 cache는 바로 코드부터 볼까요?
// find시에 일어나는 일들
// 1. 1차 cache에서 조회
// 2. 없다면 데이터베이스에 쿼리를 날려서 영속성 컨텍스트로 가져오고 1차 cache로 저장
val team1 = em.find(Team::class, "id1") // 이 경우에는 1차 cache에 없으므로 DB에서 가져옴
val team = em.find(Team::class, "id1") // 이 경우에는 1차 cache에 이미 있으므로 DB를 조회하지 않음
바로 이러한 이유때문에 똑같은 조회 쿼리에 대해서
계속 쿼리를 날리는게 아니라
딱 1번만 조회하여 DB 성능향상을 이끌어낼 수 있는 것이죠
쓰기 지연
한마디로, 위에서 언급한 동일한 transaction 내에서는 쿼리를 모아두고,
commit 되는 시점에 한번에 쓰는 것을 의미합니다
transaction.begin()
em.persist(team1)
em.persist(team2)
transaction.commit() // 이 시점에 team1, team2 insert 쿼리가 날라감
그렇기 때문에 더 신기한 작업이 하나 있죠
변경 감지 및 병합
동일한 transaction 내에서 영속성 컨텍스트에 등록된 엔티티에 대하여
변경을 감지하는 작업을 하게 됩니다.
이거는 어떻게 하냐구요.?
바로 식별자를 기반으로 구별하게 되는데요.
일반적으로 데이터베이스에서 객체를 가져오면,
가짜 객체(SNAPSHOT)을 만들고, 영속성 컨텍스트가 저장하게 됩니다
그리고, transaction이 끝나는 시점에 SNAPSHOT과 비교하여 어떤 부분이 바뀐 것인지 인지하고
update 쿼리를 날리게 됩니다
transaction.begin()
val team1 = em.find(Team::class, "id1") // 영속성 컨텍스트에 등록
team1.changeMember(member) // member 변경
transaction.commit() // member 변경에 대한 update 쿼리가 날라감
그렇다면 병합(merge)는 뭘까요,?
쉽게 설명하자면.. 준영속 상태의 객체로 덮어씌우는 방식입니다
동작방식은요
- 준영속 엔티티의 식별자(id) 값으로 영속 엔티티를 조회
- 영속 엔티티의 값을 해당 준영속 엔티티의 값으로 모두 교체
- transaction 커밋 시점에 변경 감지 기능이 동작해서 update 쿼리가 날라감
transaction.begin()
val team = Team(id = "id1", name = "A")
em.merge(team) // id1 이라는 DB 엔티티를 가져오고, team객체로 덮어씌우게 됨
transaction.commit() // id1에 대한 update 쿼리 실시
만일 team 객체에 null 값이 포함된 경우라면..
실제로 DB에도 empty Column 으로 들어갈 수 있기 때문에 update를 위해서는
merge 보다는 변경 감지를 활용하는 것이 좋습니다
그래서 일반적으로 병합은 특별한 경우가 아니라면 잘 사용하지 않는 것이 좋습니다
지연로딩
만일 엔티티 객체 내에 연관관계가 있는 객체가 존재할 경우
영속성 컨텍스트는 모든 객체를 조회하는 것이 아니라, 해당 객체를 사용하는 시점에 조회 하게 됩니다
transaction.begin()
val team = em.find(Team::class, "id1") // team 테이블 select 쿼리 발생, 영속성 컨텍스트 저장
val member = team.getMember() // 이때 member table select 쿼리 발생
한마디로, 사용 시점에 연관 관계에 mapping된 객체를 조회한다는 것이죠
그렇기 때문에 여기서 바로 N+1 문제가 발생하게 됩니다..
team의 Member가 N명이면, 모든 member 를 조회시에 총 N+1 번의 쿼리가 발생하게 됩니다
( 이를 해결해주는 것은 fetch join 등이 있습니다 ) = 쉽게 생각하면 1번에 모든 member를 가져오자
참고자료
JPA - 영속성 관리
'Developer > Spring' 카테고리의 다른 글
번외편 - Junit LifeCycle과 @ExtendWith 에 대해 알아보자 (0) | 2021.05.02 |
---|---|
Spring Cloud Feign Testing - Feign Client를 테스트해보자 (6) | 2021.04.11 |
Spring DI(Dependency Injection) & IoC Container - 의존성 주입이란? (0) | 2020.10.18 |
Spring Boot 에서 Log4j2 를 설정하는 방법 (0) | 2020.09.28 |
Spring AOP의 원리 - CGlib vs Dynamic Proxy (11) | 2020.08.18 |