엔티티 매니저는 엔티티를 저장하고,수정하고, 삭제하고, 조회 하는 등 엔티티와 관련된 모든 일을 처리한다. 이름 그대로 엔티티를 관리하는 관리자다.
앞장에서 패러다임의 불일치를 다루면서 계속해서 "데이터를 DB가 아닌, 컬렉션에 저장 했더라면? " 과 같은 내용을 언급하면서 컬렉션저장과 DB저장을 비교했던 것이 기억나는가?
사실 이건 지금 배울 엔티티 매니저를 위한 떡밥이였다. 이제 JPA를 사용하게 되면 개발자는 더 이상 SQL을 작성해 직접 데이터를 DB에 저장할 필요가 없고 마치 컬렉션에 객체를 집어넣듯이 데이터를 저장할 수 있게 된 것이다. 그리고 여기서 컬렉션 역할을 바로 엔티티 매니저가 하게되는것이다.
즉, 개발자 입장에서 엔티티 매니저는 엔티티를 저장하는 가상의 데이터베이스 쯤으로 생각하면 된다.
엔티티 매니저는 엔티티 매니저 팩토리에서 인스턴스가 생성된다.
[엔티티 매니저 팩토리]
1) 엔티티 매니저 팩토리는 생성되는 비용이 굉장히 크다.
2) 그래서 애플리케이션 실행시 딱 1번만 생성이 되며
3) 생성 될 때
persistence.xml의 설정 정보를 읽어들여서 그걸 토대로 생성이 된다.4) 생성된 팩토리는 어플리케이션 전체에서 공유가 되며 내부적으로는 멀티스레딩 환경에서 굉장히 안정적이고 안전하게 동작되도록 구현이 되어있다.
5) 마지막으로 이름에서 알 수 있다시피 팩토리에서 엔티티 매니저가 생성이된다.
[엔티티 매니저]
1) 엔티티 매니저는 생성 비용이 굉장히 적다.
2) 대신 엔티티 매니저는 절대 다른 쓰레드와 공유되어서는 안 된다. 그림에서도 확인 할 수 있다시피 요청 별로 하나의 엔티티 매니저가 할당이 된다.
3) 그리고 트랜잭션이 시작되기 전까지 커넥션을 생성하지 않으며 시작시에만 커넥션을 사용한다.
※ 스프링 복습!
스프링에서는 엔티티 매니저를 빈으로 등록해 싱글턴으로 관리하는데 서로 공유 되지 않는 이유?
-> 실제로는 스프링이 엔티티 매니저의 프록시 객체를 주입해주기 때문.
우리는 이제 엔티티 매니저가 엔티티에 관한 모든걸 관리한다는것을 알았다. 그럼 이제 영속성 컨텍스트라는 개념을 알아볼 차례이다.
영속성 컨텍스트란 엔티티를 영구적으로 저장하는 환경 혹은 저장소를 뜻한다.
사실 DB의 경우 안전한 트랜잭션 처리를 보장하기위해 지켜야할 4가지 특성들(ACID) 이 있는데 그 중 하나가 바로 영속성(Durability) 이다.
영속성이란 트랜잭션이 성공적으로 완료했다면 그 결과가 영구적으로 반영되어야 함 을 의미한다.
즉, 엔티티 매니저는 단위 트랜잭션 동안 엔티티 객체를 자신의 영속성 컨텍스트에 저장한 다음, 엔티티 객체의 상태를 지속적으로 추적하고 유지하며 관리 함으로써 트랜잭션의 영속성을 보장해주는 것이다. 영속성 컨텍스트는 엔티티 매니저가 생성 될 때 하나만 만들지며 그렇기 때문에 엔티티 매니저는 절대 다른 쓰레드나 사용자와 공유되어서는 안 된다.
(※ 물론 여러 개의 매니저가 하나의 영속성 컨텍스트에 접근도 가능하게 할 순 있지만 해당 내용은 좀 뒷 편에서 다루게된다.)


