App과 DB 사이에 있는 가상의 DB라고 생각하는 것이 직관적인 것 같다.
EntityManager Factory가 스레드마다 DB에 접근 시 EntityManager를 생성한다.
EntityManager가 영속성 컨텍스트를 관리한다.
목차
- 1차 캐시
- 더티 체킹
- 쓰기 지연
- 동일성 보장
- 영속성 컨텍스트의 4가지 상태
- transaction
영속성 콘텍스트의 기능
1차 캐시
Map의 형태로 Key는 id, Value는 Entity
1. [1차 캐시]에 조회하고자 하는 id가 있는지 확인한다.
(persist 이후 1차 캐시에 있다면 조회 쿼리가 동작하지 않고 찾아온다.)
2. 1차 캐시에 없다면 [DB]에 있는지 확인한다. (DB select 쿼리가 동작한다.)
1차 캐시를 먼저 확인하고, DB에 접근하는 방식이다. 캐시를 재사용하므로 성능이 향상된다.
Dirty Checking
상태 변화 감지
영속성 콘텍스트의 최초 조회한 상태(스냅샷)가 변경되면 트랜잭션 종료 시점에서 자동으로 해당 Entity를 update 한다.
persist로 Entity의 등록 상태가 아니라 조회를 한 상태여야 한다.
조회한 Entity를 변경하면 detach Entity가 생성 되는데 detach Entity를 merge -> update한다. (자세한 내용은 아래에)
트랜잭션을 이용한 쓰기 지연
영속성 컨텍스트에서 발생하는 sql쿼리를 모아놨다가 트랜잰션이 종료될 때 모든 sql쿼리를 반영한다.
@Transactional 안에 save()가 있다면, 즉시 반영이 아니라 해당 메서드가 종료될 때 반영 (@Transactional이 상위이므로)
영속성 콘텍스트 4가지 상태
New 상태
객체를 생성한 상태, 객체를 생성했다고 해서 Managed가 아니다. (persist필요)
영속성 콘텍스트에 Entity가 등록되지 않은 상태
Manage 상태
영속성 콘텍스트에 Entity가 등록되어 관리되고 있는 상태
Remove 상태
영속성 콘텍스트에 Entity가 등록되어 관리되고 있다가 삭제된 상태
Detach 상태
영속성 콘텍스트에서 관리되지 않도록 분리한다. (1차 캐시, Dirty Checking이 동작되지 않는다.)
EntityManager에서만 Detach 상태를 만들 수 있고, JpaRepository를 이용해서는 불가능하다.
(JPA에서는 굳이 직접 Detach 상태로 만드는 것이 의미가 없다고 생각했는지)
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// 기존 엔티티를 가져와서 Detached 상태로 변경
MyEntity detachedEntity = entityManager.find(MyEntity.class, entityId);
entityManager.detach(detachedEntity);
// Detached 상태의 엔티티 수정
detachedEntity.setName("Updated Name");
// 수정된 내용이 반영된 새로운 엔티티를 영속성 컨텍스트에서 관리
MyEntity managedEntity = entityManager.merge(detachedEntity);
managedEntity.setAnotherProperty("Updated Property");
transaction.commit();
기존 Entity는 detached가 되고, update한 내용인 Entity는 managed 상태가 된다.
트랜잭션 - 지연 쓰기, 캐시
✔ @Transactional
트랜잭션이 종료되면 영속성 콘텍스트를 DB에 반영하고, 종료한다. (생명주기가 같다고 본다.)
반영할 sql을 영속성 콘텍스트에 모아두었다가 한 번에 처리해주는 역할
✔ flush()
영속성 컨텍스트를 DB와 동기화한다. (Managed와 DB를 맞춘다.)
(영속성 컨테이너에 Entity는 소멸되지 않는다.)
1. 트랜잭션 종료
2. flush() 호출
3. jpql 쿼리 동작
✔ AutoFlush
jpql 쿼리 실행시 자동으로 flush() -> jpql 동작
jqpl을 사용하면 DB에 접근하게 되는데, 영속성 컨텍스트 내용을 반영하고, 다시 가져온다.
동작에 이상없도록 하기 위한 JPA의 설정이다.
✔ save()
save()는 하나의 트랙잭션이다(@Transactional Propagation required가 달려있다.)
트랜잭션이므로 호출 시점에서 영속성 콘텍스트 내용을 DB에 반영하고, 종료한다.
하지만 상위 @Transactional로 묶여있다면 save는 DB에 저장하겠다는 의미가 아니다.
상위 트랜잭션에 묶여 사용된다. (Propagation)
save에서 persist & merge의 이해
persist - 새로운 객체를 영속성 콘텍스트에서 등록 관리한다. 트랜잭션 종료 시 insert
merge - detach Entity를 manage상태로 만든다. 트랜잭션 종료 시 update
Detach Entity는 단순히 볼게 아니다.
처음 key 1을 갖고있는 Entity는 manage상태이고
같은 키를 갖는 새로 생성한 Entity는 detach 상태가 된다. (키가 같기 때문에)
키가 같은 Entity는 detach상태이고, merge가 동작하여 update가 된다.
기존의 Entity를 조회하여 set...()으로 값을 변경했을 때도 마찬가지로 detach Entity상태가 되고, merge 동작하게 된다.
void 영속성상태테스트(){
// 해당 key 가 없으면 insert
MyUser myUser = new MyUser();
myUser.setId(1L);
myUser.setName("Park");
myUserRepository.save(myUser); // insert
System.out.println(myUserRepository.findById(1L));
// 해당 key 가 있으면 update
MyUser myUser1 = new MyUser();
myUser1.setId(1L);
myUser1.setName("Park123");
myUserRepository.save(myUser1);
System.out.println(myUserRepository.findById(1L));
// 해당 key 가 있으면 update
MyUser myUser2 = myUserRepository.findById(1L).orElseThrow(RuntimeException::new);
myUser2.setName("Park456");
myUserRepository.save(myUser2);
System.out.println(myUserRepository.findById(1L));
}
첫번째 save는 insert
두번째, 세번째 save 는 update가 동작한다. save()에서 select로 해당 키를 가져와서 merge -> update한다.
간단하게 새로운 id라면 insert, 기존 DB에 있는 id라면 update
✔ 예외처리
checked - 컴파일 타임 때 알 수 있으므로 무조건 예외 처리된다는 전제하에 RollBack 되지 않고 DB에 반영된다. (실제 예외가 발생해도 commit)
unchecked - 개발자가 놓칠 수 있으므로 예외 발생 시 RollBack
✔ Transaction Propagation
(default) - required 상위 트랜잭션이 있다면 유지해서 사용, 없다면 생성
support - 상위 트랜잭션이 있다면 사용, 없다면 없이 진행
notSupport - 해당 영역은 무조건 트랜잭션 없이 사용
requires_new 트랜잭션을 새로 생성해서 사용
nested 같은 트랜잭션을 사용하지만 종속된 트랜잭션은 상위에 영향을 주지않는다. 상위로부터는 영향받는다.
mandatory - 상위 트랜잭션이 있어야하고, 없으면 오류발생
never - 상위 트랜잭션이 없어야하고, 있으면 오류발생
'프로그래밍 > JPA' 카테고리의 다른 글
[JPA] LazyInitializationException 해결 (0) | 2023.09.29 |
---|---|
[JPA] fetch LAZY인데 EAGER처럼 쿼리가 나갈 때 (0) | 2022.08.20 |
[JPA] 관계 설정 연관 관계의 주인, Cascade, N+1 (0) | 2022.05.09 |