본문 바로가기

Spring/JPA & Hibernate

영속성 컨텍스트

반응형

영속성 컨텍스트란 ?

  • 엔티티를 영구 저장하는 환경
  • entityManager.persist(entity) // entity를 영속성 컨텍스트에 저장하겠다.
  • 영속성 컨텍스트를 이용해서, 엔티티를 영속화 한다. 즉, 디비에 저장하는 것이 아니라 엔티티를 영속성 컨텍스트에 저장한다.
  • 엔티티 매니져를 통해 영속성 컨텍스트에 접근한다.
Entity Manager Factory 란?
요청이 들어오면, 엔티티 매니저를 생성한다.

Entity Manager 란?
내부적으로 데이터베이스 커넥션을 이용하여 디비에 접근한다.

 

 

Entity manager factory 를 통해 요청이 올 때마다 Entity manager를 생성하게 되고, 이 Entity manager는 내부적으로 db의 커넥션을 사용하여 디비에 접근하게 된다.

 

엔티티 매니져 마다 영속성 컨택스트를 가진다. 스프링 프레임워크에서는 영속성 컨택스트를 공류할 수 있다.

 

 

엔티티의 생명주기

엔티티의 생명주기

  • 비영속 (new/transient)
    최초 엔티티를 생성한 상태
  • 영속 (managed)
    객체를 영속성 컨텍스트에 저장, 영속성 컨택스트에 의해 관리되는 상태 // em.persist(entity);
  • 준영속 (detached)
    영속성 컨택스트에 저장되었다가 분리된 상태 // em.detach(entity);
  • 삭제 (removed)
    객체가 삭제된 상태 // em.remove(entity);

 


EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

try{
	// 비영속 상태
	Member member = new Member();
	member.setId( 3L );
	member.setName( "HelloJPA" );

	// 영속 상태 ( 영속성 컨텍스트에 저장 )
	System.out.println( "======== BEFORE ========" );
	em.persist( member );
	System.out.println( "======== AFTER ========" );
			
	// database 저장
	tx.commit();
} catch(Exception e){
	tx.rollback();
} finally {
	em.close();
}
emf.close();

 

코드 실행 결과

코드 실행 결과를 보면 알 수 있듯이, 객체가 영속 상태가 된다고 해서 디비에 쿼리가 반영되는 것이 아니다.

트렌젝션 안에서 일어나는 모든 일은 영속성 컨텍스트에 저장한다. 이는 트렌젝션이 커밋되는 시점에 모든 반영 사항이 쿼리로 생성되어 디비에 반영한다.

 

영속성 컨택스트는 단지 애플리케이션과 데이터 베이스의 중간 다리 역활을 수행할 뿐이다.

그렇다면, 이렇게 애플리케이션과 데이터베이스 사이에 영속성 컨텍스트를 둠으로써 얻을 수 있는 이점은 무엇일까?

 

영속성 컨택스트의 이점

  • 1차 캐시
  • 동일성(identity) 보장
  • 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
  • 변경 감지(Dirty Checking)
  • 지연 로딩(Lazy Loading)

엔티티 조회, 1차 캐시

영속성 컨택스트 안에는 1차 캐시가 있다.

객체를 조회할 때, 데이터베이스에서 조회하는 것이 아니라, 1차 캐시에서 조회할 수 있다.

 

만약 1차 캐시에 엔티티가 없다면?

1차 캐시에 데이터가 없다면, DB에서 조회한 결과를 1차 캐시에 저장한다.

그리고, 1차 캐시에 저장되어있는 엔티티를 반환한다.

 

엔티티 메니져는 데이터베이스 트랜잭션을 생성할 때 만들고, 끝날 때 종료한다. 그렇기 때문에, 하나의 요청을 처리하는 하나의 트랜잭션 안에서만 공유가 가능하다. ( 애플리케이션 전체에서 공유할 수 있는 것은 2차캐시 )

 

🌟 엔티티 생성 시 1차 캐시에 저장되는 모습


// 비영속 상태
Member member = new Member();
member.setId( 100L );
member.setName( "HelloJPA" );

// 영속 상태 ( 영속성 컨텍스트에 저장 )
System.out.println( "======== BEFORE ========" );
// 1차 캐시에 저장 
em.persist( member );
System.out.println( "======== AFTER ========" );

// 1차 캐시에서 조회
Member findMember = em.find( Member.class, 100L );

System.out.println( "findMember id: " + findMember.getId() + ", findMember name: " + findMember.getName() );

// database 저장
tx.commit();

 

실행 결과

em.find를 통해 객체를 조회했지만, select 쿼리는 실행되지 않음. 또한, 조회하고 나서 insert 쿼리가 실행된 것을 확인할 수 있다.

즉, insert한 멤버 객체는 디비에 저장되는 것이 아니라, 1차 캐시에 먼저 저장되고, 조회 시에 1차 캐시에서 찾는다.

 

🌟 엔티티 조회 시 1차 캐시에 저장되는 모습


