해당 포스팅은 "자바 ORM 표준 JPA 프로그래밍 - 기본편" 을 기반으로 작성되었습니다.
강의와 동일한 내용이지만 좀 더 세부적으로 내용를 추가한 영속성 컨텍스트에 대한 내용은 아래 포스팅 참고.
영속성 컨텍스트
웹 어플리케이션이 구동하는 시점에 EntityManagerFactory 가 생성되며, 사용자의 요청마다 EntityManager를 생성하여 내부 커넥션 풀(Connection Pool) 을 사용하여 DB를 핸들링한다.
영속성 컨텍스트란?
영속성 컨텍스트는 눈에 보이지 않는 논리적인 개념을 뜻한다.
- 엔티티를 영구 저장하는 환경
- EntityManager.persist(entity);
→ 좀 더 풀어서 설명하면 DB에 저장한다기보다는 영속성 컨텍스트를 통해 엔티티를 영속화 한다는 의미
- 영속성 컨텍스트에 엔티티를 저장한다는 말 - 엔티티 매니저를 통해서 영속성 컨텍스트에 접근
엔티티의 생명주기
비영속(new/ transient)
- 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
영속(managed)
- 영속성 컨텍스트에 관리되는 상태
준영속(detached)
- 영속성 컨텍스트에 저장되었다가 분리된 상태
삭제(removed)
- 삭제된 상태
비영속
JPA에 관계없이 그냥 객체 생성만 된 상태
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
영속
영속성 컨텍스트에 관리되는 상태
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//객체를 저장한 상태(영속)
System.out.println("=== BEFORE ===");
em.persist(member);
System.out.println("=== AFTER ===");
영속상태가 된다고 쿼리가 날라가는게 아니다.
준영속, 삭제
//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
//객체를 삭제한 상태(삭제)
em.remove(member);
- 준영속은 영속성 컨텍스트에서 삭제하는 것
- 삭제는 실제로 DB에서 해당 ROW를 삭제하는 것.
영속성 컨텍스트의 이점
- 1차 캐시
- 동일성(identity) 보장
- 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
- 변경 감지(Dirty Checking)
- 지연 로딩(Lazy Loading)
1차 캐시
// 비영속
Member member = new Member();
member.setId(101L);
member.setName("HelloJPA");
// 영속
System.out.println("=== BEFORE ===");
em.persist(member); // 1차 캐시에 영속화
System.out.println("=== AFTER ===");
// 1차 캐시에서 조회
Member findMember = em.find(Member.class, 101L);
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.name = " + findMember.getName());
- 조회 시 영속 컨텍스트안에서 1차 캐시를 조회 후 해당 엔티티가 있을 경우 캐시를 조회 해 온다.
- 조회 시 영속 컨텍스트 안에서 1차 캐시를 조회 후 해당 엔티티가 없을 경우 데이터베이스에서 조회 해 온다.
- 엔티티 메니저는 데이터베이스 트랜잭션 내부에서 만들고 종료되기 때문에 하나의 비즈니스 로직이 종료될 경우 1차캐시는 다 사라지기 때문에 큰 도움이 되지는 않는다. (비즈니스 로직이 복잡해 질 수록 효과는 커진다.)
실행 결과를 보면 SELECT 쿼리가 날라가지 않고 바로 Member id, name이 출력된 것을 확인할 수 있다.
데이터베이스에서 조회
만약 1차 캐시에 데이터가 없는 경우 DB를 통해 데이터를 조회하고 1차 캐시에 저장한 엔티티를 반환한다.
Member findMember1 = em.find(Member.class, 101L);
Member findMember2 = em.find(Member.class, 101L);
결과를 보면 select 쿼리가 한번만 나가는 것을 확인할 수 있다.
이는 첫번때 조회 때 쿼리를 날려 DB에서 데이터를 조회해오고 두번째 조회 시 1차 캐시에 있는 엔티티를 반환하기 때문이다.
동일성(identity) 보장
Member findMember1 = em.find(Member.class, 101L);
Member findMember2 = em.find(Member.class, 101L);
System.out.println("result = " + (findMember1 == findMember2)); // true
마치 자바의 Collection에서 동일한 위치의 요소를 get() 하면 동일성이 보장되는 것처럼 JPA의 조회도 동일성이 보장된다.
이는 1차 캐시로 인해 가능한 것이다.
1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공
※ 참고
반복 가능한 읽기(REPEATABLE READ) 란?
특정 행을 조회 시 항상 같은 데이터를 응답하는 것을 보장하는 격리 수준이다. 즉, 트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회할 수 있다.
한번 조회한 데이터는 트랜젝션 내에서 다시 조회해도 같은 데이터가 나오는게 보장된다.
엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연
Member member1 = new Member(150L, "A");
Member member2 = new Member(160L, "B");
em.persist(member1);
em.persist(member2);
System.out.println("===========================");
tx.commit();
memberA와 memberrB는 둘 다 쓰기지연 SQL저장소에 저장되어있고 실제 DB에 적용은 안된 상태
commit() 시점에 쓰기지연 SQL에 저장된 쿼리들을 다 실행시켜서 DB에 적용한다
왜 쓰기지연을 사용하는가?
query를 매번 날리는 것은 성능상 이점이 없기 때문에 버퍼링 기능을 위해 제공된다.
<property name="hibernate.jdbc.batch_size" value="10"/> 를 통해 10개씩 쌓일때마다 적용하게 하는 기능 쿼리를 여러번 날리지 않고 최적화가 가능하다.
엔티티 수정 - 변경감지(Dirty Checking)
transaction.begin(); // [트랜잭션] 시작
//엔티티 조회
Member findMember = em.find(Member.class, "memberA");
//영속 엔티티 데이터 수정
findMember.setUsername("hi");
findMember.setAge(10);
// em.persist(member) // 이런 코드가 있어야 하지 않을까?
transaction.commit(); // [트랜잭션] 커밋
- 1차 캐시안에는 @Id, Entity , 스냅샷 이 있다. 여기서 스냅샷 은 최초로 영속성 컨텍스트(1차캐시)에 들어오는 순간 스냅샷을 찍어서 저장해둔다.
- JPA는 트랜잭션이 커밋(commit)되는 순간 엔티티와 스냅샷을 모두 비교한다.
- 변경된 것이 있을 경우 쓰기지연 SQL 저장소 에 업데이트 쿼리를 저장하고 수행하게 된다.
엔티티 삭제
//삭제 대상 엔티티 조회
Member member = em.find(Member.class, "memberA");
em.remove(member);//엔티티 삭제
플러시
flush란 persistence context의 변경 내용을 DB에 동기화 하는 작업을 말한다.
INSERT , UPDATE , DELETE 할 때 flush를 수행한다.
플러시 발생
flush는 다음의 동작으로 이루어진다.
- 변경 감지를 통해 수정된 엔티티를 찾는다.
- 수정된 엔티티가 있다면 UPDATE 쿼리를 persistence context에 있는 SQL 저장소에 등록한다.
- 쓰기 지연 SQL 저장소의 쿼리( 추가, 삭제, 수정 )를 모두 DB에 보냄으로써 동기화를 한다.
영속성 컨텍스트를 플러시 하는 방법
- em.flush() : 직접 호출
- 트랜잭션 커밋: 플러시 자동 호출
- JPQL 쿼리 실행: 플러시 자동 호출
JPQL 쿼리 실행 시 자동으로 호출되는 이유
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//중간에 JPQL 실행
query = em.createQuery("select m from Member m ", Member.class)
List<Member> members = query.getResultList();
JPQL쿼리를 실행하는 시점에서 위에 영속화 컨텍스트에 등록한 member들이 조회가 안되는 경우를 막기 위해 JPA에서는 JPQL 쿼리를 수행하기전에 flush를 실행해서 DB와 영속성 컨텍스트간에 동기화를 해준다.
※ 참고
플러시는 영속성 컨텍스트를 비우지 않는다.
영속성 컨텍스트의 변경내용을 데이터베이스에 동기화하기 위한 작업이다
즉, 트랜잭션이라는 작업 단위가 중요하다. => 커밋 직전에만 동기화를 하면 된다.
플러시 모드 옵션
em.setFlushMode(FlushModeType.COMMIT)
- FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시 (기본값)
- FlushModeType.COMMIT : 커밋할 때만 플러시
준영속 상태
- 영속 → 준영속
- 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
- 영속성 컨텍스트가 제공하는 기능을 사용 못한다.
- 변경감지, 1차캐시 등등...
준영속 상태로 만드는 방법
- em.detach(entity)
→ 특정 엔티티만 준영속 상태로 전환 - em.clear()
→ 영속성 컨텍스트를 완전히 초기화 - em.close()
→ 영속성 컨텍스트를 종료
em.detach(entity)
// 영속 상태
Member member = em.find(Member.class, 150L);
member.setName("AAAA"); // Dirty Checking
// 준영속 상태
em.detach(member);
System.out.println("===========================");
tx.commit(); // Member는 준영속 상태이므로 영속성 컨텍스트가 관리하지 않게 된다.
실행 결과를 보면 update 쿼리가 날라가지 않는 것을 확인할 수 있다.
em.clear()
// 영속 상태
Member member = em.find(Member.class, 150L);
member.setName("AAAA");
em.clear();
System.out.println("===========================");
Member member2 = em.find(Member.class, 150L);
tx.commit();
중간에 있는 em.clear() 로 인해 영속성 컨텍스트가 비워지므로 select 쿼리가 다시 날라가는 것을 확인할 수 있다.