3. 영속성 관리 - 내부 동작 방식
영속성 관리 - 내부 동작 방식
영속성 컨텍스트
영속성 컨텍스트 : "엔티티를 영구 저장하는 환경"이라는 뜻.
EntityManager.Persist(entity);
EntityManager를 통해서 DB에 저장하는 것이 아닌 영속성 컨텍스트에 객체를 영속화 시키는 개념이다. 영속성 컨텍스트는 논리적 개념으로 눈에 보이지 않는다. EntityManager를 사용하게 되면 객체 내부에 1:1로 영속성 컨텍스트가 존재하게 된다.
엔티티의 생명주기
- 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
- 영속(managed): 영속성 컨텍스트에 관리되는 상태
em.persist(member)
하여 영속성 컨텍스트에 객체가 영속 상태로 변환.
- 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed): 삭제된 상태
영속성 컨텍스트의 이점
- 1차 캐시
- 변수이름을 key로 Entity를 value로 캐시에 저장이 됨.
- 만약 검색하려는 객체가 캐시에 존재하면 DB까지 접근하지 않고 캐시에서 데이터를 반환할 수 있다는 이점이 있음.
- 객체가 캐시에 없을 때는 DB에서 조회하여 1차 캐시에 저장해두고 반환해줌.
- 단! EntityManager가 사용될 때 내부에서 생기는 것이므로 찰나의 순간에 생성되고 사라짐.
- 그렇기 때문에 성능적으로 도움이 되진 않으나 트랜잭션에 서비스가 복잡한 경우에는 도움이 될 수 있음.
- 동일성(identity) 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지(Dirty Checking)
- 지연 로딩(Lazy Loading)
1차 캐시 - 코드로 이해해보기
//영속성 컨텍스트 설명
Member member = new Member();
member.setId(100L);
member.setName("HelloJPA");
//지금까지는 비영속 상태
//EntityManager를 통해서 영속 상태로 변경
System.out.println("=== Before ===");
em.persist(member);
System.out.println("=== After ===");
Member findMember = em.find(Member.class, 101L);
System.out.println("findMember.getId() = " + findMember.getId());
System.out.println("findMember.getName() = " + findMember.getName());
EntityManager를 통해 Persist하는 것은 DB에 보내는 것이 아니라 영속성 컨텍스트에 객체를 올리는 개념임. 그렇기 때문에 Before와 After 사이의 persist에서 쿼리가 나가지 않음.
tx.commit()
단계에서 쿼리가 날라가게 됨.
또한, DB에 Insert문이 나가지 않았지만 EntityManager의 1차 캐시에 영속화되었으므로 값을 가져다가 쓸 수 있음.
//조회 테스트
Member findMember1 = em.find(Member.class, 101L);
Member findMember2 = em.find(Member.class, 101L);
DB에 저장된 같은 객체를 조회하게 되면 다음 사진처럼 쿼리는 한번만 나감. 첫번째로 나갔던 쿼리로 인해 영속성 컨텍스트에 이미 101L 아이디 값을 가진 Member 객체가 영속화되어 그 값을 사용할 수 있기 때문이다.
영속 엔티티의 동일성 보장 - 코드로 이해해보기
만약 findMember1과 findMember2의 값을 비교해보면 어떻게 될까?
System.out.println(findMember1 == findMember2);
1차 캐시에서 같은 객체를 꺼내오기 때문에 동일성을 보장해주게 됨.
-> 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공해준다.
트랜잭션을 지원하는 쓰기 지연 - 코드로 이해해보기
//쓰기지연 실습코드
Member member1 = new Member(150L, "A");
Member member2 = new Member(151L, "B");
em.persist(member1);
em.persist(member2);
System.out.println("-----------------");
// 트랜잭션 커밋.
tx.commit();
위 코드들에서 볼 수 있듯 tx.commit()
하기 전까지는 쿼리를 날리지 않고 쓰기를 지연했던 것을 확인할 수 있다. 물론, 예외의 경우로 쿼리를 중간에 보낼 수도 있다.
EntityManager를 통해 Persist 하게 되면 MemberA를 1차 캐시에도 저장하지만 쓰기 지연 SQL 저장소에도 관련된 쿼리를 저장해두게 된다. 이후에 MemberB를 또 Persist 하게 되면 Insert SQL 문을 SQL 저장소에 저장해둔다. 이렇게 되면 MemberA와 MemberB에 대한 Insert문이 바로 처리가 된 것이 아니라 함께 보관되어 있다가 트랜잭션이 커밋될 때 모든 쿼리가 한번에 flush 되면서 DB에 적용되게 된다.
- batch 옵션
- DB에 보낼 쿼리를 모아서 한번에 보낼 수 있도록 옵션을 제공한다.
<property name="default_batch_fetch_size" value="10"/>
- DB에 보낼 쿼리를 모아서 한번에 보낼 수 있도록 옵션을 제공한다.
변경 감지(Dirty Checking) - 코드로 이해해보기
//더티체킹 실습코드
Member member = em.find(Member.class, 150L);
member.setName("ZZZZZ");
//컬렉션 다루듯 사용하기 때문에 따로 persist를 할 필요가 없다
// em.persist(member); //X
//삭제
em.remove(member);
단순히 member 객체 내부의 값만 변경해도 업데이트 코드가 자동으로 나감.
- 1차 캐시
@Id Entity 스냅샷(최초 영속화 되었을 때의 값을 저장해둠) "memberA" memberA memberA 스냅샷 "memberB" memberB memberB 스냅샷
플러시
EntityManager에서 트랜잭션이 끝날 때(커밋될 때) SQL 저장소에 저장되어있는 SQL문들이 DB로 나가는 것을 플러시(flush)라고 한다.
영속성 컨텍스트틀 플러시 하는 방법
- em.flush() - 직접 호출(거의 할 일 없음)
- 트랜잭션 커밋 - 플러시 자동 호출
- JPQL 쿼리 실행 - 플러시 자동 호출
실습코드
//플러시 실습코드
Member member = new Member(200L, "200");
em.persist(member);
//flush: 트랜잭션이 끝나기 전 미리 보내고 싶을 때
em.flush();
System.out.println("-----------------");
// 트랜잭션 커밋.
tx.commit();
flush를 하면 트랜잭션을 기다리는 것이 아니라 바로 쿼리가 나가는 것을 확인할 수 있다.
flush를 해도 1차 캐시는 유지되고 쓰기 지연 SQL 저장소에 저장되어 있던 쿼리들이 DB로 가는 과정임.
플러시 모드 옵션
- FlushModeType.AUTO: 커밋이나 쿼리를 실행할 때 플러시(기본값)
- FlushModeType.COMMIT: 커밋할 때만 플러시
- Member를 하다가 Post를 쿼리해와야 할 때 굳이 플러시를 하고 싶지 않다면 이 옵션을 사용할수 도 있음
가급적 Auto 기본으로 사용하자
준영속 상태
영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)되는 것을 말한다. 분리되게 되면 영속성 컨텍스트가 제공하는 기능을 못사용한다.
준영속 상태로 만드는 방법.
- em.detach(entity): 특정 엔티티만 준영속 상태로 전환
- em.clear(): 영속성 컨텍스트를 완전히 초기화
- em.close(): 영속성 컨텍스트 종료
준영속 상태 - 코드로 이해해보기
//준영속 상태 실습
Member member = em.find(Member.class, 150L);
member.setName("AAAAA");
//준영속 상태로 변경. ZZZZZ가 AAAAA로 변경되지 않음 member를 컨텍스트가 관리하지 않기 때문.
em.detach(member);
//영속성 컨텍스트 아예 삭제
em.clear();
//영속성 컨텍스트 닫음
em.close();