3. 영속성 관리
1. 엔티티 매니저 팩토리와 엔티티 매니저
- 엔티티 매니저 팩토리
persistence.xml
로부터 데이터베이스 정보를 읽어와 엔티티 매니저 팩토리가 생성됨- 비용이 많이 들지만 여러 스레드가 동시에 접근해도 안전하기 때문에 한번만 생성하고 공유해서 사용해야 함
- 하이버네이트를 포함한 대부분의 구현체가 엔티티 매니저 팩토리가 생성될 때 DB의 커넥션 풀도 같이 생성해줌
- 엔티티 매니저
- 테이블 접근이 필요할 때마다 엔티티 매니저를 생성해서 테이블에 접근, 비용이 거의 들지 않음
- 동시성 문제가 있어 스레드 간에 공유하면 안됨
- 데이터베이스 연결이 필요한 시점에 커넥션을 획득함(EntityManager2)
- 연결이 필요한 시점? -> 트랜잭션을 시작할 때
2. 영속성 컨텍스트란?
- 영속성 컨텍스트(persistence context)는 엔티티를 영구적으로 저장하기 위한 논리적인 환경
em.persiste(object)
메소드를 호출하면 엔티티 매니저를 사용해서 엔티티를 영속성 컨텍스트에 저장함- 엔티티 매니저를 생성할 때 하나의 영속성 컨텍스트가 만들어짐
- 엔티티 매니저를 통해 영속성 컨텍스트에 접근, 관리가 가능함
- 여러 엔티티 매니저가 같은 영속성 컨텍스트에 접근할 수 있음
3. 엔티티 생명주기
- 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속(managed) : 영속성 컨텍스트에 저장된 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
삭제(removed) : 삭제된 상태
- 비영속(new/transient)
- 엔티티 객체를 생성하고 아직 저장하지 않은 상태
// 객체를 생성한 상태 (비영속) Member member = new Member(); member.setId(1); member.setName("name");
- 영속(managed)
- 엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장한 상태
- 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라고 함
// 객체를 저장한 상태(영속) em.persist(member); // 조회한 엔티티도 영속 상태임 Member member = em.find(Member.class, id);
- 준영속(detached)
- 영속 상태의 엔티티를 영속성 컨텍스트가 더이상 관리하지 않을 때의 상태
- 다음 세 방법으로 준영속 상태로 만들 수 있음
em.detach(member); // 엔티티를 영속성 컨텍스트로에서 분리 em.close(); // 영속성 컨텍스트를 닫음 em.clear(); // 영속성 컨텍스트를 초기화 함
- 삭제(removed)
- 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한 상태
em.remove(member);
4. 영속성 컨텍스트의 특징
- 영속성 컨텍스트와 식별자 값
- 영속성 컨텍스트는 엔티티를 식별자 값(@Id 어노테이션으로 테이블의 기본 키와 매핑한 값)으로 구분함
- 따라서 영속 상태는 식별자 값이 반드시 존재해야 함
- 영속성 컨텍스트와 데이터베이스 저장
- 트랜잭션을 커밋하는 순간 영속성 컨텍스트의 내용이 데이터베이스에 반영됨
flush
- 트랜잭션을 커밋하는 순간 영속성 컨텍스트의 내용이 데이터베이스에 반영됨
- 영속성 컨텍스트가 엔티티를 관리해서 얻는 장점
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
- 1차 캐시
- 영속성 컨텍스트가 내부적으로 가지고 있는 캐시
- 영속 상태의 엔티티는 모두 이 1차 캐시에 저장됨
- Map 자료구조 형태로 @Id를 키, 엔티티 인스턴스를 값으로 가지도록 구현되어 있음
- 아래 사진과 같이 조회 요청에 대해서 1차 캐시에 엔티티가 없을 경우, 데이터베이스에서 엔티티를 1차 캐시로 가져온 다음에 영속 상태의 엔티티를 반환함
- 영속 엔티티의 동일성 보장
Member a = em.find(Member.class, 1); Member b = em.find(Member.class, 2); System.out.println(a == b); // 동일성 비교
- 같은 엔티티를 반복해서 호출하므로 1차 캐시에서 동일한 엔티티를 가져오게 됨
- 동일성 vs 동등성
- 동일성(identity) : 실제 인스턴스가 같음 (
==
) - 동등성(equality) : 인스턴스가 가지고 있는 값이 같음 (
equals()
)
- 동일성(identity) : 실제 인스턴스가 같음 (
- 쓰기 지연
em.persist(memberA); em.persist(memberB); // 여기까지는 SQL을 데이터베이스에 쿼리하지 않음 tx.commit(); // 트랜잭션을 커밋하는 순간 INSERT SQL 쿼리
- 위 사진과 같이 트랜잭션을 커밋하기 전까지 내부 쿼리 저장소에 SQL문을 저장해둠
- 이것을 쓰기 지연
transactional write-behind
라고 함 commit()
요청이 올 때 DB에 쿼리를 하고 이를flush
라고 함- flush : 영속성 컨텍스트의 변경 내용을 데이터베이스와 동기화하는 작업
- 변경 감지
- JPA에서 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 됨
- em.update() 같은 메소드 없음, 엔티티의 변경사항을 알아서 반영해줌
- 이를 변경 감지(dirty checking)라고 함
- JPA는 엔티티를 영속성 컨텍스트에 보관할 때 최초 상태를 스냅샷 해둠
- 이후 플러시 시점에 스냅샷과 비교하여 변경된 엔티티를 찾아 수정 쿼리를 생성함
- 변경 감지는 영속성 컨텍스트가 관리하는(영속 상태) 엔티티에만 적용됨
- 업데이트 쿼리를 생성할 때는 엔티티의 모든 필드를 수정에 반영함
- 데이터의 전송량이 증가하는 단점
- 업데이트 쿼리가 항상 같아 재사용이 가능하다는 장점
- 데이터베이스에 동일한 쿼리를 보낼 경우 한 번 파싱된 쿼리를 재사용 할 수 있음
- 필드가 많거나 저장되는 내용이 많을 경우엔 하이버네이트 확장 기능을 통해 수정된 데이터만 사용해서 동적으로 UPDATE SQL을 생성할 수 있음
DynamicUpdate
@Entity @org.hibernate.annotations.DynamicUpdate // 수정 필드만 업데이트 쿼리 @Table(name = "Member") public class Member {...}
- 일반적으로 필드가 30개가 넘어갈 경우 동적 업데이트를 사용하는 것이 성능이 좋음
- 정적 쿼리 방식으로 하다가 최적화가 필요하다 싶으면 동적을 시도해보는게 바람직
5. 플러시
플러시(flush())는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것
- 변경 감지가 동작해서 영속성 컨텍스트의 모든 엔티티를 스냅샷과 비교, 수정된 엔티티를 찾음
- 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송
- 플러시를 하는 방법
- em.flush()를 직접 호출
- 테스트나 다른 프레임워크와 JPA를 함께 사용할 때를 제외하고 거의 사용되지 않음
- 트랜잭션 커밋 시 자동 호출
- 트랜잭션을 커밋할 때 flush가 자동으로 호출됨
- JPQL 쿼리 실행
- JPQL 쿼리를 실행하면 flush가 자동으로 호출됨
- 이유는?
- JPQL은 데이터베이스에 직접 쿼리를 하기 때문에 쿼리 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해줘야 함
- em.flush()를 직접 호출
- 플러시 모드 옵션
FlushModeType.AUTO
: 커밋이나 쿼리를 실행할 때 플러시 (기본값)FlushModeType.COMMIT
: 커밋할 때만 플러시em.setFlushMode(FlushModeType.COMMIT); // 플러시 모드 직접 설정
- 플러시는 영속성 컨텍스트에 보관된 엔티티를 지우는것이 절대 아님.. 데이터베이스와 동기화 하는 것..
6. 준영속
영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)된 것을 준영속 이라고 함 따라서 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없음
준영속 상태로 만드는 방법
- em.detach(entity) : 특정 엔티티만 준영속 상태로 전환
- em.clear() : 영속성 컨텍스트를 완전히 초기화
em.close() : 영속성 컨텍스트를 종료
- 엔티티를 준영속 상태로 전환 : detach()
- 해당 메소드를 호출하는 순간 영속성 컨텍스트의 1차 캐시부터 쓰기 지연 SQL 저장소 까지 모두 제거됨
- 영속성 컨텍스트를 초기화 : clear()
- 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만듬
- 영속성 컨텍스트를 제거하고 새로 만드는 것과 동일한 상태
- 영속성 컨텍스트 종료 : close()
- 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만듬
주로 준영속 상태가 되는 것은 영속성 컨텍스트가 종료되면서 준영속이 됨. 개발자가 직접 준영속 상태로 만드는 것은 드문 일
- 준영속 상태의 특징
- 거의 비영속 상태에 가까움
- 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 기능들 동작 안함
- 식별자 값을 가짐
- 비영속 상태와의 차이점, 비영속 상태는 식별자 값이 없을 수 있음
- 준영속 상태는 영속 상태에 있었던 것이기 때문에 식별자 값이 반드시 있음
- 지연 로딩을 할 수 없음
- 지연 로딩(Lazy Loading)은 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 것
- 준영속 상태 영속성 먼텍스트가 관리하지 않으므로 지연 로딩 시 문제가 발생
- 거의 비영속 상태에 가까움
- 병합 : merge
- 준영속 상태의 엔티티를 다시 영속 상태로 변경하는 것
merge()
- 준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환함
- 병합은 준영속, 비영속 상태일 때 모두 가능함
- 식별자 값으로 엔티티가 조회가 되면 불러와서 병합
- 식별자 값으로 조회가 불가능 하면 새로운 엔티티이므로 새로 생성해서 병합함
- merge() 실행
- 파라미터로 넘어온 준영속 엔티티(member)의 식별자 값으로 1차 캐시에서 엔티티 조회
- 2-1. 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 조회 후 1차 캐시에 저장
- 파라미터로 넘어온 준영속 엔티티(member)의 식별자 값으로 1차 캐시에서 엔티티 조회
- 조회한 영속 엔티티(mergeMember)에 member 엔티티의 값을 채워 넣음
- mergeMember 반환