영속성 컨텍스트

짱J·2023년 4월 10일
0

Spring Boot

목록 보기
7/7
post-thumbnail

엔티티 매니저 팩토리와 엔티티 매니저

EntityManager Factory를 통해 요청이 올 때마다 EntityManager를 생성한다.

매니저는 내부적으로 커넥션 풀을 사용하여 DB에 접근한다.


영속성 컨텍스트

  • 엔티티를 영구 저장하는 환경
EntityManager.persist(entity);

엔티티를 영속화하는 코드이다.
엄밀히 말하면 DB에 엔티티를 저장하는 것이 아니라, 영속성 컨텍스트라는 곳에 저장하는 것이다.

영속성 컨텍스트는 눈에 보이지 않으며, EntityManager를 통해 접근한다.


엔티티의 생명주기

비영속 → 영속 → 준영속 → 삭제

비영속

  • 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

영속

  • 영속성 컨텍스트에 관리되는 상태
    • 💥 이 때 DB에 저장되는 것이 아니다. 💥
      (트랜잭션이 commit될 때 DB에 저장된다.)
// 위에 코드(비영속 상태)와 이어서
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

// 객체를 저장한 상태 (영속)
em.persist(member);

준영속

  • 영속성 컨텍스트에 저장되었다가 분리된 상태
em.detach(member);

삭제

  • 삭제된 상태
em.remove(member);

영속성 컨텍스트의 이점

1차 캐시

  • 엔티티 매니저 안에 1차 캐시가 존재
  • key가 PK, value가 객체 그 자체
Member findMember = em.find(Member.class, "member1");


DB가 아니라 1차 캐시에서 엔티티를 찾는다. 만약 1차 캐시에 존재하다면, 바로 가져온다.

Member findMember = em.find(Member.class, "member2");

만약 1차 캐시에 찾는 엔티티가 없다면, DB에서 조회하여 1차 캐시에 저장한 후 반환한다.
그렇기 때문에 쿼리 로그를 찍으면, select 쿼리가 날라가지 않는 것을 볼 수 있다.

이후 member2를 또 찾게 된다면 그 때는 1차 캐시에서 바로 가져오면 된다.

하지만 비즈니스가 끝나면 엔티티 매니저가 종료되므로(= 트랜잭션 하나에서 작동), 큰 이득을 얻을 수 없다 😞
비즈니스가 굉장히 복잡할 때는 쿼리를 줄일 수 있다.

영속 엔티티의 동일성(identity) 보장

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

System.out.println(a == b); // true

1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 DB가 아닌 애플리케이션 차원에서 제공한다.

  • 트랜잭션의 격리 수준에 대해서는 해당 포스트를 참고하자.

트랜잭션을 지원하는 쓰기 지연

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction()
transaction.begin();

em.persist(memberA);
em.persist(memberB);

transaction.commit();

persist() 함수가 호출될 때 바로 insert 쿼리가 날라가지 않는다.
커밋할 때까지 JPA가 쿼리를 모아뒀다가, 커밋이 됐을 때 한번에 쿼리를 DB로 보낸다.


영속성 컨텍스트에는 1차 캐시 말고, 쓰기 지연 SQL 저장소라는 것도 있다.
persist() 메소드가 호출될 때, 1차 캐시 저장과 함께 insert 쿼리가 쓰기 지연 SQL 저장소에 쌓인다.

이렇게 쌓인 쿼리는 트랜잭션이 커밋될 때 DB에 저장된다. (JPA에서는 이 과정을 flush라고 한다.)

변경 감지 (dirty checking)

Member memberA = em.find(Member.class, "memberA");

memberA.setUsername("hi");

// 엔티티를 다시 저장하는 코드가 필요 없다 !
// em.persist(memberA);

transaction.commit();

엔티티를 조회할 때, 엔티티와 스냅샷을 비교하여 달라졌으면 쓰기 지연 SQL 저장소에 update 쿼리가 저장되고 트랜잭션이 commit될 때 DB에 반영된다.


플러시 (flush)

: 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영

플러시는 영속성 컨텍스트를 비우는 것이 아니라, 변경 내용을 데이터베이스에 동기화하는 작업이라는 것을 꼭 알아두자.

앞서 살펴본 내용을 정리하면, 플러시가 발생될 때 아래 3가지 일이 일어난다.

  • 변경 감지
  • 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
  • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송

영속성 컨텍스트를 플러시하는 방법

  1. em.flush() - 직접 호출 (강제 호출)
  2. 트랜잭션 커밋 - 플러시 자동 호출
  3. JPQL 쿼리 실행 - 플러시 자동 호출

플러시 모드 옵션

  • FlushModeType.AUTO (기본값) - 커밋이나 쿼리를 실행할 때 플러시
  • FlushModeType.COMMIT - 커밋할 때만 플러시
    • 쿼리를 실행할 때(=JPQL 실행)는 플러시되지 않는다

되도록이면 옵션을 바꾸지 않고 사용하는 것을 추천한다.


준영속 상태

영속 상태에서 준영속 상태로 분리되면, 영속성 컨텍스트가 제공하는 기능을 사용하지 못하게 된다.

Member memberA = em.find(Member.class, "memberA");

memberA.setUsername("hi");
em.detach(memberA);

해당 코드를 실행하면 select 쿼리만 가고, update 쿼리는 가지 않는다. (준영속 상태가 되었기 때문!)

준영속 상태로 만드는 방법

// 준영속 상태로 만드는 방법
em.detach(entity); // 특정 엔티티만 준영속 상태로 전환
em.clear(); // 영속성 컨텍스트를 초기화
em.close(); // 영속성 컨텍스트를 종료

☘️ Reference

profile
[~2023.04] 블로그 이전했습니다 ㅎㅎ https://leeeeeyeon-dev.tistory.com/

0개의 댓글