영속성 컨텍스트
JPA를 공부할 때 가장 중요한 2가지가 있다.
- 객채와 관계형 데이터베이스를 맵핑하는 것
- 영속성 컨텍스트를 이해하는 것
엔티티 매니저 팩토리와 엔티티 매니저
- 엔티티 매니저 팩토리를 통해서 고객의 요청이 올 때마다 EntityManager를 생성한다.
- EntityManager 매니저는 내부적으로 데이터베이스 커넥션을 통해서 DB를 접근한다
영속성 컨텍스트
- "엔티티를 영구 저장하는 환경"이라는 뜻
- EntityManager.persist(entity)
- 객체를 DB에 저장하는 거라고 배웠지만 실제로는 깊은 내용이 있다.
- 엔티티를 영속성 컨텍스트라는 곳에 저장하는 것이다.
엔티티 매니저? 영속성 컨텍스트?
- 엔티티 매니저를 생성하면 눈에 보이지 않는 영속성 컨텍스트에 담긴다.
엔티티의 생명주기
- 비영속(new/transient)
- 영속성컨텍스트와 전혀 관계가 없는 새로운 상태
//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
- 영속(managed)
- 영속성 컨텍스트에 관리되는 상태
//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
EntityManager em = emf.createEntityManager();
em.getTranscation().begin();
//객체를 저장한 상태(영속)
em.persist(member);
- 준영속(detached)
- 영속성 컨텍스트에 저장되었다가 분리된 상태
//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
- 삭제(removed)
- 삭제된 상태
//객체를 삭제한 상태(삭제)
em.remove(member);
영속성 컨텍스트의 이점
- 영속성 내부에는 1차 캐시가 존재한다.
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
엔티티 조회, 1차 캐시
//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
- 1차 캐시가 존재함
- key : DB에서 PK로 지정한 값
- value : 엔티티 객체 자체가 값이 된다.
- 현재 예시의 key : "member1", value : member
1) 1차 캐시에 값이 존재할 때
//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
- em.find로 조회 시 DB에서 조회하는 것이 아니라 영속성 컨텍스트의 1차 캐시의 해당하는 PK값으로 멤버 객체를 가져온다.
2) 1차 캐시에는 없고 DB에는 값이 존재할 때는 어떻게 진행이 될까?
//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//DB에서 조회
Member findMember = em.find(Member.class, "member2");
- find('member2')로 1차 캐시에서 조회를 한다. 결과는 없음
- DB에서 조회를 한다. 결과는 있음
- 조회된 member2의 값을 1차 캐시에 저장한다
- 1차 캐시에 저장된 member객체를 반환한다.
영속 엔티티의 동일성 보장
- 마치 자바 컬렉션에서 객체를 뽑아서 비교했을 때 참조값이 같은 것처럼 영속성 엔티티의 동일 성을 보장해준다.
- 이게 가능한 것이 1차 캐시가 존재하기 때문이다
- 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공해준다.
member a = em.find(Member.class, "member1");
member b = em.find(Member.class, "member1");
System.out.println(a==b); // 동일성 비교 true
엔티티 등록 시 트랜잭션을 지원하는 쓰기 지연
EntityManager em = emf.createEntityManager();
EntityTranscation transcation = em.getTranscation();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transcation.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 Insert sql을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 insert sql을 보낸다.
transaction.commit(); // 트랜잭션 커밋
내부적 로직
동작 순서
- em.persist(memberA); 를 하는 순간 1차 캐시에 키와 값을 저장한다.
- insert sql을 생성해서 쓰기 지연 Sql 저장소에 저장한다
- em.persist(memberB); 를 하는 순간 1차 캐시에 키와 값을 저장한다.
- insert sql을 생성해서 쓰기 지연 sql 저장소에 저장한다. 쓰기 지연 SQL 저장소에는 2개의 insert문이 저장되어 있다.
- 이후 transaction.commit() 함수를 실행시키는 순간 쓰기 지연 Sql 저장소에서 flush를 통해서 DB에 저장한 후 commit을 실행시킨다.
이렇게 사용하게 되면 어떤 이점이 생기는 것일까?
persistenc.xml에 추가적인 옵션을 줄 수 있다.
- 이는 JDBC 일괄 처리 옵션으로 커밋 직전까지 insert 쿼리를 버퍼에 모아서 한 번에 전송할 수 있다.
<property name="hibernate.jdbc.batch_size" value=10/>
엔티티 수정 변경 감지
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // 트랜잭션 시작
// 영속 엔티티 조회
Member memberA = em.find(member.class, "memberA");
// 영속 엔티티 데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);
//em.update(member) 이런 코드가 있어야 하지 않을까?
transaction.commit();
내부 로직
- em.find를 한 순간 1차 캐시에 값이 저장이 되는데, 이때 스냅샷에는 최초 가져온 값의 데이터가 저장이 된다.
- member.set을 통해서 객체의 값을 변경을 하고 transaction.commit을 하게 되면
- JPA의 내부 로직을 통해서 변경된 객체 값과 스냅숏의 값을 비교해서 다른 부분을 체크한 후
- update 쿼리를 쓰기 지연 SQL에 저장한다
- 이후 flush와 커밋을 통해서 DB의 데이터 값을 변경한다.
플러시
- 플러시는 영속성 컨텍스트를 비우지 않음
- 영속성 컨텍스트의 변경사항가 데이터 베이스를 맞추는 과정(동기화 과정)
- 트랜잭션이라는 작업 단위가 중요 -> 커밋 직전에만 동기화하면 됨
플러시 발생
- 데이터 베이스 트랜잭션이 발생될 때 플러시 실행
- 변경 감지
- 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시 하는 방법
- em.flush() - 직접 호출
- 트랜잭션 커밋 - 플러시 자동 호출
- JPQL 쿼리 시행 - 플러시 자동 호출
플러시를 하게 되면 1차 캐시는 유지되며, 오직 영속성 컨텍스트의 쓰기 지연 SQL 저장소의 쿼리문들이 데이터베이스에 반영이 되는 것이다.
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들이 조회가 될까? 조회는 된다. 이유는 JPQL이 실행되기 전에 flush를 해주기 때문이다.
- 이 동작이 없다고 생각하면, persist는 영속성 컨텍스트에 저장만 되어있고, 실제 DB에는 반영이 안 되기 때문이다.
플러시 모드 옵션
em.setFlushMode(FlushModeType.COMMIT);
- FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시 (기본값)
- FlushNodeType.COMMIT : 커밋할 때만 플러시
그렇다면 플러시 모드 옵션이 이유는 무엇일까?
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// 중간에 JPQL 실행
query = em.createQuery("select m from Order m", Order.class);
List<Member> members = query.getResultList();
- 위 예제를 보면 JPQL의 Select 테이블이 Order로 변경되었을 때, 우리는 굳이 플러시를 실행시킬 필요가 없는 것이다. 이때 정원 하면 플러시 모드를 변경하면 되는 것이다.
준영속 상태
준영속 상태란 영속상태의 엔티티가 영속성 콘텍스트에서 분리된 상태를 말한다.
- entityManager.detach(entity) : 특정 엔티티만 준영속 상태로 전환
- entityManager.clear() : 영속성 컨텍스트를 완전히 초기화
- entityManager.close() : 영속성 컨텍스트를 종료
결론
JPA의 내부 영속성 컨텍스트가 어떻게 동작되는지 알 수 있었고, 제일 놀라웠던 부분은 객체와 데이터베이스의 괴리가 점점 사라지고 객체지향적으로 작성하면 자동으로 DB에 C/R/U/D가 된다는 것이다. 앞으로 사용방법을 더 익히겠지만, JPA를 좀 더 확실하게 익히면 장점된다고 생각하게 되었다.
이 글은 인프런의
제목 : 자바 ORM 표준 JPA 프로그래밍 - 기본 편
강사 : 김영한 님의 동영상을 참조해 만들었습니다.
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
'Spring > 스프링 JPA' 카테고리의 다른 글
고급 맵핑 (0) | 2022.03.30 |
---|---|
다양한 연관관계 맵핑 (0) | 2022.03.29 |
연관관계 맵핑 기초 (0) | 2022.03.28 |
JPA 엔티티 매핑 (0) | 2022.03.25 |
JPA 시작하기 (0) | 2022.03.23 |