System.out.println( "findMember1 조회" );
Member findMember1 = em.find( Member.class, 101L );

System.out.println( "findMember2 조회" );
Member findMember2 = em.find( Member.class, 101L );

실행 결과에서 볼 수 있 듯이, findMember1 을 조회하는 시점에서는 영속성 컨텍스트에 PK가 101인 member객체가 존재하지 않으므로, insert 쿼리가 발생한다. 그러나 findMember2를 조회하는 시점에서는 앞선 쿼리로 영속성 컨텍스트의 1차 캐시에 member 객체를 저장했기 때문에 쿼리를 실행하지 않고, 1차 캐시에서 조회하기 때문에 쿼리가 발생하지 않았다.

 

 

영속 엔티티의 동일성 보장


Member a = em.find(Member.class, "member1");   
Member b = em.find(Member.class, "member1"); 
System.out.println(a == b); // 동일성 비교 true 

자바 컬랙션에서 같은 레퍼런스가 있는 객체를 꺼내면, == 비교가 가능한 것 처럼... 영속성 컨택스트에서 객체를 꺼낼 경우, 같음을 보장한다. 이는 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공해 주기 때문이다. 즉, JPA의 1차 캐시는 그 트랜잭션이 완료될 때까지 SELECT 문장이 사용하는 모든 데이터에 read lock을 통해 데이터의 동일성을 보장한다.

반복할 수 없는 읽기(nonrepeatable read)
- 트랜잭션이 같은 질의를 두 번 이상 수행할 때 서로 다른 데이터를 얻게 되는 문제
- 보통 각 질의 수행 사이에 동시 진행 중인 다른 트랜잭션에서 이 데이터를 변경하는 경우에 발생함.

 

엔티티 등록 
트랜잭션을 지원하는 쓰기 지연


EntityManager em = emf.createEntityManager(); 
EntityTransaction transaction = em.getTransaction(); 

//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다. 
transaction.begin();  // [트랜잭션] 시작 
em.persist(memberA); 

memberA 를 1차 캐시에 저장하고, 동시에 memberA를 분석하여 INSERT 쿼리를 생성하여 쓰기 지연 SQL 저장소에 저장한다.

persist 호출 시, 영속성 컨택스트의 내부 모습


em.persist(memberB); 
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다. 

//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다. 
transaction.commit(); // [트랜잭션] 커밋

persist 호출 시, 영속성 컨택스트의 내부 모습

memberB 를 1차 캐시에 저장하고, 동시에 memberB를 분석하여 INSERT 쿼리를 생성하여 쓰기 지연 SQL 저장소에 또 저장한다.

commit 호출 시, 영속성 컨택스트의 내부 모습

이렇게 쓰기 지연 SQL 저장소에 쌓인 쿼리는 transaction.commit() 시에 flush (디비에 반영) 되며 쓰기 지연 SQL 저장소에서 삭제된다.

 

이렇게 데이터베이스에 한 번에 쿼리가 나감으로써 얻을 수 있는 이점은

  • SQL을 최적화 할 수 있다.
  • JDBC Batch 기능을 이용해 트랜잭션 커밋 직전에 한번의 네트워크로 데이터베이스에 모아둔 여러 쿼리를 반영할 수 있다.
    하이버네이트의 경우 hibernate.jdbc.batch_size를 설정하여, 그 사이즈를 정할 수 있다.

변경 감지 (Dirty Checking)


Member member = em.find( Member.class, 150L );
member.setName( "AAAA" );

// database 저장
tx.commit();

JPA에서는 엔티티를 em.find(entity)를 통해 가져오면, 그 객체에 대한 동일성을 보장하기 때문에

객체를 변경하는 경우, 데이터베이스에 변경 사항을 반영하기 위한 (UPDATE/DELETE 쿼리를 생성하기 위한) 추가 작업(em.update(entity) 와 같은)을 하지 않아도 된다.

 

❓ 어떻게

JPA는 변경감지 기능이 제공되기 때문에!

 

트랜잭션이 commit되는 시점에 내부적으로 flush() (그림의 1번)가 호출되게되는데,

이때, JPA는 내부적으로 엔티티와 스냅샷을 비교한다. 만약, 스냅샷에서 변경된 부분이 있다면, UPDATE 쿼리를 생성하여 쓰기 지연 SQL 저장소에 저장한다. 이렇게 쌓인 모든 쿼리는 commit 시에 database에 반영한다.

이러한 내부 매커니즘 덕분에 개발자는 앤티티의 변경 사항에 대한 추가 작업을 하지 않아도 된다.

스냅샷: (데이터베이스에서 값을 읽어왔을 때나) 최초로 영속성 컨택스트에 들어왔을 때의 초기값 
반응형

'Spring > JPA & Hibernate' 카테고리의 다른 글

준영속 상태  (0) 2019.12.18
플러시  (0) 2019.12.18
JPA Example  (0) 2019.12.18
h2 database 사용하기  (0) 2019.12.17
고급 매핑  (0) 2019.12.17