maeng0830
뇌 채우기 운동
maeng0830
전체 방문자
오늘
어제
  • maeng0830-note (85)
    • 자바 (3)
    • 스프링 (39)
      • Core (21)
      • DB (16)
      • Security (2)
      • Test (0)
    • 자료구조 & 알고리즘 (19)
      • 자료구조 (12)
      • 알고리즘 (7)
    • 다른 개발 도구들 (4)
      • Git&Github (1)
      • Redis (3)
    • 프로젝트 (9)
      • Album (9)
    • CS (10)
      • 운영체제 (5)
      • 데이터베이스 (5)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 자료구조
  • 트랜잭션
  • JPQL
  • JPA
  • spring security

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
maeng0830

뇌 채우기 운동

스프링/DB

데이터 접근 기술_JPA_영속성 컨텍스트

2023. 3. 7. 21:29

영속성 컨텍스트

영속성 컨텍스트는 JPA에서 가장 중요한 개념 중 하나이다.

JPA의 내부 동작 원리를 정확히 이해하기 위해서는 영속성 컨텍스트를 알아야한다.

 

EntityManagerFactory와 EntityManager

웹 어플리케이션이 구동되면 하나의 EntityManagerFactory가 생성된다.

그리고 트랜잭션 단위의 요청이 발생할 때마다, EntityManagerFactory는 EntityManager를 생성한다.

EntityManager들은 ConnectionPool에서 Connection을 획득하여 DB 작업을 실행한다. 

 

영속성 컨텍스트

영속성 컨텍스트는 엔티티를 영구 저장하는 환경을 의미하며, EntityManger를 통해 접근 가능하다.

영속성 컨텍스트는 눈에 보이지 않는 논리적인 개념이다.

 

사실 EntityManager.persist(entity)는 DB에 데이터를 저장하는 것이 아니라.

데이터를 영속성 컨텍스트에 저장하는 것이다.

 

Member는 JPA가 인식할 수 있는 엔티티(Entity)라는 것을 염두하고, 아래의 코드를 살펴보자. 

 

tx.begin()을 통해 트랜잭션이 시작되고, 그 아래에서 작업이 진행된다.

 

생성됐을 당시 member는 비영속 상태이다.

비영속 상태는 영속성 컨텍스트에 저장되지 않은 상태를 말한다.

 

em.persist(member)를 통해 영속성 컨텍스트에 저장될 때

비로소 member는 영속 상태가 된다. 영속 상태는 영속성 컨텍스트에 저장된 상태를 말한다.

그리고 영속 상태인 객체를 영속 엔티티(Entity)라고 부른다.

사실 더 정확히 말하자면

영속성 컨텍스트에 저장되는 것이 아니라, 영속성 컨텍스트 내부의 1차 캐시에 저장되는 것이다.

 

1차 캐시

영속성 컨텍스트 내부의 1차 캐시는 말 그대로 어플리케이션과 DB 사이에서 캐시 역할을 한다.

1차 캐시에는 엔티티가 PK(DB) : 엔티티 : 스냅샷의 형태로 저장된다.

  • PK(DB)는 1차 캐시에서 특정 엔티티를 조회하기 위한 식별자이다. PK 없이는 1차 캐시에 엔티티가 저장될 수 없다.
  • 엔티티는 말 그대로 현재 저장된 엔티티 객체이다.
  • 스냅샷은 엔티티 객체의 기존 상태이다.

 

엔티티 객체의 변경 사항, 즉 데이터의 변경 사항(저장, 수정, 삭제)은 DB에 바로 전달되지 않는다. 

우선 영속성 컨텍스트의 1차 캐시에 저장되며, flush()가 호출될 때 비로소 변경 사항이 반영된 쿼리가 DB에 전달된다.

 

아래 코드에서는 트랜잭션을 커밋하는 tx.commit()에서 쿼리가 전달된다.

그 이유는 commit()을 호출할 경우, 그 전에 flush() 자동으로 호출되기 때문이다.

 

DB에서 데이터를 조회할 때는 다음과 같은 과정이 진행된다.

우선 1차 캐시에 조회하고자 하는 데이터의 엔티티가 저장되어 있는지 확인한다.

