사수님께서 Persistence Context에 관한 좋은 내용이 담긴 글을 공유해주셨다. 

https://msolo021015.medium.com/jpa-persistence-context-deep-dive-2f36f9bd6214

 

JPA Persistence Context Deep Dive

최근 저는 새로운 프로젝트를 진행하면서, JPA를 도입하였습니다.

msolo021015.medium.com

 

@Modifying 을 사용중인 나에게 꽤나 충격적이었음..

findById를 해도 update된 데이터가 아니길래 정말 한참을 헤맸었는데...

역시 개념에 대한 정확한 이해 없이 개발을 하면 이렇게 되나보다.

 

JPA를 공부하겠다고 산 두꺼운 서적을 다시 한 번 제대로 살펴봐야겠다.

영속성 컨텍스트를 정확하게 이해하지 않고 그 누가 JPA를 사용한다고 하랴..

 

그래서 다시 한번 중요 개념을 정리하고 넘어가보자 한다.

수없이 봤던 영속성 컨텍스트의 LifeCycle

  • 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);

 

  • 왜 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 쿼리를 보낸다.

 

  • 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 [개발자의 기록습관]

+ Recent posts