detach() 를 통해 준영속상태가되고 merge() 로 다시 병합된다. 그런데 실무에서 직접 detach() 할 일은 거의 없다.// 회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
// 객체를 삭제한상태 (삭제)
em.remove(member);
반드시 엔티티객체는 식별자(@Id) 를 가지고 있어야한다.
이는 어찌보면 당연한데 저장소 역할을 하는 영속성 컨텍스트는 각 엔티티들을 구분하기 위해서는 식별자가 필요하다.
캐시를 사용해 엔티티들을 보관한다
영속 컨텍스트 내부에는 Map 구조의 캐시 저장소가 존재하며 이곳에서 모든 엔티티들을 보관한다.
flush() 를 통한 데이터 베이스 반영
영속성 컨텍스트에 저장된 데이터는 매니저가 commit 할 때 flush() 가 호출 되며 데이터 베이스에 변경 된 값들이 반영된다. 자세한 내용은 뒤에 설명한다.
앞서 언급했듯이 영속 컨텍스트에는 캐시가 존재하고 이곳에서 엔티티들을 저장한다.
이 1차 캐시 덕분에 우리는 총 2가지 이점을 누릴 수 있다.
성능상 이점
System.out.println("=====첫번째 조회=========="); Member memberA = em.find(Member.class, 1L); System.out.println("=====두번째 조회=========="); Member memberB = em.find(Member.class, 1L);1) 첫번째 조회 의 경우 캐시에 아무런 엔티티가 없기 때문에 db에
select쿼리를 보내 가져온다.
2) 그 후 캐시에 조회한 엔티티를 저장해놓는다.
3) 그리고 두번재 조회 에서는 이번엔 db가 아닌 캐시에서 해당 엔티티를 반환해준다.![]()
동일성 보장
이전에서 언급했듯이 패러다임 불일치로 발생한 문제중 하나였던 동일성 보장 문제를 해결해준다.
같은 인스턴스를 반환해주기 때문에 위 코드 같은 경우memberA == memberB이다.![]()
엔티티 매니저는 영속성 컨텍스트에 엔티티들을 저장 할 때 동시에 DB에 보낼 INSERT 쿼리문을 SQL 저장소에 차곡차곡 모아둔다. 그리고 commit 한 순간에 모아두었던 SQL을 한번에 전송 (flush) 해버린다.
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
em.persist(memberA);
em.persist(memberB);
System.out.println("====== 커밋 전 ======");
tx.commit();
System.out.println("====== 커밋 후 ======");
} catch (RuntimeException e) {
System.out.println("롤백 실행");
tx.rollback();
}finally {
em.close();
}
emf.close();
}
}
엔티티 삭제 또한 동일한 방식으로 실제 commit 전까지 영속성 컨텍스트에 남아있다가 commit 이 된 후에 DB와 캐시에서 모두 제거가 된다.
이전에 SQL 중심 개발에서는 데이터를 변경/수정 해야 할 경우 직접 UPDATE 쿼리문을 작성해줬어야했다.
하지만 만일 엔티티의 컬럼이 여러개이며 또 모든 컬럼들이 수정 가능한 경우, 모든 경우에 따른 UPDATE 쿼리문들을 작성해줬어야 했다.
하지만 JPA의 엔티티 매니저를 활용하면 그럴 필요가 없어진다. 우리가 컬렉션에 저장된 객체의 상태값을 바꿀때처럼 그냥 영속 컨텍스트에 저장된 객체의 상태값을 setter 로 변경만해주면 끝이다.
그러면 엔티티매니저가 알아서 엔티티 값이 변경된것을 감지하고 commit 할 때 UPDATE 쿼리를 전송한다.
// 영속 엔티티 조회
Member member A = em. find (Member. class, "memberA");
// 영속엔티티 데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);
transaction. commit (); // [트랜잭션] 커밋
DirtyChecking
그럼 엔티티 매니저는 어떻게 알아서 변경을 감지하는 것일까? 바로 스냅샷 을 활용하는 것이다.
![]()
그림에서 보다시피 1차캐시에는 식별자, 엔티티 말고도 스냅샷 이란것을 저장해놓는데 이것은 엔티티가 영속성 컨텍스트에 최초로 저장될 때 미리 해당 객체를 복사해놓은 값이다.
1)
find()가 호출되었으므로 쿼리저장소의SELECT문이 전송되고 DB로부터 받은 엔티티들을 영속 컨텍스트에 저장시킨다.2) 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
3) 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
4) 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
5) 데이터베이스 트랜잭션을 커밋한다.
정적 수정 쿼리
엔티티 매니저는
UPDATE쿼리를 생성할때 모든 컬림이 들어있는 하나의 수정 쿼리만을 생성하고 전송한다.
그러면 하나의 컬럼만 수정이 될 때도 모든 컬럼에 대한UPDATE쿼리가 전송되는것이기 때문에 이는 얼핏 전송량이 늘어나서 성능에 악영향을 미치는 것 처럼 보인다.하지만, 이 방식의 경우 항상 수정쿼리가 동일 하기 때문에 미리 쿼리문을 생성해놓고 재사용할 수있다.
즉, SQL 재사용성이 증가 라는 장점이 더욱 크기 때문에 모든 필드가 담긴 하나의 쿼리만을 사용한다.물론 동적으로
UPDATE쿼리문을 생성하는 전략도 사용할 수 있다. 엔티티 클래스 위에@DynamicUpdate어노테이션을 등록해주면된다. 일반적으로 필드가 30개 이상일 경우 동적 쿼리 전략을 사용하는것이 더 빠르다고 하는데 테이블의 컬럼이 30개가 넘어간다는것은 애초에 데이터 모델링이 잘못 된것일 수도 있다,,
쭉 설명했지만 플러쉬는 SQL 저장소에 쌓인 SQL 문들을 DB로 내보내는 걸 반영시키는것을 의미한다.
플러쉬를 시키는 방식은 3가지가 있는데
1) commit
2) 직접 호출
3) JPQL 쿼리 실행 직전
이다.
사실 직접 호출을 할 일은 거의 없으며 기본적으로 commit 이나 JPQL 쿼리실행 직전에는 플러시가 자동 실행된다. 왜냐하면 개발자가 깜빡하고 플러시를 하지않고 커밋만 해버리는 경우 DB에 반영이 안 되는 문제가 발생할 수 있으므로 기본적으로 자동 플러시를 지원해준다.
커밋시에 자동 플러시는 이해가 가는데 JPQL 쿼리를 실행하면 자동으로 플러시가 실행되는 것은 납득이 안 갈 수 있다. 그렇다면 아래 코드를 확인해보자.
em.persist(memberA};
em.persist(memberB);
em.persist(memberC);
//중간에 JPQL 실행
query = em.createQuery("select m from Member m”, Member•class);
List<Member> members= query.getResultList();
커밋이 되기 전에 JPQL이 도중에 실행된 상태이기 때문에 이 경우 영속성 컨텍스트에만 엔티티가 존재하고 DB에는 아무런 데이터가 존재하지 않은 상태가 된다. 이런 일을 방지하기 위해 JPQL 이 실행되기 직전에 먼저 반영할 SQL들을 모조리 flush로 실행시켜서 DB를 최신 상태로 만들어 놓는것이다.
[주의]
플러쉬는 영속 컨텍스트를 비우는 작업이 아니고, SQL 저장소에 저장된 SQL들을 실행시킴으로써 DB와 영속 컨텍스트의 변경점을 동기화 시키는 것 이다!!
비영속 상태와 준영속 상태는 거의 비슷하지만 한가지가 다르다.
비영속 상태의 객체는 아무런 값으로도 초기화 되지 않은 갓태어난 객체일 수도 있기에 식별자가 존재하지 않을 수도 있지만,
준영속 상태의 객체는 이미 한번 영속성 컨텍스트에 저장이 되었던 객체이므로 반드시 객체 식별자가 존재한다*.
본 포스트는
김영한의 자바 ORM 표준 JPA프로그래밍 기본 강의 및 도서를 참고하여 정리했습니다.