영속성 컨텍스트(Entity Cache)

이종윤·2022년 2월 11일
0

Spring JPA

목록 보기
14/23

🤔 영속성 컨텍스트는 일종의 JPA 컨테이너 안에서 동작하는 entity의 맥락을 관리하는 것이다. 이 안에서 entity는 생성되고 지워지고 조회된다. 그 context 안에서 가장 중요한 역활을 하는 것이 EntityManager 객체이다. 이 역활을 알아보고, EntityManager가 entity를 처리하는 과정에서 Cache를 사용하는데 어떻게 사용하는지 알아보자.

EntityManager

  • EntityManager는 JPA에서 정의하고 있는 Interface 이다. persist merge remove find 등등 정의되어있다. 그리고 구현체를 빈으로 등록하고 있기 때문에 Autowire를 이용해 사용할수 있다.
  • 쿼리메서드나, simplejparepository 등 내부적인 실제 동작은 entityManager를 통하여 실행되기 때문에 SpringDataJpa에서 제공하지 않는 기능을 사용하거나, 성능문제가 있어 별도로 커스터 마이징을 해야하면 EntityManager를 받아서 사용하면 된다.
@SpringBootTest
public class EntityManagerTest {

    @Autowired
    private EntityManager entityManager;

    @Test
    void entityManagerTest() {
        System.out.println(entityManager.createQuery("select u from User u").getResultList()); // entityManager에서 직접 쿼리를 만들서 사용
        // userRepository.findAll(); 이거와 같다.
    }
  • 위처럼 커스텀쿼리를 만들 수 있지만, 이번 강의에서는 EntityManager의 Cache를 공부할꺼니까 넘어가자.

Entity 캐시 알아보자

  • EntityManager에서 사용하는 Entity캐시는 뭘까? 영속성 컨텍스트 내에서 엔티티들을 관리하고 있는 EntityManager에서는 Cache를 가지고있는제 실제로 우리가 save메서드를 실행시키는 시점에 DB에 반영되는게 아니다. 즉, 우리가 사용하는 영속성 컨텍스트로 실제 DB사이에서 Data 갭이 일어난다는 뜻이다.
    @Test
    void cacheFindTest() {
        System.out.println(userRepository.findByEmail("martin@fast.com"));
        System.out.println(userRepository.findByEmail("martin@fast.com"));
        System.out.println(userRepository.findByEmail("martin@fast.com"));
        }
  • 위 코드를 Test 하자
    select
        user0_.id as id1_7_,
        user0_.created_at as created_2_7_,
        user0_.updated_at as updated_3_7_,
        user0_.email as email4_7_,
        user0_.gender as gender5_7_,
        user0_.name as name6_7_ 
    from
        user user0_ 
    where
        user0_.email=?
  • 위 쿼리를 3번 된다. 이번에는 Transactional어노테이션을 걸고 findById를 실행 해 보자.
@SpringBootTest
@Transactional
public class EntityManagerTest {
    @Test
    void cacheFindTest() {
        System.out.println(userRepository.findById(2L).get());
        System.out.println(userRepository.findById(2L).get());
        System.out.println(userRepository.findById(2L).get());
   }
    select
		...
        ...
    where
        user0_.id=?
User(super=BaseEntity(createdAt=2022-02-11T16:44, updatedAt=2022-02-11T16:44), id=2, name=demis, email=demis@fast.com, gender=null)
User(super=BaseEntity(createdAt=2022-02-11T16:44, updatedAt=2022-02-11T16:44), id=2, name=demis, email=demis@fast.com, gender=null)
User(super=BaseEntity(createdAt=2022-02-11T16:44, updatedAt=2022-02-11T16:44), id=2, name=demis, email=demis@fast.com, gender=null)
  • 위와같이 select는 한번 실행됬는데 결과는 3개 나오는걸 확인할수 있다. 영속성 컨텍스트에서 존재하는 캐시가 직접 처리한 것이다. 진짜 DB쿼리를 조회하지 않고 말이다.
  • 우리가 따로 캐시 설정을 하지 않았지만, 영속성 컨텍스트내에서 자동으로 엔티티에 대한 캐쉬처리하는 것이 JPA의 1차 Cache처리라고 한다.
  • 그렇다면 findById과 findByEmail에서 1차 Cache가 적용되고, 되지 않는것의 차이가 뭘까?
    - 1차 캐시는 Map 형태로 만들어 지고 Key는 Id값 value는 해당 엔티티가 들어있다.
    • Id로 조회하면 먼저 영속성 컨텍스트내에 존재하는 1차 캐시에 조회해 보고 있으면 DB조회없이 바로 값을 리터해 준다.
    • 값이 없으면 실제 쿼리로 DB조회해서 1차캐시에 저장하고 리턴해준다.
  • 1차 캐시를 사용함에 따라서 기본적인 JPA 조회 성능이 올라간다. 개발자가 직접 ID값을 사용해서 조회하는 경우는 드물다. findById(2L) 같은거 말이다.
  • JPA 특성상 ID값을 이용한 조회가 빈번하게 일어난다. update나 delete같은거 말이다. 그래서 해당 로직의 성능저하가 일어난다. 이때 1차 캐시를 활용해서 성능저하를 줄이기 위한 대책이 된다.
    @Test
    void cacheFindTest() {
        userRepository.deleteById(1L);
    }
Hibernate: 
    select
    	...
        ...
    from
        user user0_ 
    left outer join
        user_history userhistor1_ 
            on user0_.id=userhistor1_.user_id 
    where
        user0_.id=?
Hibernate: 
    update
        review 
    set
        user_id=null 
    where
        user_id=?
Hibernate: 
    delete 
    from
        user 
    where
        id=?
  • 위 처럼 JPA내부적으로 ID에 대한 조회가 일어날때가 많아서 하나의 트랜젝션을 실행할 경우 1차 캐시를 사용하므로써 성능저하를 방지한다.
@Transactional
public class EntityManagerTest {

