영속성 컨텍스트
영속성 컨텍스트는 JPA를 사용함에 있어서 가장 중요한 요소이다.
영속성 컨텍스트 (persistence-context) 는 엔티티를 영구 저장하는 환경이라는 뜻이며, 눈으로는 볼 수 없는 논리적인 개념이다. 앞서 생성한 엔티티 매니저를 통해서 영속성 컨텍스트에 접근한다.
2023.12.13 - [JPA] - [JPA] 엔티티 매니저 팩토리, 엔티티 매니저
하나의 엔티티 매니저가 하나의 영속성 컨텍스트와 매핑되지만 spring 프레임워크와 같은 컨테이너 환경에서는 여러개의 엔티티 매니저가 하나의 영속성 컨텍스트와 매핑된다.
영속성 컨텍스트와 관련하여 엔티티의 생명주기는 4가지가 있다.
엔티티의 생명주기 1 - 비영속 (new/transient)
비영속 상태란 단지 엔티티를 생성한 상태를 의미한다. 영속성 컨텍스트에는 해당 엔티티에 대한 정보가 없는 단계이다.
// 객체 생성 (비영속)
Member member = new Member();
member.setId(1L);
member.setName("Hello");
엔티티의 생명주기 2 - 영속 (managed)
영속 상태란 영속성 컨텍스트에 저장되며, 영속성 컨텍스트에 의해 관리되는 상태를 의미한다. 이 상태는 실제 DB에 저장되는 것이 아니라 단지 영속성 컨텍스트 내의 1차 캐시에 저장되는 것이다. 이후 트랜잭션을 커밋하는 순간 DB에 쿼리가 날라가서 실제 DB에 저장되는 것이다.
// 객체 생성 (비영속)
Member member = new Member();
member.setId(1L);
member.setName("Hello");
// 객체 저장 (영속)
em.persist(member);
엔티티의 생명주기 3 - 준영속 (detached)
준영속 상태는 영속 상태였던 엔티티가 영속성 컨텍스트에서 분리된 상태를 의미한다. 이 상태에서의 엔티티는 영속성 컨텍스트와는 아무런 관계도 없어진다.
// 영속성 컨텍스트에서 분리 (준영속)
em.detach(member);
엔티티의 생명주기 4 - 삭제 (removed)
삭제 상태는 실제 영구저장한 DB에서 해당 엔티티의 삭제를 요청한 상태이다. 따라서 엔티티는 DB와 영속성 컨텍스트에 모두 남아있지 않게 된다.
// (삭제)
em.remove(member);
영속성 컨텍스트 구성
- 1차 캐시 : 영속성 컨텍스트 내의 1차 캐시에 key-value 형태로 엔티티를 저장함 (key : PK값, value : 엔티티)
- 쓰기 지연 SQL 저장소 : 트랜잭션이 커밋되는 순간 DB에 변경 내용을 반영하기 위해 데이터 변경에 해당하는 쿼리를 모아놓는 저장소
- 스냅샷 : (변경감지와 관련) 값을 읽어온 시점의 상태를 저장해 놓은 것
영속성 컨텍스트 이점
여기서 DB에 전송되는 쿼리를 확인하기 위해 아래 게시글과 같이 설정하였다.
1. 1차 캐시와 엔티티 조회
앞서 언급하였듯 트랜잭션이 커밋되기 이전에는 1차 캐시에 엔티티가 저장된다. 따라서 select 문이 DB에 날아가지 않고 1차 캐시에 존재하는 엔티티를 먼저 조회한다.
Member member = new Member(1L, "nameA");
em.persist(member); // 1차 캐시에 저장됨
// 1차 캐시에 저장된 엔티티를 조회함. 따라서 DB에 쿼리 날라가지 않음
Member findMember = em.find(Member.class, 1L);
System.out.println("==========");
// 트랜잭션 커밋
tx.commit();
DB에 전송되는 쿼리
- em.persist 에 해당하는 insert 쿼리 전송 X
- em.find에 해당하는 select 쿼리 전송 X
- 확인을 위한 "==========" 출력 (트랜잭션 커밋 직전)
- 트랜잭션 커밋 이후 DB에 insert 쿼리 전송
그러나 1차 캐시에 저장되어 있지 않은 엔티티를 조회할 때는 물론 DB에 쿼리를 보내서 조회한다. 이때, 조회한 엔티티는 1차 캐시에 저장하게 된다. 따라서 같은 트랜잭션 내에서 다시 해당 엔티티를 조회할 때는 DB에 접근하지 않고 1차 캐시에서 조회하는 것이다.
// 1차 캐시에 존재하지 않는 엔티티를 조회할 때 DB에 쿼리 전송됨
System.out.println("Member1 찾을 때!");
Member findMember1 = em.find(Member.class, 100L);
// findMember1 은 1차 캐시에 저장됨
// 1차 캐시에 저장된 엔티티이므로 DB에 접근하지 않고 1차 캐시에서 바로 조회
System.out.println("Member2 찾을 때!");
Member findMember2 = em.find(Member.class, 100L);
DB에 전송되는 쿼리
- Member1 찾을 때는 select 쿼리가 DB에 전송
- Member2 찾을 때는 select 쿼리가 전송X
2. 영속 엔티티의 동일성 보장
1차 캐시에서 바로 엔티티를 조회하기 때문에 식별자 (PK)가 동일하다면 조회를 할 때 마다 매번 같은 인스턴스에 접근한다.
// 영속 상태인 동일 엔티티를 2번 조회
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 1L);
// 조회한 엔티티 비교
System.out.println( findMember1 == findMember2 ); // true 출력
3. 트랜잭션을 지원하는 쓰기 지연과 엔티티 등록
em.persist(엔티티); 를 통해서 엔티티를 영속성 컨텍스트에 저장한다. 이 때는 실제 DB에 변경사항이 적용되는 것이 아니다. 즉, DB에 insert 쿼리를 날리는 것이 아니다.
em.persist를 실행하면 영속성 컨텍스트 안의 1차 캐시에 해당 엔티티가 저장된다. 이 때, 영속성 컨텍스트 내의 쓰기 지연 SQL 저장소에 insert 쿼리가 저장된다. 이후, 데이터 변경이 모두 끝나고 트랜잭션 커밋이 직전에 플러시가 발생하고 쓰기 지연 SQL 저장소 내의 쿼리가 DB로 날라가는 것이다.
플러시란?
영속성 컨텍스트의 변경 내용을 실제로 DB에 반영하는 것을 말한다.
플러시는 영속성 컨텍스트를 비우는 작업을 뜻하는게 아니다! 단지 영속성 컨텍스트의 변경내용을 DB에 동기화 시키는 것이다!
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
// 트랜잭션 시작
tx.begin();
Member memberA = new Member(50L, "nameA");
Member memberB = new Member(51L, "nameB");
em.persist(memberA);
em.persist(memberB);
// ====== 여기까지는 DB에 쿼리를 날리지 않는다. ====== //
System.out.println("==========");
// 트랜잭션 커밋
// 커밋하는 순간 DB에 insert 쿼리를 보낸다.
tx.commit();
DB에 전송되는 쿼리
- memberA에 대한 insert 쿼리 전송 X
- memberB에 대한 insert 쿼리 전송 X
- 트랜잭션 커밋 직전에 확인을 위한 "==========" 출력
- 트랜잭션 커밋 이후 DB에 memberA와 memberB에 대한 insert 쿼리 2개 모아서 전송
4. 변경 감지 (dirty-checking) 와 엔티티 수정
영속 엔티티의 데이터 변경이 일어나면 다음과 같은 과정을 거친다.
- 플러시가 실행되면 1차 캐시 내부의 엔티티와 스냅샷을 비교
- 스냅샷과 다른 엔티티에 해당하는 update 쿼리를 쓰기 지연 SQL 저장소에 생성
- 트랜잭션 커밋이 일어나기 직전에 플러시가 발생하여 쓰기 지연 SQL 저장소에 생성된 update 쿼리를 DB에 전송
- 트랜잭션 커밋이 일어나고 DB의 데이터가 변경
즉, 영속 엔티티의 데이터를 변경하고 나면 em.update()와 같이 update 쿼리를 날려줄 필요가 없다!
// 1차 캐시에 존재하는 엔티티 (영속 엔티티) 조회
Member findMember = em.find(Member.class, "memberA");
// 데이터 변경
findMember.setName("HelloJPA");
// em.update(member); => 이런 코드는 존재하지 않아도 된다!
// 트랜잭션 커밋 : 자동으로 update 쿼리가 db에 반영됨
tx.commit();
5. 엔티티 삭제
// 삭제할 엔티티 조회
Member findMember = em.find(Member.class, "memberA");
// 엔티티 삭제
em.remove(findMember);
'Spring Boot > JPA' 카테고리의 다른 글
[JPA] 연관관계 매핑 - 상속관계 매핑, @MappedSuperclass (1) | 2023.12.18 |
---|---|
[JPA] 연관관계 매핑 - 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) (0) | 2023.12.18 |
[JPA] 연관관계 매핑 - 단방향 매핑, 양방향 매핑 (1) | 2023.12.18 |
[JPA] 엔티티 매핑 관련 annotation 정리 (0) | 2023.12.14 |
[JPA] 엔티티 매니저 팩토리, 엔티티 매니저 (0) | 2023.12.13 |