영속성 컨텍스트

Hanjmo·2023년 8월 8일
0

EntityManagerFactory, EntityManager

엔티티 매니저(EntityManager)는 엔티티를 생성하고, 수정, 삭제, 조회(CRUD)하는 등 엔티티와 관련된 모든 일을 처리한다.

엔티티를 영속성 컨텍스트에 저장하는 persist()와 삭제하는 remove()같은 다양한 메서드를 제공한다.

이러한 엔티티 매니저를 생성하고 관리하는 것이 바로 EntityManagerFactory다.

WAS에 요청이 들어오면, EntityManagerFactory는 각 요청마다 하나의 EntityManager를 생성한다.

생성된 EntityManager는 커넥션 풀에 있는 커넥션을 사용하여 DB에 엔티티를 저장하거나 수정하는 등 엔티티 관련 작업을 수행한다.

영속성 컨텍스트

영속성 컨텍스트란 엔티티를 영구적으로 저장하는 환경을 말하며, 엔티티 매니저가 엔티티를 보관하고 관리하는 곳이 바로 이곳이다.

영속성 컨텍스트의 특징

영속성 컨텍스트에서 관리되는 엔티티는 필수로 식별자 값을 가져야 하는데, 영속성 컨텍스트에서 엔티티를 key-value 형태로 관리하기 때문이다.

엔티티는 영속성 컨텍스트에서 관리되다가 트랜잭션을 커밋하면 관리중인 DB에 반영되며, 이를 flush라고 한다.

Flush란 영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업을 말한다.

영속성 컨텍스트의 이점

  • 1차 캐시를 이용해 DB에 SQL문을 전달하지 않고도 엔티티를 빠르게 조회 및 변경할 수 있다.
  • 변경 감지(dirty-checking)를 통해 엔티티를 수정하면 자동으로 update SQL을 수행한다.
  • 1차 캐시로 반복 가능한 읽기 등급의 트랜잭션 격리 수준을 제공하는 덕분에 동일성을 보장할 수 있다.
  • 트랜잭션을 지원하는 쓰기 지연을 통해서 쿼리를 모아서 커밋하는 시점에 한꺼번에 수행할 수 있다. 이로 인해서 최적화가 가능해진다.
  • 지연 로딩 덕분에 엔티티를 조회할 때 연관된 엔티티를 함께 조회할 수 있다.

엔티티 생명주기

엔티티의 상태는 4가지로 구분된다.

  • 비영속 (New / Transient)
    엔티티가 처음 생성되어 영속성 컨텍스트에 저장되지 않은 상태

  • 영속 (Managed)
    엔티티가 영속성 컨텍스트에 저장되어 관리중인 상태

  • 준영속 (Detached)
    영속성 컨텍스트에서 관리되던 엔티티가 분리된 상태

  • 삭제 (Removed)
    엔티티가 영속성 컨텍스트, DB에서 삭제된 상태

엔티티는 처음 생성되어 영속성 컨텍스트에 저장되지 않은 비영속 상태로 있다가 persist()를 통해 영속화, 즉 영속 상태가 된다.

영속성 컨텍스트에서 관리중인 영속 상태인 엔티티는 flush()를 통해 DB에 저장되며, find()를 통해 DB에 존재하는 엔티티를 영속 상태로 만들 수도 있다.

영속성 컨텍스트에 있는 엔티티가 떨어져 나가 분리된 상태를 준영속 상태라고 하는데, 해당 엔티티는 더이상 영속성 컨텍스트에서 관리되지 않는다.

영속 상태의 엔티티를 준영속 상태로 만드는 방법은 세 가지가 존재한다.

  • detach(): 임의로 하나의 엔티티만 준영속 상태로 만든다.
  • clear(): 영속성 컨텍스트에 있는 모든 엔티티를 준영속 상태로 만든다.
  • close(): 영속성 컨텍스트를 아예 종료해서 관리중이던 엔티티를 모두 준영속 상태로 만든다.

준영속 상태의 엔티티는 merge()를 통해 다시 영속화할 수 있다.

영속성 컨텍스트에 있는 엔티티를 삭제하려면 remove()를 사용하면 되며, 삭제된 상태의 엔티티는 다시 persist()를 통해 영속화할 수 있으며 flush()를 통해 DB에 반영할 수 있다.

CRUD 예제

저장

void save() {
		EntityManager em = emf.createEntityManager(); // 엔티티 매니저 생성
		EntityTransaction transaction = em.getTransaction(); // 트랜잭션 획득
		transaction.begin(); // 트랜잭션 시작
	
		Customer customer = new Customer(); // 비영속
		customer.setId(1L);
		customer.setFirstName("jam");
		customer.setLastName("han");

		em.persist(customer); // 영속화

		transaction.commit(); // 트랜잭션 커밋(종료)
}

엔티티 매니저가 트랜잭션을 획득하고, begin()을 통해 트랜잭션을 시작한다.