    @Test
    void cacheFindTest2() {
        User user = userRepository.findById(1L).get();
        user.setName("marrrrrrrtin");
        userRepository.save(user);

        System.out.println("---------------------");

        user.setEmail("marrrrrrtin@fast.com");
        userRepository.save(user);

    }
  • 위 처럼 update를 2번 save를 2번 하여도 영속성 컨텍스트의 캐시가 가지고 있다가 merge를 해서 한번에 적용한다. 만약 @Transactional이 걸려있으면 최종 merge를 한 Data와 DB랑 차이가 없어서 쿼리를 호출하지 않을것이다.(이게 맞는지 모르겠네....)
  • 영속성 컨텍스트의 cache를 잘 이해하게 되면 이전에 배운 flush의 역활도 이해할수 있다. flush는 모여있는걸 비워낸다는 뜻이다. 즉, cache에 있는 쿼리를 DB에 강제로 적용 시키겠다는 거다.
  • 그렇다하더라고 flush를 남발하면 안된다. cache의 역활을 무효화 시키는 것이기 때문에 성능 저하가 일어날수 있다.
    - 위 쿼리의 save 밑에 "userRepository.flush();"를 각각 입력하면 insert나 update할때 history에 저장하는 쿼리는 2번 호출하게 되는것 처럼 말이다.
  • cache진짜 어렵다. 내가 save할려고 save했는데 save를 안해... 그럼 영속성 컨텍스트와 DB가 동기화 되는 시점은 언제일까?
    - 첫 번째로는 flush()메서드가 호출될때. 동기화 된다.
    • 두 번째로는 트랜젝션이 끝나서 commit 될때이다.
    • 마지막으로 id값이 아닌 JPQL 쿼리가 실행될때 AutoFlush가 발생한다. 복잡한 쿼리가 일어나면 Autoflush가 된다고 생각하면된다.
profile
OK가자

0개의 댓글