EntityManagerFactory, EntityManager #
(보통) EntityManagerFactory를 생성할 때, 커넥션 풀을 만든다.
EntityManager는 EntityManagerFactory 에서 생성한다.
EntityManager는 데이터베이스 연결이 꼭 필요한 시점(like transaction)까지 커넥션(connection, conn)을 얻지 않는다.
영속성 컨텍스트란? #
‘엔티티를 영구히 저장하는 환경’ 이다. (EntityManager 로) entity 를 저장, 조회하면 EntityManager 는 영속성 컨텍스트에 entity 를 보관/관리한다. EntityManager 를 생성하면 ‘영속성 컨텍스트’ 라는 것이 (한 개) 같이 생성된다. EntityManager 를 통해 영속성 컨텍스트에 접근,관리할 수 있다.
즉, 애플리케이션과 DB 사이에서 객체를 보관하는 가상의 데이터베이스와 같은 역할을 한다. 영속성 컨텍스트 덕분에 1차 캐시, 동일성 보장, 쓰기 지연, 변경 감지, 지연 로딩 등의 기능을 사용할 수 있게 된다.
em.persist(member);
위의 코드가 의미하는 것은 다음과 같다.
" EntityManager 를 사용해서 member 엔티티를 영속성 컨텍스트에 저장한다. (간단하게는, member 엔티티를 저장한다.)"
Entity 의 생명주기 #
Entity 에는 4가지의 상태(생명주기)가 있다.
- 비영속 상태(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속 상태(managed) : 영속성 컨텍스트에 저장된 상태
- 준영속 상태(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제 상태(removed) : 삭제된 상태
준영속(detached)
↑
|
|
|
비영속(new) ------> 영속(managed) <------> DB
| ↑
| |
| |
↓ |
삭제(removed) ----------
비영속 상태
객체(엔티티)를 생성했을 때이다. (영속성 컨텍스트, DB 등과 전혀 관련이 없는) 순수한 Java 객체이다.
영속 상태
EntityManager 를 통해 객체(엔티티)를 영속성 컨텍스트에 저장했을 때이다. ‘영속성 컨텍스트에 저장한다’는 것은 ‘EntityManager(영속성 컨텍스트)가 관리하는 엔티티’라는 것을 의미한다.
준영속 상태
영속성 컨텍스트가 관리하던 객체(엔티티)가 더 이상 관리하지 않으면 준영속 상태가 된다. 영속성 컨텍스트를 닫거나(em.close()
), 초기화하거나(em.clear()
), 해당 엔티티를 명시적으로 분리하거나(em.detach()
) 하면 준영속 상태가 된다.
(준영속 상태에 관하여 조금 더 깊게 알아볼 필요가 있다.)
삭제 상태
객체(엔티티)를 영속성 컨텍스트(DB)에서 삭제한다.
영속성 컨텍스트의 특징 #
1. 식별자 값이 반드시 있어야 한다.
영속성 컨텍스트는 (관리하는)엔티티를 식별자 값(@Id
)으로 구분한다. (식별자 값이 없으면 예외 발생한다.)
2. flush : (영속성 컨텍스트에서 관리되고 있는) 엔티티의 내용이 실제 데이터베이스에 저장되는 시점이다.
* 영속성 컨텍스트가 엔티티를 관리하여 얻는 장점은 무엇이 있을까?
- 1차 캐시
- 동일성 보장
- 쓰기 지연 (트랜잭션 지원)
- Dirty checking (변경 감지)
- 지연 로딩
인메모리 상에서 객체를 관리하여 얻는 장점(DB와의 접촉 자제)들인 것 같다. 객체로써 관리할 수 있다는 장점도 자연스럽게 이어지는 것 같다.
1차 캐시
영속성 컨텍스트는(?, EntityManager는 이라고 하면 안되는건가(?)) 내부에 캐시(cache)를 가지고 있다. 이를 1차 캐시라고 한다. 1차 캐시는 영속상태의 엔티티가 저장되는 곳이다. (영속성 컨텍스트 내부에 Map 객체
가 있다. Key는 @Id
의 값이고, Value는 엔티티 인스턴스이다.)
엔티티를 저장(save)하면 1차 캐시에 저장되는 것이고, 엔티티를 조회(find)하면 먼저 1차 캐시에서 찾고, (1차 캐시에 없으면) DB에서 찾는 것이다.
만약 조회하고자 하는 엔티티가 1차 캐시에 없다면, DB에서 조회해올 것이다. DB에서 조회한 후에 1차 캐시에 저장(영속 상태로 관리)하고 이 객체(엔티티)를 반환한다.
(위의 문장에서)엄밀히 구분하면 ‘영속성 컨텍스트는’이 아니라 ‘EntityManager는’ 이 더 적절한 것은 아닌가? (영속성 컨텍스트 == EntityManager 와 같이 동일선상에서 보는 건가?)
+ EntityManager 는 영속성 컨텍스트를 사용하기 위한 인터페이스라고 볼 수 있을 것 같다. ( https://docs.oracle.com/javaee/7/api/javax/persistence/EntityManager.html) 편의상(?) 영속성 컨텍스트 == EntityManager 라는 동일선상에서 보는 것 같다.(?)
+ 1차 캐시를 통해 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다는 장점이 있다. ( = 이해할 것!)
동일성 보장
1차 캐시에 있는 같은 엔티티를 반환 == 같은 객체 == 동일성 보장
Member member1 = em.find(Member.class, "member1");
Member member2 = em.find(Member.class, "member1");
System.out.println(a == b); // 참(true)
* 동일성 : 실제로 인스턴스(참조 값)가 같다. 즉,
==
의 값이 참이다.
* 동등성 : 인스턴스가 갖고 있는 값이 같다.equals()
메서드의 값이 참이다. (equals()
가 구현되었다는 가정하에)
쓰기 지연
- 트랜잭션을 커밋(commit)하기 전까지 데이터베이스에 쿼리를 날리지 않는다. 즉, 데이터베이스에 엔티티(값)을 저장하지 않는다.
- 커밋하기 전까지는 내부의 쿼리 저장소에 쿼리문을 쌓아둔다.
- 트랜잭션을 커밋하면, 쌓아둔 쿼리를 DB에 보낸다.
이 과정(개념)을 트랜잭션을 지원하는 쓰기 지연이라고 한다.
(영속성 컨텍스트, EntityManager에서) 트랜잭션을 커밋(commit) == 영속성 컨텍스트를 flush == 영속성 컨텍스트의 내용을 DB에 동기화하는 작업(즉, 삽입, 수정, 삭제의 내용을 반영) == 내부에 쌓아둔 쿼리를 DB에 보낸다.
이후 실제 DB의 트랜잭션을 커밋(commit)한다.
쓰기 지연이 가능한 이유는, “트랜잭션” 덕분이다. DB에 쿼리를 날리는 것은 결국에 (해당하는)트랜잭션이 끝나기 전에만 쿼리를 날리면 된다. 그렇기에 DB에 쿼리를 보내는 것은 일찍 보내든, 늦게 보내든 트랜잭션이 끝나기 전에만 보내지면 된다.
Dirty Checking(변경 감지)
공감되는 문구가 있다. 기존 개발 방식(Update 쿼리를 작성하는 것)의 문제점은 비슷비슷한 수정 쿼리가 많아진다는 것, 많아짐에 따라 (분석을 위해) 계속해서 확인해야하는 것, (+ Query에 의존하게 된다는 것)
(JPA)는 엔티티를 영속성 컨텍스트에 보관(관리를 시작)할 때, 최초의 상태를 복사하여 저장해둔다. 이것을 스냅샷이라고 한다. 이후 플러시 시점에 해당 엔티티와 스냅샷의 값을 비교해서 변경된 엔티티(혹은 부분/값)을 찾는다.
* 당연하게도, 변경 감지는 영속성 컨텍스트가 관리하는 엔티티에만 적용이 된다. 비영속, 준영속 상태의 엔티티는 적용되지 않는다.
흐름은 다음과 같다.
- flush()
- 엔티티와 스냅샷 비교하여 변경된 엔티티를 찾는다.
- 변경된 엔티티가 있다면, update 쿼리를 생성하여 쓰기 지연 sql 저장소에 보관한다.
- sql 저장소에서 DB로 쿼리를 보낸다.
- DB 트랜잭션을 커밋한다.
변경 감지로 인해 생성된 Update 쿼리는 모든 필드에 대한 update 쿼리를 생성하여 보낸다.
예시는 다음과 같다.
// (name 만 변경했을 때) 우리가 예상한 쿼리는 아래와 같을 것이다.
update member
set name = ?
where
id = ?
실제 발생하는 쿼리는 모든 필드에 대한 쿼리가 발생한다.
update member
set name = ?,
age = ?,
grade = ?,
...
where
id = ?
이렇게 모든 필드에 대한 쿼리가 발생하면 (데이터 전송량이 증가되니)단점이 아닌가? 라는 생각이 들 수 있다. 하지만 아래와 같은 장점이 더욱 많다고 한다.
- 수정 쿼리가 항상 같다. (동일한 form 이다.) 따라서 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있다.
- 데이터베이스 관점에서 동일한 쿼리(form)라면 이전에 한 번 파싱된 쿼리를 빠르게 재사용할 수 있다고 한다.
+ 필드가 너무 많거나, 수정하고자 하는 데이터가 너무 크다면(즉, 데이터 전송량이 너무 클 것 같다면) 동적으로 Update 쿼리를 생성하는 전략을 사용할 수도 있다.
+ 상황에 따라 다르지만, 컬럼(필드)이 대략 30개 이상이면 @DynamicUpdate
를 사용하는게 더 빠르다고 한다. (정확한 것은 직접 테스트해보는 것이다.)
+ 추천하는 것은 일단 정적 update 쿼리를 사용하고, 튜닝이 필요하다고 느낄 때 테스트 -> 사용 하는 것이다.
@Entity
@org.hibernate.annotations.DynamicUpdate <-- 이것!
@Table(name = "Member")
public class Member { ... }
엔티티 삭제
엔티티를 삭제하려면 먼저 대상 엔티티를 조회해야 한다. 삭제 역시 ‘쓰기 지연 SQL 저장소’ 에서 query가 관리되고, 커밋(flush)시점에 쿼리를 DB에 전달한다.
다만, 엔티티를 삭제하면(em.remove(entity)
) 그 즉시 영속성 컨텍스트에서 제거(관리되지 않음)된다.
이렇게 삭제된 엔티티는 재사용하지 말고 GC에 의해 자연스럽게 제거될 수 있도록 두는 것이 좋다.
플러시(Flush) #
* 플러시라는 이름으로 인해 영속성 컨텍스트에 보관된 엔티티를 지운다고 생각하면 안된다고 한다.
* 영속성 컨텍스트의 변경 내용을 DB에 동기화하는 것이 ‘플러시’이다. * 데이터베이스와의 동기화는 최대한 늦춘다. (가능한 이유 = 트랜잭션이라는 작업 단위가 있기 때문이다.)
플러시는(flush)는 영속성 컨텍스트의 내용(변경 내용)을 데이터베이스에 반영하는 것이다.
구체적으로는 다음의 과정이 수행된다.
- 변경 감지 동작 -> 영속성 컨텍스트에 있는 모든 엔티티들에 대하여 스냅샷 비교 -> 변경(수정)된 엔티티를 찾는다. 수정된 엔티티에 대해서는 update 쿼리를 만들어 ‘쓰기 지연 sql 저장소’에 쿼리를 등록한다.
- 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송한다. (삽입, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시하는 방법은 3가지이다.
em.flush()
직접 호출- 트랜잭션 커밋 시 flush 자동 호출
- jpql(criteria 등) 쿼리 실행 시 flush 자동 호출 (* 이 부분은 기억해두자! )
jpql 쿼리 실행 시 flush 가 자동으로 호출되는 이유
em.persist(member1);
em.persist(member2);
em.persist(member3);
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
(1차 캐시에서 가져오는 것이 아니기 때문에) 만약 jpql 실행 전에 flush가 되지 않는다면, jpql(sql)을 실행 시 member1, 2, 3이 조회되지 않을 것이다.
(jpql 과 같이) 이렇게 sql 을 직접 수행하는 것에는 다 동일한 개념일 것이다.
플러시 모드 옵션
EntityManager 에 플러시 모드를 직접 지정할 수 있다.
- FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시 (default)
- FlushModeType.COMMIT : 커밋할 때만 플러시 (간혹 성능 최적화를 위해 사용될 수 있다고 한다.)
준영속 #
영속성 컨텍스트에서 관리하던 엔티티가 분리된 것을 준영속 상태라고 한다. 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능(1차 캐시, 쓰기 지연 SQL 저장소, dirty checking 등)을 사용할 수 없다.
준영속 상태가 되는 순간 1차 캐시
, 쓰기 지연 SQL 저장소
에서 해당 엔티티와 관련된 모든 정보가 제거된다.
준영속 상태로 만드는 방법은 3가지이다.
em.detach(entity)
: 특정 엔티티를 준영속 상태로 전환한다. 해당 엔티티와 관련된 1차 캐시, 쓰기 지연 SQL 저장소의 정보가 제거된다.em.clear()
: 영속성 컨텍스트를 완전히 초기화한다. 즉, 1차 캐시, 쓰기 지연 SQL 저장소 등의 모든 정보가 초기화된다.em.close()
: 영속성 컨텍스트를 종료한다.
* 주로 영속성 컨텍스트가 종료되면서 영속 상태 -> 준영속 상태의 엔티티가 된다고 한다. 개발자가 직접 준영속 상태로 만드는 일은 드물다고 한다.
준영속 상태의 특징
- 비영속 상태와 거의 동일하다.
영속성 컨텍스트가 관리하지 않으므로 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 등의 어떠한 기능도 제공(동작)되지 않는다. - 식별자 값을 가지고 있다.
비영속 상태는 식별자 값이 있을 수도, 없을 수도 있다. 하지만 준영속 상태는 이미 한번은 영속 상태에 있었기에, 반드시 식별자 값을 가지고 있다. (식별자가 있다. = DB에 row가 있다. = 다시 영속 상태로 돌아올 때, 이전에 어떤 엔티티였는지 구분이 가능하다.) - 지연 로딩이 불가능하다.
지연 로딩은 실제 객체 대신 프록시 객체를 로딩해두고, 해당 객체가 실제로 사용될 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법이다.
준영속 상태에서는 영속성 컨텍스트가 더 이상 관리하지 않으므로 지연 로딩 시 문제가 발생된다.
병합(merge()
)
비영속/준영속 상태의 엔티티를 다시 영속 상태로 변경하려면 ‘병합(merge()
)’ 을 사용한다.
병합(merge()
) 은 비영속/준영속 상태의 엔티티를 받아서, 그 정보로 새오운 영속 상태의 엔티티를 반환한다.
비영속 상태에서 병합할 때, 1차 캐시나 DB에 찾고자 하는 엔티티가 없다면 새로운 엔티티를 생성하는 개념이다.
준영속 상태에서 병합할 때는, (무조건) DB에 찾고하는 엔티티가 있다. 그 엔티티를 1차 캐시로 조회해오고, 병합하는 개념이다.