1차 캐시에 해당 엔티티가 존재하는 경우, 그 엔티티를 획득한다.

만약 1차 캐시에 조회하고자 하는 데이터의 엔티티가 없을 경우, 비로소 쿼리를 통해 DB에서 데이터를 조회해온다.

조회된 데이터는 엔티티화되어 1차 캐시에 저장되고, 1차 캐시에서 해당 엔티티를 획득하게 된다. 

 

아래 코드에서 em.find(Member.class, 101L)를 호출하여 PK 값이 101L인 Memeber 엔티티를 획득하고자 시도했다.

이때 DB에서 쿼리를 통해 직접 데이터를 조회하지 않는다.

em.persist(member)를 통해 해당 엔티티가 1차 캐시에 저장되어 있기 때문이다. 

 

flush 등 영속성 컨텍스트(1차 캐시)에 대한 더욱 세부적인 내용은 추후에 다른 코드를 참고하면서 더 자세히 알아보자.  

public class JpaMain {

	public static void main(String[] args) {
		/**
		 * META-INF/persistence.xml의 정보를 읽어 EntityManagerFactory를 생성한다.
		 * 어플리케이션 구동 시점에 딱 1개의 emf만 생성
		 */
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

		/**
		 * EntityManagerFactory에서 entityManager를 생성한다.
		 * 트랜잭션 단위의 작업이 실행 될 때마다, em을 생성
		 * 해당 em 내부에 영속성 컨텍스트가 생성된다.
		 */
		EntityManager em = emf.createEntityManager();

		// 트랜잭션 획득 및 시작
		// JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
		EntityTransaction tx = em.getTransaction();
		tx.begin();

		// 저장
		try {
			// 비영속 상태
			Member member = new Member();
			member.setId(101L);
			member.setUsername("HelloJPA");

			// 영속 상태
			// em 내부의 영속성 컨텍스트에 entity가 저장된다.
			System.out.println("=== em.persist(member) ===");
			em.persist(member);

			// DB에 select 쿼리 전달 X, 영속성 컨텍스트의 1차 캐시에 저장된 데이터를 가져오기 때문이다.
			System.out.println("=== em.find(Member.class, 101L) ===");
			Member findMember = em.find(Member.class, 101L);
			System.out.println("findMember.id = " + findMember.getId());

			System.out.println("=== tx.commit() ===");
			// DB에 쿼리 전달
			tx.commit();
		} catch (Exception e) {
			tx.rollback();
		} finally {
			// 리소스 정리
			em.close();
		}

		...

		emf.close();
	}
}

 

반복 가능 읽기 등급의 트랜잭션 격리 수준 제공

영속성 컨텍스트는 1차 캐시를 통해 반복 가능 읽기 등급의 트랜잭션 격리 수준을 DB가 아닌 어플리케이션 차원에서 제공해준다. 마치 자바 컬렉션과 같은 기능을 제공해주는 것이다.

 

자바 컬렉션에서 동일한 key를 조회하면, 동일한 객체가 조회되고,

컬렉션에서 획득한 객체에 변경사항을 적용하면, 자바 컬렉션에 저장된 객체에도 변경사항이 적용되는 것을 생각해보자.

 

1차 캐시도 자바 컬렉션과 동일하게 기능한다. 영속 엔티티의 동일성을 보장하는 것이다.   

 

아래 코드에서 동일한 key로 조회한 findMember1과 findMember2는 동일한 객체이다.

findMember1 == findMember2는 true이다.

		EntityManager em2 = emf.createEntityManager();
