데이터 영속성은 프로그램이 종료되더라도 사라지지 않는 데이터의 특성을 가진다. 또한 비휘발성 스토리지 정보를 유지하고 검색하는 수단이다.
JPA(Java persistence API)는 객체와 영속성 관계 맵핑 및 기능을 관리해주는 메커니즘을 제공한다.
persistence API뜻 그대로 데이터를 영속화해주는 API라고 볼 수 있다.
JPA는 영속성 컨텍스트를 이용해 엔티티를 영구 저장하는 환경을 제공한다.
EntityManager를 통해 영속성 컨텍스트를 접근할 수 있다.
DB와 상호작용을 위한 EntityManager를 생성하는데 사용되는 클래스다.
생성하는 비용이 커서 보통 1개 만들어 애플리케이션 전체에 공유한다.
META-INF/persistence.xml
<persistence-unit name="hello">
...
</persistence-unit>
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
요청이 들어오면 EntityManagerFactory에서 EntityManager를 생성하여 동작한다.
EntityManager em = emf.createEntityManager();
EntityManager 클래스 안에 영속성 컨텍스트가 들어있다.
주의 사항:
entityManagerFactory는 thread간에 자원을 공유해도 안전하다. 하지만 entityManager는 여러 thread가 동시에 접근하면 DB와 connection에 문제가 생길 수 있어 공유하면 안된다!
비영속(new/transient)
: 영속성 컨텍스트와 관계가 없는 새로운 상태
Member member = new Member();
영속(managed)
: 영속성 컨텍스트에 의해 관리되는 상태
em.persist(member)
준영속(deteched)
: 영속성 컨텍스트에 저장되었다가 분리된 상태
//영속성 컨텍스트의 member만 준영속
em.detach(member)
//영속성 컨텍스트의 모든 엔트리 준영속
em.clear()
//영속성 컨텍스트를 종료하면 모든 엔트리 준영속
em.close()
삭제(remove)
: 삭제된 상태
em.remove(member);
영속성 컨텍스트의 이점
영속성 컨테이너 내부에는 1차 캐시라는 저장소가 있다.
트랜젝션 내부에서 작업이 이루어지기 때문에 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다.
DB를 접근할 때 드는 비용은 네트워크를 이용하기 때문에 내부 메모리에 접근하는 시간보다 훨씬 비싸다.
1차 캐시를 쓰면 데이터를 빠르게 접근하는 이점이 있다.
데이터 조회 로직
- 찾고 싶은 엔트리를 1차 캐시를 통해 찾는다.
- 만약 엔트리가 없으면 DB로 이동한다.
- DB에서 찾은 엔트리를 1차 캐시에 저장한다.
- return한다.
Member member1 = em.find(Member.class,100L);
Member member2 = em.find(Member.class,100L);
member1과 member2는 같은 인스턴스로 동일성이 보장된다. 그 이유는 1차 캐시에 있다.
member1은 1차 캐시에 엔트리가 있다면 1차 캐시를 통해 엔트리를 얻는다. 만약 1차 캐시에 없다면 DB를 통해 접근해 엔트리를 얻고 1차 캐시에 얻은 엔트리가 저장된다.
member2는 1차 캐시로 접근해 member1과 같은 엔트리를 얻는다.
따라서 member1과 member2는 같은 인스턴스다.
em.persist(memberA);
em.persist(memberB);
persist만으로 DB에 SQL을 보내지 않는다.
보내야 할 SQL들은 영속성 컨텍스트 내부의 쓰기 지연 SQL저장소에 저장된다.
em.commit();
commit하는 시점에 flush를 한 뒤 트랜잭션을 종료한다.
em.flush()
flush는 쓰기 지연 SQL저장소에서 SQL을 DB로 보내는 역할을 한다.
flush한다고 1차 캐시가 flush된다는 것이 아니라 영속성 컨텍스트의 변경 내용을 DB에 동기화 하는 것이다.
flush 메서드 호출 3가지 방법
1. 직접 호출시 em.flush() 사용
2. 트랜잭션 commit 시
3. JPQL쿼리 사용 시
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
List<Member> members = em.createQuery("select m from Member m, Member.class);
memberA,B,C는 영속상태로 DB에는 없고 1차 캐시에만 있는 상태다.
만약 JPQL flush가 안되서 쓰기지연 SQL 저장소에 있는 insert SQL들이 들어가지 않는다면 DB에는 memberA,B,C는 저장이 안되어 있는 상태일 것이다.
하지만 JPQL은 SQL로 변환되어 DB의 엔티티를 조회하기 때문에 members에는 memberA,B,C는 들어있지 않을 것이다.
따라서 JPQL를 쓰는 시점에 flush를 해주어야 한다.
Member memberA = em.find(Member.class,"memberA");
memberA.setName("hi");
memberA의 name을 바꾸면 em.update(memberA);라는 코드가 필요할 것 같지만 필요 없다. 그 이유는 1차 캐시에 있다.
1차 캐시에는 엔티티 뿐만 아니라 스냅샷도 존재한다.
스냅샷은 처음 1차 캐시에 저장하는 순간의 엔티티가 저장된다.
memberA는 member table을 조회한 영속상태 엔티티 이므로 1차 캐시에 저장되어 있다.
name을 변경할 때 1차 캐시 엔트리도 변경 된다. 이때 스냅샷을 통해 처음 저장된 엔티티를 비교해 변경된 엔티티가 있으면 쓰기저장 SQL 저장소에 update query를 넣는다.
flush가 될 때 DB안의 엔트리 값이 변경된다.
<강의/책>
1. 자바 ORM 표준 JPA 프로그래밍 - 기본편
본 글은 김영한님의 강의와 책을 보고 작성한 글입니다.