04. 영속성 관리(2) - 영속성 컨텍스트의 이점
영속성 컨텍스트의 이점
이번에는 영속성 컨텍스트를 사용함으로써 얻을 수 있는 추가 이점에 대해서 알아봅시다.
1. 1차 캐시
기존의 JDBC에서 저장된 데이터를 읽으려고 합니다. 그러기 위해서는 당연히 SELECT 쿼리를 DB에 전송해야 합니다.
그럼 JPA에서는 어떨까요? 데이터를 읽는 코드를 짜 봅시다.
static void cacheTest() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
//비영속 상태
Member newMember = new Member(101L, "1차캐시테스트");
//영속 상태
em.persist(newMember);
System.out.println("===영속 상태 START===");
Member findMember = em.find(Member.class, 101L);
System.out.println("영속 상태 = " + findMember);
System.out.println("===영속 상태 END===");
//준영속
System.out.println("===준영속 상태 START===");
em.detach(newMember);
Member findMemberAfterDetach = em.find(Member.class, 101L);
System.out.println("준영속 상태 = " + findMemberAfterDetach);
System.out.println("===준영속 상태 END===");
transaction.commit();
em.close();
emf.close();
}
신기한 결과가 나오게 됩니다. 영속 상태일 때는 SELECT 쿼리가 전송되지 않습니다!
여기서 얻을 수 있는 정보는 다음과 같습니다.
1) 영속성 컨텍스트는 캐시와도 같은 역할을 한다.
영속성 컨텍스트에 들어있는 엔티티는 굳이 DB 레벨까지 가지 않고 영속성 컨텍스트에 있는 데이터를 사용합니다.
결국 영속성 컨텍스트는 내부에 캐시를 가지고 있다는 것을 알 수 있는데, 이를 1차 캐시라고 합니다.
내부에 Map <@Id로 매핑한 식별자, member인스턴스>가 존재하고 있다 생각하시면 됩니다.
영속 상태의 엔티티는 모두 이곳에 저장됩니다.
추가적으로 1차 캐시에서 엔티티를 찾고, 여기에 없으면 데이터베이스에서 조회하게 되는데, 이때 1차 캐시에 엔티티를 저장한 후 반환을 해 또 필요할 시에는 1차 캐시에서 가지고 올 수 있게 설계되어 있습니다.
드라마틱한 성능 이점을 기대하기 어렵지만 한 데이터를 여러 번 불러야 하는 복잡한 로직에서는 성능 이점을 볼 수 있습니다.
2) 영속성 컨텍스트에 들어 있다고, DB에 저장되는 것은 아니다.
- 영속성 컨텍스트에 있는 해당 엔티티를 제거하는 준영속 상태와 삭제에서는 찾고자 하는 엔티티가 영속성 컨텍스트에 없어 DB 레벨까지 내려가 SELECT 쿼리를 전송하지만, null이 반환됩니다.
- 말 그대로 persist( )는 DB에 아무 영향을 끼치지 못하는 것입니다. 그래서 영속성 컨텍스트와 DB를 동기화해줄 수단이 필요한데, 'flush( )'가 이 기능을 수행합니다!
PLUS. flush( )
영속성 컨텍스트의 변경내역을 데이터베이스에 반영하는 기능입니다.
플러시의 구동
- 변경 감지 동작
- 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송(등록, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시 하는 방법
1) 직접 호출 : em.flush()
'commit( )'전에 DB에 선 반영하고 싶을 때 강제 호출 용도로 쓰입니다. (보통 테스트에 많이 쓰인다.)
static void cacheTest() {
...
//비영속 상태
Member newMember = new Member(101L, "1차캐시테스트");
em.persist(newMember); //영속 상태
em.flush(); // DB 선반영
...
}
cacheTest( )에서 'em.flush( )'를 추가해봅시다.
flush( )로 인해 원래 null이었던 준영속 상태가 데이터를 잘 가져온 것을 알 수 있습니다.
2) 자동호출 : 트랜젝션 커밋 or JPQL 쿼리 실행
(1) 트랜젝션 커밋
DB 변경 내용을 SQL로 전달하지 않고 트랜잭션만 커밋하면 어떤 데이터도 DB에 반영되지 않는다.
꼭 플러시를 호출해서 영속성 컨텍스트의 변경 내용을 DB에 반영해야 함.
(2) JPQL 쿼리 실행
em.persist(member1);
em.persist(member2);
em.persist(member3);
//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
'member1' ~ 'member3'을 수정할 필요가 생겨 수정을 진행을 하고 쿼리를 전송을 했다 칩시다.
이런 경우에는 DB에 쿼리가 전송이 되기는 하지만, flush( )가 일어나지 않아 반영이 되지 않을 수 있습니다.
그래서 JPQL 전달 시 내부 동작으로 flush( )가 자동으로 수행됩니다.
2. 동일성 보장
같은 영속성 컨텍스트가 관리한다는 것을 조건으로 인스턴스 동일성을 보장합니다!
1차 캐시에 존재하는 인스턴스를 계속해서 내보내기 때문이죠. 확실히 하기 위해서 코드를 짜 봅시다.
static void equalGuarantee() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member newMember = new Member(2L, "member2");
em.persist(newMember);
em.flush();
System.out.println("=findMember1, 2 생성=");
Member findMember1 = em.find(Member.class, 2L);
Member findMember2 = em.find(Member.class, 2L);
System.out.println("findMember1.equal(findMember2)? : " + findMember1.equals(findMember2));
System.out.println("===영속성 컨텍스트 초기화===");
System.out.println("=findMember3, 4 생성=");
em.clear();
Member findMember3 = em.find(Member.class, 2L);
Member findMember4 = em.find(Member.class, 2L);
System.out.println("findMember1.equal(findMember3)? : " + findMember1.equals(findMember3));
System.out.println("findMember3.equal(findMember4)? : " + findMember3.equals(findMember4));
System.out.println("=findMember5 생성=");
Member findMember5 = em.find(Member.class, 2L);
System.out.println("findMember3.equal(findMember5)? : " + findMember3.equals(findMember5));
System.out.println("===영속성 컨텍스트 초기화===");
em.clear();
System.out.println("=findMember6 생성=");
Member findMember6 = em.find(Member.class, 2L);
System.out.println("findMember5.equal(findMember6)? = " + findMember5.equals(findMember6));
} catch (Exception e){
tx.rollback();
} finally {
em.close();
}
emf.close();
}
1차 캐시에 있는 같은 엔티티 인스턴스를 반환하기 때문에 동일성이 보장된다는 것을 알 수 있습니다.
3. 트랜잭션을 지원하는 쓰기 지연과 지연 로딩
(1) 쓰기 지연(Lazy Write)
영속 컨텍스트는 단순히 1차 캐시의 기능만 지원하는 것이 아니라, 쓰기 지연 역시 지원합니다!
1차 캐시에 엔티티를 저장하는 동시에, 쓰기 지연 SQL 저장소라는 버퍼에 'INSERT' 쿼리를 저장합니다.
그리고 commit을 하는 시점에 한꺼번에 DB에 전송해 처리를 하는 기능입니다.
이렇게 버퍼에 'INSERT' 쿼리를 계속해서 쌓아뒀다가...

