영속성 컨텍스트
영속성 컨텍스트는 엔티티를 영구적으로 저장하는 저장소이다.
영속성 컨텍스트는 엔티티 매니저를 생성할 때 만들어지며, 엔티티 매니저를 통해서 영속성 컨텍스트에 접근하고 관리할 수 있습니다.
아래 그림과 같이 데이터베이스와 애플리케이션 사이에 뭔가 중간 계층이 있는 느낌이라고 이해하면 된다.
| 예를 들어 Member 테이블에 10개의 row가 저장되어 있을때 select * from member
와 같이 조회한다면 영속성 컨텍스트에 10개의 엔티티가 생성되고 관리되어 진다고 생각하면 된다.
영속성 컨텍스트의 동작방식
select * from member
와 같이 조회했을 때 영속성 컨텍스트의 동작방식을 간단하게 설명하자면 아래와 같다.
6개로 작성되어 있는 처리 과정을 코드 한 줄로 표현할 수 있다.
Member member = em.find(Memebr.class, 1);
- 조회할 데이터가 영속성 컨텍스트에 존재하는지 확인
- 데이터가 없으면 쿼리를 생성
- 쿼리를 DB에 전송
- 결과 값을 영속성컨텍스트가 전달 받음
- 전달 받은 데이터를 엔티티로 저장
- 엔티티 인스턴스를 리턴
영속성 컨텍스트의 특징
영속성 컨텍스트는 엔티티를 식별자 값(@Id 를 사용하여 테이블의 기본 키(PK)와 매핑한 값)으로 구분한다.
따라서 영속 상태가 되기 위해서는 반드시 식별자가 존재해야 한다.
영속성 컨텍스트가 제공해주는 기능
1차 캐시
영속성 컨텍스트는 내부에 1차 캐시를 가지고 있다.
캐시는 Map의 형태로 만들어지며 key는 id값, value는 해당 entity값이 들어있다.
1차 캐시 예시와 흐름
// 엔티티를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.serUsername("회원1");
// 엔티티를 영속화, 1차캐시에 저장됨
em.persist(member);
// 엔티티 조회, 1차캐시에서 조회됨
Member findMember = em.find(Member.class, "member1");
- Member라는 인스턴스를 생성했을때는 엔티티를 생성한 상태이기 때문에 비영속상태이다.
- em.persist(member) 코드를 실행하면 비영속상태였던 member가 영속성 컨텍스트(1차 캐시)에 저장된다.
- "member1"조회 요청 시 JPA는 영속성 컨텍스트의 1차 캐시에서 해당 PK값을 가진 Member객체를 찾아야한다.
- 1차 캐시에 존재한다면 그 값을 반환하고, 존재하지 않는다면 DB를 조회하여 값을 가져온다.
- DB를 조회하여 값을 가져온 경우라면, 가져온 값을 1차 캐시에 저장해둔다.
- 다음번에 "member1"을 조회하면, DB를 거치지 않고 1차 캐시에서 가져올 수 있게된다.
1차 캐시에 존재하지 않는 "member2" 조회할 경우의 그림
| 참고 : 엔티티 매니저는 보통 데이터베이스 트랜잭션 단위로 생성하고, 트랜잭션이 끝나는 시점에 종료가 된다. 엔티티 매니저가 종료되면 엔티티 매니저의 1차 캐시에 존재하는 데이터들도 모두 지워지는데 트랜잭션은 굉장히 짧은 찰나에만 존재하기 때문에 1차 캐시 사용이 큰 성능의 이점이 되지는 않는다고 한다.
동일성 보장
// id가 1L인 Member 엔티티 조회
Member a = em.find(Member.class, 1L);
Member b = em.find(Member.class, 1L);
System.out.println(a == b); //동일성 비교 true
JPA는 하나의 트랜잭션 안에 존재하는(= 1차 캐시 내에 존재하는) Id(식별자 값)가 같은 엔티티에 대해서 동일성을 보장해줍니다.
⇒ 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.
- 영속 엔티티와 비영속/준영속 엔티티의 동일성 보장 비교
Member memberA = em.find(Member.class, 1L);
em.clear(); // 영속성 컨텍스트 비우기
Member memberB = em.find(Member.class, 1L);
System.out.println(memberA == memberB); // false
memberA는 준영속상태이고, memberB는 영속상태이기 때문에 false의 결과가 나타난다.
Member memberA = em.find(Member.class, 1L);
Member memberB = em.find(Member.class, 1L);
em.clear();
System.out.println(memberA == memberB); // true
첫 번째 find를 통해 DB에서 Member를 조회한 후, 1차캐시에 등록한다.
두 번째 find는 1차 캐시에서 값을 확인했는데 존재한다. 따라서 1차 캐시에 존재하는 값을 가져온다.
그 이후 영속성 컨텍스트를 비우더라도, 이미 memberB는 1차 캐시에 존재하는 member의 참조주소를 가지고 왔기에 true가 출력이 된다.
트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
entity의 값을 변경하면 DB에 바로 업데이트 되지 않고 영속성 컨텍스트는 쓰기 지연 저장소에 저장한다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
- em.persist(memberA);를 하면 INSERT SQL 이 생성되어 쓰기 지연 저장소에 저장한다.
- 계속 변경이 일어나면 SQL을 생성하여 쓰기 지연 저장소에 저장했다가, 트랜잭션이 커밋되는 순간 한번에 DB에 전송한다.
hibernate.batch_size=10
해당 옵션을 사용하면 10개 모았다가 한번에 커밋하도록 지정할 수 있다.
변경 감지(Dirty Checking)
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(memberA) 이런 코드가 있어야 하지 않을까?
transaction.commit(); // [트랜잭션] 커밋
위 코드를 보면 update 코드가 존재하지 않지만 결과를 보면 업데이트 쿼리가 날라가는 것을 알 수 있다.
JPA는 1차 캐시에 들어있는 엔티티가 변경되면, 그 변경된 내용을 감지하여 트랜잭션 커밋 시점에 변경된 내용을 DB에 반영한다. 이 것을 더티체킹이라고 한다.
변경감지 원리
1차 캐시에는 @Id와 Entity 외에도 스냅샷이라는 컬럼이 존재한다.
엔티티가 1차 캐시에 저장될 때, 저장되는 시점의 상태를 스냅샷으로 만들어서 1차 캐시에 보관한다.
트랜잭션이 커밋되는 시점에 엔티티와 스냅샷을 비교하는데, 이때 만약 엔티티가 변경되었다면 당연히 스냅샷과 차이가 있을 것이고, JPA는 이를 감지하여 DB에 반영한다.
가장 중요한 것은 커밋되는 시점에 엔티티와 스냅샷을 비교한다는 것이다.
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // [트랜잭션] 시작
// 영속 엔티티 조회
Member memberA = em.find(Member.class, 1L); // 이 시점에 username="리리", age=22
memberA.setUsername("투투");
System.out.println(memberA.getUsername()); // 투투 출력
memberA.setUsername("리리");
System.out.println(memberA.getUsername()); // 리리 출력
transaction.commit(); // [트랜잭션] 커밋
위의 코드 처럼 커밋되기 전에 엔티티를 수정하여 사용하였다가 커밋되는 시점에 다시 원상복구 시키면 update 쿼리가 실행되지 않는다.
하나의 트랙잭션 내에서는 엔티티를 어떻게 수정하여 사용하는것에 상관없이, 트랜잭션이 커밋되는 시점의 엔티티 상태와 스냅샷을 비교한다는 것을 알고 있어야한다.
참고
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
https://lng1982.tistory.com/273
https://ttl-blog.tistory.com/108
'spring 🍀 > spring-data-jpa' 카테고리의 다른 글
JPA N+1 문제 해결과정 ( join fetch + CountQuery) (0) | 2023.11.23 |
---|---|
JPA 프록시와 지연로딩/즉시로딩 (1) | 2023.11.23 |
JPA의 Entity는 기본 생성자가 왜 반드시 필요할까? (1) | 2023.11.20 |
EntityManager와 EntityManagerFactory (0) | 2023.11.20 |
엔티티 생명주기 - 비영속, 영속, 준영속, 삭제 (0) | 2023.11.20 |