그리고나서 Customer 객체를 생성하고, 값을 설정한 뒤에 persist()를 통해 생성한 엔티티를 영속화하면 영속성 컨텍스트의 1차 캐시에 행당 엔티티가 저장된다.

동시에 쓰기 지연 저장소에 insert문을 저장하는데, 후에 트랜잭션을 커밋하면 쓰기 지연 저장소에 있던 쿼리가 DB에 전달되면서 flush된다.

조회

1차 캐시에서 조회

void readFromCache() {
		EntityManager em = emf.createEntityManager(); // 엔티티 매니저 생성
		EntityTransaction transaction = em.getTransaction(); // 트랜잭션 획득
		transaction.begin(); // 트랜잭션 시작

		Customer customer = new Customer(); // 비영속
		customer.setId(1L);
		customer.setFirstName("jam");
		customer.setLastName("han");

		em.persist(customer); // 영속화

		transaction.commit(); // 트랜잭션 커밋(종료)

		Customer findCustomer = em.find(Customer.class, 1L);
		log.info("{} {}", findCustomer.getFirstName(), findCustomer.getLastName());
}

트랜잭션을 시작하고, 엔티티를 영속화한 뒤 커밋하면 엔티티는 DB에 저장되기도 하지만 영속성 컨텍스트의 1차 캐시에도 남아 있게 된다.

따라서 엔티티를 조회하면 1차 캐시에서 먼저 찾아본 뒤에 있으면 DB에 쿼리를 날리지 않고 바로 반환한다.

1차 캐시에 조회할 엔티티가 없다면, 그때 DB에 쿼리를 날려서 조회한다.

위 사진은 예제 코드를 실행한 사진으로, select 쿼리가 나가지 않고도 엔티티를 조회할 수 있음을 알 수 있다.

DB에서 조회

void readFromDB() {
		EntityManager em = emf.createEntityManager(); // 엔티티 매니저 생성
		EntityTransaction transaction = em.getTransaction(); // 트랜잭션 획득
		transaction.begin(); // 트랜잭션 시작

		Customer customer = new Customer(); // 비영속
		customer.setId(1L);
		customer.setFirstName("jam");
		customer.setLastName("han");

		em.persist(customer); // 영속화
		transaction.commit(); // 트랜잭션 커밋(종료)
	
		em.clear(); // 영속성 컨텍스트 초기화

		Customer findCustomer = em.find(Customer.class, 1L);
		log.info("{} {}", findCustomer.getFirstName(), findCustomer.getLastName());
		em.find(Customer.class, 1L);
}

엔티티를 영속화하고 커밋한 뒤에 clear()를 통해 영속성 컨텍스트를 초기화하면 1차 캐시에는 어떤 엔티티도 존재하지 않게 된다.

이때 엔티티를 조회하면, 1차 캐시에 해당 엔티티가 없으므로 DB로 select 쿼리를 날려 엔티티를 조회한다.

수정

void update() {
		EntityManager em = emf.createEntityManager(); // 엔티티 매니저 생성
		EntityTransaction transaction = em.getTransaction(); // 트랜잭션 획득
		transaction.begin(); // 트랜잭션 시작

		Customer customer = new Customer(); // 비영속
		customer.setId(1L);
		customer.setFirstName("jam");
		customer.setLastName("han");

		em.persist(customer);
		transaction.commit(); // 트랜잭션 커밋(종료)

		transaction.begin();

		Customer findCustomer = em.find(Customer.class, 1L);
		findCustomer.setFirstName("sun");
		findCustomer.setLastName("jo");
		
		transaction.commit(); // 트랜잭션 커밋(종료)
}

엔티티를 영속화하고나서 해당 엔티티를 수정한 뒤에 커밋하게 되면, update 쿼리가 수행되어 엔티티가 자동으로 수정된다.

이렇게 update()를 사용한 별다른 작업 없이 자동으로 엔티티가 수정되는 이유는 변경 감지(dirty-checking) 덕분이다.

변경 감지는 말 그대로 JPA가 변경을 감지하여 자동으로 update 쿼리를 수행하는 것을 말한다.

JPA는 엔티티를 영속성 컨텍스트에 저장할 때, 최초 상태를 복사해서 저장하는데 이것을 스냅샷이라고 한다.

만약 트랜잭션을 커밋하여 flush하는 시점에 스냅샷과 엔티티의 상태를 비교해서 변경된 내용이 있다면 update 쿼리를 수행한다.

삭제

void delete() {
		EntityManager em = emf.createEntityManager(); // 엔티티 매니저 생성
		EntityTransaction transaction = em.getTransaction(); // 트랜잭션 획득
		transaction.begin(); // 트랜잭션 시작

		Customer customer = new Customer(); // 비영속
		customer.setId(1L);
		customer.setFirstName("jam");
		customer.setLastName("han");

		em.persist(customer);

		transaction.commit(); // 트랜잭션 커밋(종료)

		transaction.begin();

		Customer findCustomer = em.find(Customer.class, 1L);
		em.remove(findCustomer);

		transaction.commit(); // 트랜잭션 커밋(종료)
}

0개의 댓글