//		EntityTransaction tx2 = em2.getTransaction();
		try {
			// DB에 select 쿼리 전달 O, 영속성 컨텍스트의 1차 캐시에 해당 엔티티가 저장되어있지 않기 때문이다.
			// DB에서 조회된 데이터는 엔티티화 되어 1차 캐시에 저장된다.
			System.out.println("=== 첫번째 em.find(Member.class, 101L) ===");
			Member findMember1 = em2.find(Member.class, 101L);
			System.out.println("findMember1.id = " + findMember1.getId());

			// DB에 select 쿼리 전달 X, 영속성 컨텍스트의 1차 캐시에 해당 엔티티가 저장되어있기 때문이다.
			System.out.println("=== 두번째 em.find(Member.class, 101L) ===");
			Member findMember2 = em2.find(Member.class, 101L);
			System.out.println("findMember2.id = " + findMember2.getId());

			// 1차 캐시를 통해 반복 가능 읽기 등급의 트랜잭션 격리 수준을 DB가 아닌 어플리케이션에서 제공 가능하다.
			// 마치 자바 컬렉션에서 객체를 조회했던 것 처럼, 1차 캐시에서 조회된 객체는 동일성이 보장된다.
			System.out.println("=== 영속 엔티티의 동일성 보장 ===");
			System.out.println(findMember1 == findMember2); // true

			System.out.println("=== tx.commit() ===");
			// 조회만 하므로 불필요
			// tx2.commit();
		} catch (Exception e) {
			// tx2.rollback();
		} finally {
			// 리소스 정리
			em2.close();
		}

 

쓰기 지연

영속성 컨텍스트는 쓰기 지연 기능을 제공한다.

영속 엔티티(데이터)에 변경 사항이 있을 경우, 해당 변경 사항을 DB에 전달하기 위해 쿼리가 생성된다.
그리고 생성된 쿼리는 영속성 컨텍스트 내부의 쓰기 지연 SQL 저장소에 저장된다.

 

변경 사항이 발생할 때마다 변경 사항을 반영하는 쿼리가 생성되고 쓰기 지연 SQL 저장소에 계속 쌓이는 것이다.

 

그리고 flush()가 호출될 때 비로소 쿼리들이 한 번에 DB에 전달된다.

 

아래의 코드에서는 트랜잭션 커밋을 의미하는 tx3.commit()이 호출될 때 쿼리들이 DB에 전달된다.

tx3.commit()이 호출되면 flush()가 먼저 호출되어 쿼리를 먼저 전달하고, 그 후에 트랜잭션 커밋되기 때문이다. 

		EntityManager em3 = emf.createEntityManager();
		EntityTransaction tx3 = em3.getTransaction();
		tx3.begin();
		try {
			Member memberA = new Member(123L, "MemberA");
			Member memberB = new Member(456L, "MemberB");

			/**
			 * !!쓰기 지연
			 * em3.persist(entity)가 실행되면, 생성된 insert 쿼리들은 영속성 컨텍스트 내부의 쓰기 지연 SQL 저장소에 저장된다.
			 * 그리고 해당 엔티티는 1차 캐시에 저장된다.
			 */
			System.out.println("=== em3.persist(entity) ===");
			em3.persist(memberA);
			em3.persist(memberB);

			// tx.commit() 시, 쓰기 지연 SQL 저장소에 저장된 insert 쿼리들이 DB에 전달된다. = flush
			// 그리고 한번에 commit 된다 = commit
			System.out.println("=== tx3.commit() ===");
			tx3.commit();
		} catch (Exception e) {
			tx3.rollback();
		} finally {
			em3.close();
		}

 

변경 감지(Dirty Checking)

변경 감지(Dirty Checking)은 영속 엔티티의 변경 사항을 감지하고, 그 변경 사항을 DB에 전달하기 위한 쿼리를 생성하는 기능이다.

 

자바 컬렉션에서 획득한 특정 객체에 변경사항이 있을 경우, 컬렉션에 저장된 해당 객체도 변경된다.

그리고 앞서 말했듯이 1차 캐시에서 획득한 영속 엔티티에 변경 사항이 있을 경우, 1차 캐시에 저장된 영속 엔티티도 변경된다. 

 

영속 엔티티에 변경 사항이 있을 경우,

1차 캐시에 저장된 해당 영속 엔티티의 스냅샷(기존 상태)과 현재 영속 엔티티를 비교하여

변경 사항을 DB에 전달하기 위한 쿼리를 생성하여 쓰기 지연 SQL 저장소에 저장한다. 

 

아래의 코드에서 em4.update(findMember)와 같은 코드가 불필요한 이유도,

findMember에 변경 사항이 생기자마자 변경 감지가 동작하여, update 쿼리를 자동으로 생성했기 때문이다.

 

EntityManager.persist(Entity)도 마찬가지이다.

