사수님께서 Persistence Context에 관한 좋은 내용이 담긴 글을 공유해주셨다.
https://msolo021015.medium.com/jpa-persistence-context-deep-dive-2f36f9bd6214
@Modifying 을 사용중인 나에게 꽤나 충격적이었음..
findById를 해도 update된 데이터가 아니길래 정말 한참을 헤맸었는데...
역시 개념에 대한 정확한 이해 없이 개발을 하면 이렇게 되나보다.
JPA를 공부하겠다고 산 두꺼운 서적을 다시 한 번 제대로 살펴봐야겠다.
영속성 컨텍스트를 정확하게 이해하지 않고 그 누가 JPA를 사용한다고 하랴..
그래서 다시 한번 중요 개념을 정리하고 넘어가보자 한다.
- EntityManager & EntityManagerFactory
- JPA는 스레드 생성시 EntityManagerFactory에서 EntityManager를 생성하고, EntityManager는 DB 커넥션 풀을 사용해 DB에 접근한다.
- Persistence Context
- 엔티티를 영구 저장하는 환경
- EntityManager.persist(엔티티)를 하게 되면 엔티티를 영속화하여 영속성 컨텍스트에 저장을 하게 되며, 영속성 컨텍스트에 접근하기 위해서는 EntityManager를 통해야 한다.
- DB에 저장하게 되는 것은 그 이후의 얘기다.
- LifeCycle
- New
- 비영속 상태 - 영속성 컨텍스트와 전혀 관계가 없는 상태
- 예를 들면 Member m = new Meber(); 이렇게 객체만 생성한 상태
- 비영속 상태 - 영속성 컨텍스트와 전혀 관계가 없는 상태
- Managed
- 영속 상태 - 영속성 컨텍스트에 저장된 상태로 DB에 저장되진 않고, 트랜잭션 커밋 시점에 쿼리가 DB로~
- EntityManager
// 객체를 생성한 상태(비영속) Member member = new Member(); member.setId("member1"); member.setUsername("회원1"); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); // 객체를 저장한 상태(영속) em.persist(member)
- Detached
- 영속성 컨텍스트에 저장되었다가 분리된 상태
- em.detach(member);
- 영속성 컨텍스트에 저장되었다가 분리된 상태
- Removed
- 삭제되고, DB에 쿼리 날라감
- em.remove(member);
- 삭제되고, DB에 쿼리 날라감
- New
- 왜 PersistenceContext (영속성 컨텍스트)를 사용하는가!
- 1차 캐시
- 영속성 컨텍스트에는 내부에 1차 캐시가 존재하고, @Id로 선언한 필드의 값과 엔티티를 Key,Value 형태로 캐시에 저장하여, 해당 Key를 갖는 엔티티를 조회할 때 캐시에서 가져다 쓰게 되므로 DB를 갔다 오는 수가 줄어들게 된다.
- 예를 들어 ~.findById(123);을 하게 되면 123이라는 key로 조회된 entity를 1차 캐시에 저장하게 된다. 해당 스레드에서 다시 123의 key값을 가지는 entity를 불러오게 되면 db에서 불러오는게 아니라 1차 캐시에서 가져오게 된다.
- 동일성 보장
- 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공하므로 같은 엔티티를 여러번 조회해도 같은 레퍼런스가 되어 동일성이 보장된다.
- 쓰기 지연 SQL 저장소로 트랜잭션 지원
- 엔티티들은 1차 캐시에 저장되고, INSERT같은 쿼리는 쓰기 지연 SQL 저장소에 쌓아서 commit()될 때 DB에 동시에 쿼리들을 보낸다. (이것이 flush())다.
- Dirty Checking
- 개인적으로 이것이 과연 이점일까 싶지만.... (당연한거 아닌가 하는 부분이라 해야할 까)
- 영속성컨텍스트에 저장된 엔티티를 수정하면 (~.setName("홍길동)") commit() 혹은 flush()가 일어날 때 변경사항이 있음을 감지하고 DB에 UPDATE 쿼리를 보낸다.
- 1차 캐시
- Flush
- 영속성 컨텍스트의 변경내용을 DB에 반영하여 싱크를 맞추는 작업
- 커밋시, 영쓰기 지연 저장소에 쌓아놓은 쿼리들이 날라가 데이터베이스에 반영된다.
- 호출시점
- entityManager.flush()로 직접 호출
- 트랜잭션 커밋
- JPQL 쿼리 실행
- JPQL 실행 전에 무조건 flush()로 DB와의 싱크를 맞춘 다음에 JPQL 쿼리를 날리게끔 되어 있다.
- 옵션
- FlushModeType.COMMIT : 커밋할 때만
- FlushModeType.AUTO : 기본 설정 값으로 커밋이나 쿼리를 실행할 때
- 플러시를 했다고 영속성 컨텍스트를 비우는 것은 아니고, 변경 내용을 데이터베이스와 동기화 하는 것이다.
- Lazy Loading
- 연관관계(@OneToMany 등)를 맺고 있는 엔티티를 검색할 때 연관관계가 있는 엔티티는 프록시 데이터로 채우고, 그것이 실제로 사용될 때만 검색하게 하여 불필요한 쿼리를 실행하지 않게 도와주는 역할
- 기본적으로 EAGER보다는 LAZY 관계를 맺게 해주어 추후에 일어날 성능상의 문제를 방지하도록 하자.
- N+1 Problem
- 연관관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 개수만큼 연관관계의 조회 쿼리가 추가로 발생하는 문제
- Lazy냐 Eager냐 상관없이 발생하는 문제임...
- 왜 발생하느냐?
- jpaRepository에 정의된 메서드를 실행하면 JPQL이 싱행되게 된다. JPQL은 특정 SQL에 종속되지 않으면서 객체와 필드 이름을 가지고 쿼리를 하는데, 따라서 findAll()이란 메소드를 수행하게 되면 딱 그 엔티티에만 SELECT절을 날리게 되고, 연관관계인 엔티티는 별도로 호출하게 되는 것이다.
- 해결방법은?
- FetchJoin을 사용하면 되는데 문제는 그렇게 되면 LazyLoading으로 해놓은 것이 무의미하게 되는 것, 연관관계를 맺고 있는 것을 한번에 다 가져오는 것이기 때문. (그것도 outer join으로..)
- 현재 운영중인 서비스에서는 획기적인 대안을 찾지 못했기 때문에 쿼리마다 특성을 파악하고 필요에 따라 FetchJoin을 사용하고 있는 상황이다.
참고
https://ict-nroo.tistory.com/130 [개발자의 기록습관]
'JPA & DB' 카테고리의 다른 글
Hikari Option 정리 (0) | 2022.03.25 |
---|---|
JPA 트랜잭션 격리수준 (Isolation Level) 과 락 (Lock) (0) | 2022.01.10 |
Cascade 이해 및 orphanRemoval=true vs CascadeType.REMOVE (0) | 2020.05.15 |
[JPA] 복합키에서 equals () 및 hashCode () 구현하는 이유 (0) | 2020.03.06 |
[JPA] OneToMany & multi OneToOne (0) | 2020.03.06 |