김영한님의 '자바 ORM 표준 JPA 프로그래밍'을 읽고 정리한 글입니다.
JPA에서 제공하는 기능은 엔티티와 테이블을 매핑하는 설계 부분과 매핑한 엔티티 실제 사용하는 부분으로 나눌 수 있다.
엔티티 매니저는 엔티티와 관련된 모든 일을 하는 관리자. 개발자 입장에서는 가상의 데이터베이스로 구현
3.1 엔티티 매니저 팩토리와 엔티티 매니저
데이터베이스를 하나만 사용하는 애플리케이션은 EntityMangerFactory를 하나만 생성한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook")
persistence.createEntityManagerFactory("jpabook") 호출시 META-INF/persistence.xml에 있는 정보를 바탕으로 EntityManagerFactory를 생성한다.
팩토리는 비용이 매우 크지만 매니저를 생성하는 비용은 적다.
그리고 엔티티 매니저는 동시성 문제로 여러 스레드가 동시에 접근하면 안된다.
(race condition)
-> DB에 데이터를 CRUD할 때 여러 스레드가 데이터를 서로 바꿈
3.2 영속성 컨텍스트란?
영속성 컨텍스트란 '엔티티를 영구 저장하는 환경
em.persist(member);
이 메소드는 단순히 회원 저장하는 것이 아닌 회원엔티티를 영속성 컨텍스트에 저장하는 것이다.
3.3 엔티티의 생명주기
엔티티에는 4가지 상태가 존재한다.
앤티티 객체를 막 생성했을 때 순수한 객체 상태이며 저장하지 않았다. 이때를 비영속 상태라 한다.
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
엔티티 매니저를 통해 영속성 컨텍스트에 저장했다. 영속성 컨텍스트가 관리하는 엔티티는 영속 상태라 한다.
em.persist(member);
영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다.
준영속 상태가 되는 3가지 메소드
참고 : https://girawhale.tistory.com/122
엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제
em.remove(member);
3.4 영속성 컨텍스트의 특징
영속성 컨텍스트는 1차 캐시라는 내부 캐시를 가진다. 영속 상태의 엔티티는 모두 이곳에 Map(식별자 : 엔티티 객체)로 저장된다.
//엔티티를 영속화
em.persist(member);
member객체를 만든 후 이 코드를 실행하면 1차 캐시에 회원 엔티티를 저장하지만 아직 데이터베이스에 저장되지 않는다.
// 엔티티 조회
Member member = em.find(Member.class, "member1");
em.find()는 1차 캐시에서 엔티티를 찾고 없으면 데이터베이스에서 조회한다.
만약 조회 엔티티가 캐시에 있다면 메모리에서 불러온다. 없다면 데이터베이스 조회 후 1차 캐시에 저장 후 엔티티 반환한다.
1차 캐시에 있다면 메모리에서 빠르게 불러올 수 있다.
영속성 컨텍스트는 성능상 이점과 동일성을 보장한다.
(em.find로 같은 엔티티를 두번 불러와도 member는 다르지 않다.)
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 쿼리문 안보냄
transaction.commit(); //트랜잭션 커밋
//커밋 순간에 insert 쿼리문을 보낸다.
위처럼 커밋할 때 쌓아둔 쿼리를 날리는 것을 쓰기 지연이라 한다.
회원A는 영속화 되어 1차 캐시에 저장된다. 동시에 쓰기 지연 SQL저장소에 쿼리를 보관한다.
이후 트랜잭션 커밋시 매니저는 '영속성 컨텍스트를 플러시'한다.
(플러시 : 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화)
트랜잭션을 커밋해야만 데이터베이스에 SQL이 제대로 전달할 수 있다.(?)
비즈니스 로직 확인위해 SQL을 계속 확인, SQL자체에 의존하게 된다.
///데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);
transaction.commit():
수정 시에는 등록 persist처럼 update같은 메소드를 사용하지 않는다. 엔티티의 변경사항을 자동으로 반영하는 변경 감지기능이 있다.
JPA는 엔티티를 영속성 컨텍스트에 저장할 때 최초 상태인 스냅샷을 저장한다.이후 플러시 시점에 스냅샷과 엔티티를 비교한다.
변경감지는 영속상태 엔티티에만 적용된다.
회원의 이름과 나이만 수정해도 모든 필드를 업데이트하는 쿼리가 전송된다.
하이버네이트의 DynamicUpdate 확장 기능을 사용하면 일부만 수정이 가능하다.
(30개 이상의 테이블일 때 더 빠르다 but 테이블이 30개 이상? -> 거의 잘못만든 것일듯..?)
삭제하기 위해서는 먼저 대상 엔티티를 조회해야 한다.
Member memberA = em.find(Meber.class, "memberA");
em.remove(memberA);
삭제 또한 커밋시 플러시를 호출하면 삭제 쿼리를 전달하는 과정을 거친다. em.remove시 memberA는 영속성 컨텍스트에서 제거된다.
3.5 플러시
플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
플러시 실행시
1. 변경 감지 동작, 모든 엔티티 스냅샷과 비교해 수정 쿼리 만들어 쓰기 지연 저장소에 저장
2. 저장소의 쿼리 데이터베이스에 전송
영속성 컨텍스트 플러시 방법
1. em.flush()
2. 트랜잭션 커밋 시 플러시 자동 호출
3. JPQL 쿼리 실행 시 플러시 자동 호출
플러시 모드 두가지
3.6 준영속
준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
영속-> 준영속 만드는 방법 3가지
//엔티티 생성, 초기화 가정, 비영속 상태
Member member = new Member();
//회원 엔티티 영속 상태
em.persist(member);
//회원 영속성컨텍스트에서 분리, 준영속상태
em.detach(member);
메소드 호출하는 순간부터 1차 캐시에서 제거 후 관련 SQL도 저장소에서 제거한다.
em.clear()는 영속성 컨텍스트를 초기화해서 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다.
///엔티티 조회, 영속 상태
Member memberB = em.find(Member.class, "memberA");
em.clear();
//준영속 상태
member.setUsername("김벡수");
준영속 상태이므로 변경 감지는 동작하지 않는다.
해당 영속성 컨텍스트가 관리하는 모든 엔티티가 준영속 상태가 된다.
아예 쓰기저장소와 1차캐시가 사라진다.
(영속 상태 엔티티는 주로 영속성 컨텍스트가 종료되면서 준영속 상태가 된다.)
준영속 상태인 회원 엔티티
준영속 -> 영속 : 병합 사용
public class ExamMergeMain {
static EntityManagerFactory emf =
Persistence.createEntityManagerFactory("jpabook");
public static void main(String args[]) {
//준영속 상태 엔티티 반환 받음
Member member = createMember("memberA", "회원1");
//준영속 상태에서 변경 --> 변경이 불가능
member.setusername("헤롱");
//merge
mergeMember(member);
}
static Member createMember(String id, String username) {
//영속성 컨텍스트1 시작
EntityManger em1 = emf.createEntityManager();
EntityTransaction txl = em1.getTransaction();
tx1.begin();
Member member = new Member();
member.setId(id);
member.setUsername(username);
em1.persist(member);
tx1.commit();
em1.close();
//영속성 컨텍스트1 종료
//member 엔티티는 준영속 상태가 된다.
return member;
}
static Member mergeMember(Member member) {
//영속성 컨텍스트2 시작
EntityManagerFactory em2 = emf.createEntityManager();
EntityTransaction tx2 = em2.getTransaction();
tx2.begin();
//영속 상태 엔티티가 반환
Member mergemember = em2.merge(member);
// 커밋 시 수정 회원명이 데이터베이스에 반영된다.
tx2.commit();
//준영속 상태
System.out.println("member = " + member.getUsername());
//영속상태
System.out.println("mergeMember = " + mergeMember.getUsername());
System.out.println("em2 contains member = " + em2.contains(member));
System.out.println("em2 contains mergeMember = " + em2.contains(mergeMember()));
em2.close();
//영속성 컨텍스트2 종료
}
}
출력 결과
member = 헤롱??
mergeMember = 헤롱
em2 contains member = false
em2 contains mergeMember = true
merge() 동작 방식
1.merge() 실행
2.파라미터의 준영속 엔티티 식별자 값이 1차 캐시에서 엔티티 조회
(만약 없으면 데이터베이스에서 엔티티 조회 후 1차 캐시에 저장)
3.조회한 영속 엔티티(mergeMember)에 member의 값을 넣는다. 이때 "회원1"이 "헤롱"으로 바뀐다.
4. mergeMember 반환
이후 commit시 변경감지 기능이 동작해 데이터베이스에 반영된다.
병합은 비영속 엔티티도 영속 상태로 만들 수 있다.
3.7 정리
꾸준히 하는 모습이 보기 좋아요 ~~
화이팅 ^^!