Entity의 PK에 해당하는 스냅샷이 없는 상태이기 때문에 영속 엔티티와 스냅샷을 맞추기 위한, 즉 스냅샷이 영속 엔티티와 동일한 상태가 되기 위해 데이터를 저장하는 insert 쿼리가 생성되는 것이다. 

EntityManager em4 = emf.createEntityManager();
		EntityTransaction tx4 = em4.getTransaction();
		tx4.begin();
		try {
			// DB로부터 해당 데이터를 조회하고, 엔티티화 하여 1차 캐시에 저장한다. 그리고 해당 영속 엔티티를 반환한다.
			System.out.println("=== Member findMember = em4.find(Member.class, 101L) ===");
			Member findMember = em4.find(Member.class, 101L);

			// !!변경 감지(Dirty Checking)
			// 자바 컬렉션에서 획득한 객체에 변경사항이 있을 경우, 컬렉션에 저장된 객체도 변경되는 것 처럼,
			// 1차 캐시에서 획득한 영속 엔티티에 변경사항이 있을 경우, 1차 캐시에 저장된 영속 엔티티도 변경된다.
			// 1차 캐시에 저장된 해당 영속 엔티티의 스냅샷(기존 상태)과 현재 영속 엔티티간의 차이가 있을 경우,
			// update 쿼리가 쓰기 지연 SQL 저장소에 저장된다.
			System.out.println("=== findMember.setName(\"modMember\") ===");
			findMember.setUsername("modMember");

			// 그러므로 em.update(findMember)와 같은 별도의 update 쿼리를 생성하는 코드가 필요 없다.

			// 쓰기 지연 SQL 저장소에 저장된 update 쿼리를 DB에 전달한다. = flush
			// 그리고 커밋한다.
			System.out.println("=== tx4.commit() ===");
			tx4.commit();
		} catch (Exception e) {
			tx4.rollback();
		} finally {
			em4.close();
		}

 

플러시(flush)

플러시는 영속 엔티티의 변경 사항을 데이터베이스에 반영하는 기능이다.

영속 엔티티와 데이터베이스의 데이터를 동기화하는 기능이라고도 할 수 있다.

 

더 구체적으로 말하자면, 영속 엔티티의 변경 사항을 데이터베이스에 반영하기 위해 쓰기 지연 SQL 저장소에 쌓여있던 쿼리들을 DB에 전달하는 기능이다. 

 

플러시를 호출하는 방법은 기본적으로 아래와 같다.

  • EntityManger.flush()로 직접 호출
  • 트랜잭션 커밋 시 자동 호출
  • JPQL 쿼리 실행 시 자동 호출

플러시 모드 옵션을 통해 약간의 변경이 가능하다. 그냥 알아만 두고, 기본 값을 사용하면 된다.

  • EntityManger.setFlushMode(FlushModeType.COMMIT): 커밋 할 때만 플러시 자동 호출
  • EntityManger.setFlushMode(FlushModeType.AUTO): 커밋 또는 쿼리 실행 시 플러시 자동 호출(기본 값)

플러시는 영속성 컨텍스트를 비워내는 것이 아니며, 변경사항만 전달한다는 것을 주의하자.

 

준영속 상태

준영속 상태란 영속 상태의 엔티티, 즉 영속 엔티티가 영속성 컨텍스트에서 분리된 상태를 말한다.

준영속 엔티티에 대해서는 변경 감지 등 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다.

 

준영속 상태로 만드는 방법은 아래와 같다.

  • EntityManager.detach(Entity): 특정 엔티티를 준영속 상태로 변경한다.
  • EntityManager.clear(): 영속성 컨텍스트를 완전히 초기화한다. 
  • EntityManager.close(): 영속성 컨텍스트를 종료한다.

출처: 자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의 (inflearn.com)

    '스프링/DB' 카테고리의 다른 글
    • 데이터 접근 기술_JPA_상속 관계 매핑
    • 데이터 접근 기술_JPA_연관 관계 매핑
    • 데이터 접근 기술_JPA_기본 개념
    • 데이터 접근 기술_JdbcTemplate
    maeng0830
    maeng0830

    티스토리툴바