-
Notifications
You must be signed in to change notification settings - Fork 0
19. [Spring Boot] 영속성 컨텍스트(Persistence Context)
SeongWon Oh edited this page Oct 10, 2021
·
1 revision
- JPA를 이해하는데 가장 중요한 용어이다.
- 엔티티를 영구 저장하는 환경이라는 뜻이다.
- 어플리케이션과 DB사이에서 객체를 보관하는 가상의 DB같은 역할을 한다.
- EntityManager는 영속성 컨텍스트에서 가장 중요한 역할을 한다.
- EntityManager에 엔티티를 저장하거나 조회하면 EntityManager는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
- 영속성 컨텍스트는 EntityManager를 생성할 때 만들어지며 EntityManager를 통해 영속성 컨텍스트에 접근하고 관리한다.
- 다음 코드로 Entity를 영속성 컨텍스트에 저장할 수 있다.
entityManager.persist(entity);
- Spring에서는 EntityManager를 주입하여 사용하면 같은 트렌잭션의 범위에 있는 EntityManager는 같은 영속성 컨텍스트에 접근한다.
- EntityManager는 영속성 컨텍스트 내에서 Entity들을 관리하고 있다.
- EntityManager는 JPA에서 제공하는 interface로 spring bean으로 등록되어 있어 Autowired로 사용할 수 있다.
@Autowired
private EntityManager entityManager;
- Query Method, Simple JPA repository는 직접적으로 entityManager를 사용하지 않도록 한번 더 감싸준 것이다.
- spring jpa에서 제공하지 않는 기능을 사용하거나 특별한 문제가 있어서 별도로 customizing을 해야한다면 entityManager를 직접 받아서 처리한다.
- EntityManager는 Entity Cache를 갖고 있다.
- 영속성 컨텍스트와 전혀 관계가 없는 상태이다.
- 엔티티 객체를 생성하였지만 아직 영속성 컨텍스트에 저장하지 않은 상태를 의미한다.
//객체만 생성한 비영속상태
User user = new User();
- 영속성 컨텍스트에 저장된 상태
- 엔티티가 영속성 컨텍스트에 의해 관리된다.
- 영속 상태가 되었다고 바로 DB에 값이 저장되지 않고 트렌젝션의 커밋 시점에 영속성 컨텍스트에 있는 정보들을 DB에 쿼리로 날리게 된다.
@Autowired
private EntityManager entityManager;
// Class내에 Autowired로 EntityManager추가
//객체만 생성한 비영속상태
User user = new User();
// 객체를 저장한 영속상태
entityManager.persist(user);
- 영속성 컨텍스트에 저장되었다가 분리된 상태
- 엔티티를 준영속 상태로 만드려면 entityManager.detach()를 호출한다.
// 영속 -> 준영속
// user엔티티를 영속성 컨텍스트에서 분리하면 준영속 상태가 된다.
entityManager.detach(user);
// 영속성 콘텍스트를 비우면 관리되고 있던 엔티티들은 준영속 상태가 된다. (대기 상태에 있는 변경 데이터들도 삭제)
entityManager.clear();
// 영속성 콘텍스트를 종료해도 관리되던 엔티티들은 준영속 상태가 된다.
entityManager.close();
// 준영속 -> 영속
// detach를 하여 준영속상태에 빠진 entity를 merge를 하면 다시 영속 상태가 된다.
entityManager.merge(user);
준영속 상태의 특징
- 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않는다.
- 식별자 값을 가지고 있다.
- 영속성 컨텍스트와 DB에서 해당 엔티티를 삭제하여 삭제된 상태이다.
// user엔티티를 영속성 컨텍스트와 DB에서 삭제
entityManager.remove(user);
- Save 메서드와 같이 DB변경하는 메서드를 실행하였을 때 바로 DB가 업데이트 되지 않고 영속성 컨텍스트 내부에 있는 캐시를 거쳐서 DB가 업데이트된다. 해당 캐시를 우리는 1차 캐시라고 부른다.
- 1차 캐시에는 영속 상태의 엔티티를 저장한다.
- 1차 cache는 Map의 형태로 만들어진다.
- Map에는 key는 id값, value는 해당 entity값이 들어있다.
- key값이 id라서 id로 조회를 하게 되면 영속성 context에 있는 1차 cache에 entity가 있는지 확인을 해보고 값이 있다면 DB조회없이 return한다. 만약 값이 없으면 쿼리문으로 조회를 하고 1차 cache에 저장후 return해준다.
- id가 아닌 다른 값을 이용하여 조회를 하면 1차 cache가 적용되지 않는다.
- Delete, Update와 같은 작업을 할 때도 JPA 내부적으로는 ID를 통한 조회를 많이 하게 된다.
- 즉, 하나의 Transactional에서 id값으로 조회하는 데이터들은 1차 cache에 저장을 하여 관리를 함으로써 JPA의 조회 성능이 올라간다.
- 1차 캐시에서 조회하는 방법
// entityManager.find(엔티티 클래스 타입, 식별자 값);
User findUser = entityManager.find("User.class", "1L");
- 아래의 코드와 같이 data를 id값으로 반복적으로 찾게 되었을 때 @Transactional이 붙는다면 JPA는 데이터를 조회하며 cache에 저장을 한 후 다음 조회때는 같은 조회를 하지 않고 cache에 있는 값을 내보낸다.
@Test
void cacheFindTest() {
System.out.println(userRepository.findById(1L).get());
System.out.println(userRepository.findById(1L).get());
System.out.println(userRepository.findById(1L).get());
}
위의 실행 결과를 보면 1차 캐시의 적용으로 인해 Select문으로 한번의 조회만 하였지만 3개의 결과가 나오는 것을 확인할 수 있다.
1차 Cache조회의 흐름
- 1차 캐시에서 탐색한 ID값의 엔티티를 찾는다. 2-1. 탐색한 결과 해당 엔티티가 1차 캐시에 존재하면 값을 가져온다. 2-2. 탐색 결과가 1차 캐시에 존재하지 않다면 DB에서 값을 조회하고 조회한 데이터를 엔티티로 생성해 1차 캐시에 저장한다. (해당 엔티티를 영속 상태로 만든다.)
- 조회한 엔티티를 반환한다.
🚨 주의
- 1차 캐시는 서로 공유하지 않고 하나의 쓰레드가 시작할때부터 끝날때까지 잠깐 사용하는 글로벌하지 않는 캐시이다.
- 100명 한테 요청 100개 오면, 엔티티 매니저 100개 생기고 1차캐시도 100개 생긴다. 스레드 종료되면, 그때 다 사라진다.
- 트랜잭션의 범위 안에서만 사용하는 굉장히 짧은 캐시 레이어이다.
- 전체에서 쓰는 글로벌 캐시는 2차 캐시라고 한다.
- 영속성 콘텍스트는 영속 엔티티의 동일성을 보장한다. ※ 동일성은 값 뿐만 아니라 실제 인스턴스 자체가 같다는 뜻이다.
- 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 DB가 아닌 애플리케이션 차원에서 제공한다.
User findUser1 = entityManager.find("User.class", "1L");
User findUser2 = entityManager.find("User.class", "1L");
System.out.print(findUser1 == findUser2) // Result: true
- 동일성을 보장하여 Id값이 1인 user를 조회한 두번의 결과의 동일성 비교를 해본 결과 true의 결과가 나오는 것을 확인할 수 있다.
- entity값을 변경하면 DB에 바로 업데이트 하지 않는다.
- 트랜젝션 내부에서 영속 상태의 entity의 값을 변경하면 INSERT SQL Query들은 DB에 바로 보내지않고 쿼리 저장소에 쿼리문들을 생성해서 쌓아둔다.
- 쿼리 저장소에 쌓여있는 쿼리들은 entityManager의 **flush()**나 트렌젝션의 commit을 통해 보내지게 된다.
entityManager.flush();
- flush()는 1차 캐시를 지우지 않고 쿼리를 DB에 날려서 DB와의 싱크를 맞추는 역할을 한다. 쿼리를 보내고 난 후에 commit()을 실행한다.
- 트렌젝션을 커밋하면 flush(), commit()을 하게 된다.
- 엔티티의 수정이 일어나도 개발자는 영속성 컨텍스트에 따로 알려주지 않아도 영속성 컨텍스트가 알아서 변경 사항을 체크해준다. 이것을 Dirty checking이라고 한다.
- 1차 캐시에 entity를 저장할때 스냅샷 필드도 따로 저장하여 commit이나 flush를 할 때 해당 entity와 스냅샷을 비교하여 변경사항이 있으면 알아서 UPDATE SQL을 만들어서 DB에 전송한다.
Dirty Checking의 흐름
- 트랙잭션을 커밋하면 entityManager의 내부에서 먼저 플러시가 호출된다.
- 엔티티와 스냅샷을 비교하여 변경된 엔티티를 찾는다.
- 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 저장한다.
- 쓰기 지연 저장소의 SQL을 플러시한다.
- 데이터베이스 트랜잭션을 커밋한다.
- flush는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
- 1차 캐시를 지우지 않고 쿼리를 DB에 날려서 DB와의 싱크를 맞추는 역할(동기화)을 한다.
- flush()를 하거나 트렌젝션의 commit을 하게 된다면 영속성 컨텍스트 내에 있는 쿼리저장소에 쌓여 있던 INSERT, UPDATE, DELETE SQL들이 데이터베이스에 날라간다.
- flush를 실행하면 Dirty checking을 통해 스냅샷과 비교하여 수정된 entity를 찾고 UPDATE Query를 만들어 쿼리 저장소에 등록한 후 쿼리 저장소에 저장된 모든 쿼리를 DB에 보내어 동기화한다.
영속성 context의 값이 DB에 반영되는 경우
- flush를 통해 개발자가 직접 반영하는 경우
- Transaction이 끝나서 해당 query가 commit되는 시점
- 복잡한 조회 조건에 JPQL query가 실행되는 시점 (위에서 데이터를 업데이트하고 아래에서 findAll을 하였을 때는 JPA에서 findAll을 하기 전에 flush를 시키고 조회를 하게된다.)