-
[JPA] 엔티티 매니저, 영속성 컨텍스트, flush()JPA 2024. 1. 1. 17:14
Entity manager
- 엔티티를 저장하고, 수정하고, 삭제하고, 조회하는 등 엔티티와 관련된 모든 일을 처리한다. 즉 엔티티를 관리하는 관리자이다.
- 엔티티 매니저 팩토리에서 생성된다.
- 개발자 입장에서 엔티티 매니저는 엔티티를 저장하는 가상의 데이터베이스로 생각하면된다.
- 보통 repository에서 다음과 같이 생성하고 사용한다.
private final EntityManager em; em.persist(member) em.merge(member) em.find(Member.class, id)
영속성 컨텍스트
⇒ 영속성 컨텍스트란 엔티티를 영구 저장하는 환경이다.
- 엔티티 매니저로 엔티티를 저장하거나 조회하면, 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
- 영속성 컨텍스트는 매니저를 생성할 때 하나 만들어진다. 엔티티 매니저를 통해 접근할 수 있고 관리할 수 있다.
- 영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할을 한다. → 1차캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩 기능 등을 사용할 수 있다.
- ex) em.persist() : 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장
엔티티의 생명주기
엔티티에는 4가지 상태가 존재한다
비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
영속(managed): 영속성 컨텍스트에 저장된 상태
준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
삭제(removed) : 삭제된상태
비영속
- 엔티티 객체를 생성했다. 아직 저장하지 않았으므로 영속성 컨텍스트나 데이터베이스와는 전혀 관련이 없다.
Member member = new Member; member.setId("member1"); member.setUsername("회원1");
영속
- em.persist(member) : 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장했다. 이렇게 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라 한다.
- em.find(Member.class, “member1”)나 JPQL을 사용해서 조회한 엔티티도 영속성 컨텍스트가 관리하는 영속상태이다.
준영속
- 영속성 컨텍스트가 관리하지 않는 상태이다.
- em.detch(member), em.close(member), em.clear(member)등을 통해서 회원 엔티티를 영속성 컨텍스트에서 분리된 준영속 상태로 만들 수 있다.
삭제
- 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다
- em.remove(member)
영속성 컨텍스트의 특징
영속성 컨텍스트와 식별자 값
영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분한다. 따라서 영속 상태는 식별자 값이 반드시 있어야 한다.
영속성 컨텍스트와 데이터베이스 저장
- 영속성 컨텍스트에 엔티티를 저장하면 이 저장된 엔티티는 JPA가 트랜잭션을 커밋하는 순간 데이터베이스에 새로 반영 된다. → 새로 저장된 엔티티를 데이터베이스에 반영하는 것을 플러시 라 한다.
- 즉, 영속성 컨텍스트에 저장된 엔티티는 플러시 시점에 데이터베이스에 반영된다. 일반적으로 트랜잭션을 커밋할 때 영속성 컨텍스트가 플러시 된다.
영속성 컨텍스트가 엔티티를 관리하면 얻게되는 장점
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 ‘쓰기 지연’
- ‘변경 감지’
- 지원 로딩
CRUD 로 알아보는 entity
1. 엔티티 조회 - find()
영속성 컨텍스트는 내부에 캐시를 갖고 있는데, 이를 1차 캐시라 한다. 영속 상태의 엔티티는 모두 이 1차 캐시에 저장된다. ⇒ 쉽게 설명하면,, 영속성 컨텍스트 내부에 Map이 하나 있는데 key: @Id로 매핑한 식별자 value: 엔티티 인스턴스 로 이해하면 되겠다.
- Member member = new Member; //엔티티 생성
- member.setId(”member1”) //id값 저장
- em.persist(member) //영속성 컨텍스트에 영속
- 이시점에 영속 컨텍스트 1차 캐시에 key-value형태로 (@Id-Entity = “member1” - member1 ) 영속됨
- em.find(Member.class, “member1”)만약 em.find(Member.class,”member2”)와 같이 1차 캐시에 없는 엔티티를 조회하는경우, 엔티티 매니저는 db를 조회 → 엔티티를 생성 → 1차캐시에 저장 → 영속 상태 엔티티 member2를 반환한다.
- 1차 캐시에 엔티티가 있으면 엔티티 매니저는 영속 상태 엔티티인 member1을 반환한다.
1차 캐시
- 영속성 컨텍스트 사용으로, 엔티티들을 조회하면 메모리에 있는 1차 캐시에서 불러오기때문에 성능상 이점을 누릴 수 있다.
영속 엔티티의 동일성 보장
- 영속성 컨텍스트는 반복해서 조회해도, 1차 캐시에 있는 같은 엔티티 인스턴스를 반환한다. 따라서 동일성 성립
Member a = em.find(Member.class, "member1"); Member b = em.find(Member.class, "member1"); // a==b (member1 == member1)
2. 엔티티 등록 - persist()
public class mine { EntityManager em = emf.createEntityManger; EntityTransaction transaction = em.getTransaction(); //엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다. transaction.begin(); em.persist(memberA); em.persist(memberB); //여기까지 INSERT SQL를 데이터베이스에 보내지 않는다. //커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다. transcation.commit(); }
- 엔티티 매니저는 “트랜잭션을 커밋하기 직전” 까지 데이터베이스에 엔티티를 저장하지 않고, 내부 쿼리 저장소에 INSERT SQL을 차곡차곡모아둔다.
- 그리고 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보내는데, 이것을 트랜잭션을 지원하는 ‘쓰기 지연’이라 한다. → 쓰기 지연을 통해 모아둔 쿼리를 데이터베이스에 한번에 전달하면서 성능 최적화!!
<트랜잭션 커밋 과정>
- 엔티티 매니저는 영속성 컨텍스트를 플러시한다. 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화 하는 작업인데 이때 등록, 수정, 삭제한 엔티티를 데이터베이스에 반영한다. 다시 말하면, 쓰기 지연 SQL저장소에 모인 쿼리를 데이터베이스에 반영한다.
- 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 flush 이후에, 실제 데이터베이스 트랜잭션을 커밋한다.
3. 엔티티 수정 - commit시 자동으로
기존 SQL수정 쿼리의 문제점
SQL을 사용하면, 수정 쿼리를 직접 장성해야한다. ex) update member set name=?
하지만, 이렇게 직접 수정 쿼리를 작성하는 방식은, 수정 쿼리가 많아지는 것은 물론이고 비즈니스 로직을 분석하기 위해 SQL을 계속 확인해야한다. 결국 비즈니스 로직이 SQL에 의존하게 만든다.
JPA의 엔티티 수정
⇒ JPA로 엔티티를 수정할 때는, 단순히 엔티티를 조회해서 데이터만 변경하면 된다.
EntityManager em = emf.createEntityManger; EntityTransaction transaction = em.getTransaction(); //엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다. transaction.begin(); //영속 엔티티조회 Member memberA = em.find(Member.class, "memberA"); //영속 엔티티 데이터 수정 memberA.setUsername("Hi"); memberA.setAge(10); //커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다. -> 엔티티 수정됨 transcation.commit();
이렇게 엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 “변경 감지”라 한다.
- JPA는 엔티티를 영속성 컨텍스트에 보관할때, 최초 상태를 복사해서 저장해둔다. 이를 ‘스냅샷’이라 한다.
- 그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다. (
- 변경 감지로 인해 실행된 UPDATE SQL문은 변경된 부분만 수정되는 것이 아니라 모든 필드를 업데이트 한다.
<순서>
- 트랜잭션을 커밋하면 엔티티 매니저 내부에서 flush() 호출
- 엔티티와 스냅샷을 비교하여 변경된 엔티티 찾음
- 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL저장소에 보냄
- 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
- 데이터베이스 트랜잭션을 커밋한다.
4. 엔티티 삭제 - 조회 후, .remove()
엔티티를 삭제하려면 먼저 대상 엔티티를 조회해야 한다.
Member memberA = em.find(Member.class,"memberA"); //삭제 대상 조회 em.remove(memberA); //엔티티 삭제
em.remove(memberA)를 호출하는 순간 memberA는 영속성 컨텍스트에서 제거된다.
DB 에서는?? ⇒ 물론 엔티티 삭제 역시, 즉시 db에서 삭제되는 것이 아니라 삭제 쿼리를 쓰기 지연 SQL 저장소에 등록해두었다가, 이후 트랜잭션을 커밋해서 플러시를 호출하면 실제 데이터베이스에 삭제 쿼리를 전달한다.
flush()
💡 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화 하는 것
플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
JPA가 transaction을 commit하는 순간에 flush()가 제일 먼저 일어난다. 이 과정을 좀 더 자세히 알아보자..
flush() 실행시
‘변경 감지’가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교하고 수정된 엔티티를 찾는다.
- 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL저장소에 등록한다.
- 쓰기 지연 SQL저장소의 쿼리를 데이터베이스에 전송한다.
영속성 컨텍스트를 플러시하는 방법
- em.flush() 직접 호출 → 거의 사용하지 않는 방식
- 트랜잭션 커밋시 자동 호출
- JPQL쿼리 실행 시 자동 호출
더보기JPQL쿼리 실행 시 왜 자동호출되는가? em.persist()를 호출해서 memberA를 영속상태로 만들었다고 가정하자. 이 엔티티는 영속성 컨텍스트에는 있지만 아직 db에 반영되지 않았다. 이때 JPQL을 실행하면 JPQL은 SQL로 변환되어 데이터베이스에서 엔티티를 조회하게된다. 그런데 memberA는 아직 db에 없으므로 조회되지 않는다. 따라서 JPQL을 실행하기 전에도 flush()를 자동 호출하는 것이다
준영속
영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된것을 준영속 상태라 한다. 따라서 준영속 상태에서는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
영속 상태의 엔티티를 준영속 상태로 만드는 방법
- em.detach(entity) : 특정 엔티티만 준영속 상태로 전환
- em.clear() : 영속성 컨텍스트를 완전히 초기화
- em.close() : 영속성 컨텍스트를 종료
em.detach()
특정 엔티티를 준영속 상태로 만든다.
em.detach()를 호출하는 순간 1차 캐시부터 쓰기 지연 SQL저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거된다. 이 상태를 준영속 상태라 한다. 쓰기 지연 SQL저장소의 INSERT SQL도 제거되어서 데이터베이스에 저장되지도 않는다.
EntityManager em = emf.createEntityManger; EntityTransaction transaction = em.getTransaction(); //엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다. transaction.begin(); //회원 엔티티 영속상태 em.persist(member); //회원 엔티티 준영속 상태 em.detach(member) //커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다. transcation.commit();
em.clear()
영속성 컨텍스트를 초기화해서, 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다.
em.close()
영속성 컨텍스트를 종료한다. 영속성 컨텍스트를 종료하면 해당 영속성 컨텍스트가 관리하던 영속 상태의 엔티티가 모두 준영속 상태가 된다.
EntityManager em = emf.createEntityManger; EntityTransaction transaction = em.getTransaction(); //엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다. transaction.begin(); //회원 엔티티 영속상태 em.persist(member); //회원 엔티티 준영속 상태 em.detach(member) //커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다. transcation.commit(); //모든 엔티티 준영속 상태 em.close()
준영속 상태의 특징
- 거의 비영속 상태에 가깝다 : 영속성 컨텍스트가 관리하지 않으므로 1차캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 어떠한 기능도 동작하지 않는다.
- 식별자 값을 가지고 있다 : 준영속 상태는 이미 한 번 영속 상태였으므로 반드시 식별자 값을 갖고 있다.
- 지연로딩 x : 지연 로딩은 실제 객체 대신 프록시 객체를 로딩해두고, 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법인데, 이 동작이 불가능해진다.
merge()
💡 준영속 상태의 엔티티를 다시 영속 상태로 변경하는 방법
⇒ 그러나 실제로는, 기존 entity가 준영속에서 영속 상태로 변경되는 것이 아니라, memberMember라는 새로운 영속상태의 인스턴스를 반환하는것. 이 둘은 서로 다른 인스턴스 ( 준영속 엔티티를 참조하던 변수를 영속 엔티티를 참조하도록 바꾸는 것이다.)
Member mergeMember = em.merge(member)
병합은 비영속 엔티티도 같은 원리로 영속 상태로 만들 수 있다.
이상.. 김영한 JPA실전 강의 보다가 이게 뭔소린가 하고 책을 펼쳤다가 큰 배움을 얻었습니다.
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=62681446
자바 ORM 표준 JPA 프로그래밍
에이콘 오픈 소스 프로그래밍 시리즈. 이 책은 JPA 기초 이론과 핵심 원리, 그리고 실무에 필요한 성능 최적화 방법까지 JPA에 대한 모든 것을 다룬다.
www.aladin.co.kr