서론
JPA에서 가장 중요한 개념 중 하나가 영속성 컨텍스트이다.
영속성 컨텍스트를 학습하면서 JPA의 내부 동작 방식을 이해하며, JPA의 장점(영속성 컨텍스트 이점) 또한도 정리해보고자 한다.
영속성 컨텍스트란?
"엔티티를 영구 저장하는 환경" 이라는 뜻으로 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. 엔티티 매니저를 통해 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
위의 그림과 같이 EntityManager Factory가 고객에 요청마다 EntityManager을 생성한다. 이 EntityManager은 DB Connection을 사용하여 DB를 사용한다
em.persist(entity) //entity을 영속성 컨텍스트에 저장
엔티티의 생명주기
비영속(new/ transient): 영속성 컨텍스트와 관계가 없는 새로운 상태
Member member = new Member();
member.setId(1L)
member.setName("JPA")
// JPA와 관련이 없는 그저 객체만 생성된 상태
영속(managed): 영속성 컨텍스트에 관리되는 상태
DB에 저장되는 것이 아니고 영속성 컨텍스트에서 관리되는 것이다. 영속된다고 쿼리가 나가는 것이 아니며, commit을 해야 쿼리가 실행되게 된다.
//객체 생성(비영속)
Member member = new Member();
member.setId(1L)
member.setName("JPA")
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//영속
em.persist(member)
준영속(detached): 영속성 컨텍스트에 저장되어 있다가 분리된 상태
em.detach(entity)
: 특정 엔티티만 준영속상태로 바꿈em.clear()
: Entity Manager의 영속성 컨텍스트를 초기화함em.close()
: 영속성 컨텍스트 종료
//비영속
Member member = new Member();
member.setId(1L);
member.setName("JPA");
tx.commit();
//영속
em.persist(member);
//준영속, 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
tx.commit();
삭제(removed): 삭제된 상태
실제 DB 삭제를 요청한 상태
//비영속
Member member = new Member();
member.setId(1L);
member.setName("JPA");
tx.commit();
//영속
em.persist(member);
//준영속, 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
//삭제, 객체를 삭제한 상태
em.remove(member);
tx.commit();
영속성 컨텍스트의 장점
1차 캐시
- 엔티티에 저장한 후 조회하게 될 경우에는 바로 DB에서 찾지 않고 영속성 컨텍스트에 있는 1차 캐시에서 조회하게 된다.
- 1차 캐시에 없는 경우에는 DB에서 조회 후 1차 캐시에 저장하고 반환하게 된다.
- 하지만, EntityManager 자체는 트렌젝션 단위로 생명주기가 이루어지기 때문에 그렇게 뚜렷한 성능 개선은 보이지 않는다.
동일성 보장
- Java의 Collection 비교가 가능한 것 처럼 JPA의 동일성을 보장해준다.
- 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜젝션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.
- REPEATABLE READ: DB에서 데이터를 조회할 때, 하나의 데이터에 대해서 여러번 조회하면 값이 변경되지 않은 한 같은 데이터가 나오게 된다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a==b) // true
트렌젝션을 지원하는 쓰기 지연
try{
Member member1 = new Member(150L, "A");
Member member2 = new Member(160L, "B");
em.persist(member1);
em.persist(member2);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다
System.out.println("====================");
tx.commit();
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다
}catch (Exception e){
tx.rollback();
}finally {
em.close();
}
- 1차 캐시에 넣음과 동시에 쓰기 지연 SQL 저장소에 insert쿼리를 생성해서 넣는다.
- 마찬가지로 memberB도 쿼리를 생성해서 쓰기지연에 넣는다.
- transaction.commit하는 시점에서 쓰기지연 SQL 저장소에 쿼리가 flush되면서 db쿼리가 날려진다.
즉, 바로 쿼리가 보내지는 것이 아니라 트랜젝션 커밋 시점에 모아둔 쿼리가 나가는 것이다.
하이버네이트 옵션
<property name="hibernate.jdbc.batch_size" value="10"/>
해당 옵션값만큼 모았다가 DB에 쿼리를 날릴 수 있다.
버퍼링같은 기능
변경 감지
- 엔티티 수정, 변경 감지, 더티 체킹
- 저장하지 않았는데 업데이트 쿼리가 날라옴
//영속 엔티티 조회
Member member = em.find(Member.class, 150L);
//영속 엔티티 수정
member.setName("SUJIN");
//커밋할 때 수정된
tx.commit();
flush가 실행되면 1차캐시랑 스냅샷(최초 상태)를 비교하게 된다.
만약 Entity와 스냅샷이 다르다면 업데이트쿼리를 보내게 되며, DB에 쿼리를 보내 업데이트한다.
플러시
플러시란?
영속성 컨텍스트의 내용을 데이터베이스에 반영하여 맞추는 작업,
영속성 컨텍스트의 쿼리를 DB에 보내는 작업이다.
플러시 발생
- 변경 감지(더티 체킹)
- 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송
플러시 방법
- em.flush() 직접 호출
flush를 하게되면 1차캐시가 지워지는 것이 아닌, 영속성 컨텍스트에 있는 쓰기지연 SQL 저장소 변경감지가 이루어져 DB에 반영되는 과정이다.
Member member = new Member(200L, "member200")
em.persist(member);
em.flush();
// db에 즉시 반영됨
// 테스트할 때 주로 이용
- 트렌젝션 커밋 - 플러시 자동 호출
- JPQL 쿼리 실행 - 플러시 자동 호출만약 위에 영속성 컨텍스트에 넣은 것을 JPQL이 DB에 조회한다면 에러가 날 것이다. 이런 상황을 방지하기 위해 JPA는 JPQL이 실행되기 전에 모든 것을 플러시한다.
em.persist (memberA); em.persist (memberB);
//JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List members = query.getResultList();
플러시 모드 옵션
em.setFlushMode(FlushModeType.COMMIT)
FlushModeType.AUTO
: 커밋이나 쿼리를 실행할 때 플러시(기본 값)FlushModeType.COMMIT
: 커밋할 때만 플러시
결론
결론적으로 JPA는 영속성 컨텍스트를 활용함으로써 많은 장점을 생겼다. 하지만, 그렇기에 JPA가 편한것만은 아닌것 같다. 그 안에 메커니즘부터 차근차근 이해하고 다가가야 쉽고 정확하게 JPA를 사용하겠다고 생각했다.
영속성 컨텍스트는 JPA의 무엇보다도 중요한 부분이니 오늘 내용은 외울만큼 반복하고 머리에 새겨야겠다.
'코딩 > JPA' 카테고리의 다른 글
[JPA] JPA개념 및 실습 환경 셋팅 (0) | 2023.07.17 |
---|---|
[JPA] 영속성 전이, 고아객체(Cascade) (1) | 2023.01.07 |
[JPA] Soft Delete 적용하기 (0) | 2023.01.06 |