최종 commit( )[=flush( )]이 일어나면 한 번에 쿼리를 보냅니다!
(2) 지연 로딩 (Lazy Loading)
쓰기를 한 번에 할 수 있다면, 읽어오는 것 역시 한번에 할 수 있을 것입니다.
실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법.
4. 변경 감지 (더티 체킹)
입력한 데이터를 수정하면 신기한 경험을 하게 됩니다.
static void dirtyChecking() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member findMember1 = em.find(Member.class, 1L);
findMember1.setName("newMember1");
// 트랜젝션 종료
tx.commit();
} catch (Exception e){
tx.rollback();
} finally {
em.close();
}
emf.close();
}
다음 코드는 찾은 데이터를 수정을 하는 코드입니다.
여기서 눈여겨볼 것은 수정만 했지 다시 DB에 업데이트하는 쿼리는 없다는 것입니다. 결과를 봅시다.
어디에서도 UPDATE 쿼리를 생성한 적이 없는데 JPA에서 자동으로 UPDATE 쿼리를 전송하는 모습입니다.
비밀은 영속 컨텍스트안에 있습니다.
엔티티를 영속성 컨텍스트에 최초로 보관(1차 캐시에 최초로 저장)할 때 전체적인 스냅샷을 찍습니다.
그 후 엔티티를 변경할 때마다 스탭 샷과 비교를 해 UPDATE 쿼리를 생성해 쓰기 지연 SQL 저장소 버퍼에 쌓아둡니다.
이러한 기능을 더티 체킹(변경 감지)라고 합니다.
주의사항
변경 감지의 update와 merge()에서의 update 정도의 차이를 유념해야 합니다.
- 변경감지 : 내가 원하는 파라미터만 바꿀 수 있음
- merge : 내가 원하든 원하지 않든 모든 파라미터가 바뀜.
참고
- JPA Reference : https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference
- 도서 : 자바 ORM 표준 JPA 프로그래밍