JPA 게시글은 대부분 인프런의 김영한님의 강의인 '자바 ORM 표준 JPA 프로그래밍' 기반으로 내용을 정리했습니다.
JPA 영속성 관리 - 영속성 컨텍스트
엔티티 매니저 팩토리와 엔티티 매니저
이 전 포스팅에서도 얘기했었는데 다시 짧게 말해보자면..
- EntityManagerFactory
- EntityManager를 생성
- EntityManager
- 내부적으로 DB 커넥션 풀을 사용해서 DB를 사용하게 된다.
영속성 컨텍스트
JPA를 이해하는데 가장 중요한 용어로, '엔티티를 영구 저장하는 환경'이라는 뜻이다. 이전 포스팅에서 DB에 데이터를 저장하기 위해 em.persist(객체) 코드를 사용했었는데 사실 DB에 바로 저장하는게 아니라 엔티티 매니저를 통해 엔티티를 영속성 컨텍스트라는 곳에 저장한다.
- 전에 작성했던 코드 같은 경우 J2SE 환경이라 EntityManager와 영속성 컨텍스트가 1:1 즉, EntityManager가 생성될 때 만들어지고 각 엔티티 매니저를 통해 영속성 컨텍스트에 접근할 수 있고, 관리할 수 있다.
- J2EE, 스프링의 경우에는 스프링 환경에서 JPA를 사용해보지 않았으므로 후술한다.
엔티티의 생명주기
- 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
- 영속(managed) : 영속성 컨텍스트에 관리되는 상태. em.persist()가 된 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) : 삭제된 상태
비영속
// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
JPA와 전혀 관계없는 일반적인 코드만 존재하면 비영속 상태이다.
영속
// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
// 객체를 저장한 상태(영속)
em.persist(member);
주의할 점은 아직 DB에 저장한 상태가 아니다. 쿼리가 날라가는 시점은 commit을 하는 시점이다.
EntityTransaction tx = em.getTransaction();
tx.commit();
준영속, 삭제
// 회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
// 객체를 삭제한 상태(삭제)
em.remove(member);
영속성 컨텍스트의 이점
- 1차 캐시
- 동일성(identity) 보장
- 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
- 변경 감지(Dirty Checking)
- 지연 로딩(Lazy Loading)
엔티티 조회, 1차 캐시
// 객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
// 엔티티 영속
em.persist(member);
위의 코드가 실행되면 영속성 컨텍스트의 내부에 아래 그림과 같이 저장된다.
추후 find를 통해 조회하는 명령이 들어오면 제일 먼저 DB에서 데이터를 찾는게 아니라, 영속성 컨텍스트 내 1차 캐시안에서 찾고 있으면 바로 반환해준다. 만약 1차 캐시 안에 찾는 데이터가 없으면 DB에서 조회 후 조회된 데이터를 1차 캐시에 저장하고 영속화 된 반환한다.
사실 1차 캐시가 그렇게 큰 도움은 안된다. 엔티티 매니저는 데이터베이스 트랜잭션 단위로 만들고 데이터베이스 트랜잭션이 종료될때 같이 종료된다. 동일한 트랜잭션 안에서만 공유되는 데이터이지 전체적, 즉 애플리케이션 범위에서 공유되는 캐시(이건 하이버네이트나 JPA에서 2차 캐시라 부르는 기능이 있다.)가 아니기 때문에 성능 향상에 그리 많은 도움을 주진 않는다.
동일성 보장
Member findMember1 = em.find(Member.class, 100L);
Member findMember2 = em.find(Member.class, 100L);
System.out.println(findMember1 == findMember2); //true
마치 자바 컬렉션에 있는 객체를 꺼냈을 때처럼 1차 캐시로 반복 가능한 읽기 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다. 어려운 말이니 그냥 쉽게 동일한 트랜잭션 안에서 동일한 객체는 동일하다고 판단한다고 이해하면 될 거 같다.
트랜잭션을 지원하는 쓰기 지연
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
- 쓰기 지연 SQL 저장소
- 영속성 컨텍스트는 1차 캐시 공간과 쓰기 지연 SQL 저장소를 가지고 있다. 쓰기 지연 SQL 저장소는 DB에 등록해야 될 엔티티 객체에 대한 INSERT 쿼리를 저장해둔다.
- transaction.commit();
커밋이 되면 쓰기 지연 SQL 저장소에 있는 INSERT SQL들이 flush가 되면서 DB에 넘어간다. (flush는 후술한다.)
이렇게 persist() 명령을 수행할 때마다 DB까지 다녀오는 게 아니라 transaction.commit()을 하기 전에는 쓰기 지연 SQL 저장소에 임시로 담아두고 commit() 명령이 실행되면 그제서야 모아둔 SQL문을 flush하는 걸 트랜잭션을 지원하는 쓰기 지연이라 한다.
변경 감지
JPA는 객체를 컬렉션 다루듯이 다룰 수 있도록하는 목적이있다. 자바에서 리스트를 사용할 때 안의 값을 변경한다고 다시 리스트에 값을 저장하는 로직을 사용하는가? 사용하지 않는다. 그래서 JPA도 데이터의 값을 변경해도 다시 저장하는 update와 관련된 코드를 작성하지 않아도 JPA가 알아서 잘 처리해준다. 물론 당연한 말이겠지만 변경 감지 대상은 모든 객체가 아닌 영속 상태 엔티티에 대해서면 변경 감지가 이루어진다.
트랜잭션을 커밋하면 플러시가 작동된다. 그리고 1차 캐시에서 객체마다 스냅샷이라는 걸 가지고 있는데 이 스냅샷에는 최초 시점의 상태를 저장한다. 그래서 플러시 후 현 상태와 스냅샷들을 비교해 만약 변경된 것이 일어날 경우 UPDATE SQL을 쓰기 지연 SQL 저장소에 저장하고 DB에 보낸 후 커밋한다.