3. 영속성 관리

  • JPA가 제공하는 기능은 크게 다음과 같이 분류 가능
    1. entity와 table을 mapping하는 설계
    2. mapping한 entity를 실제로 사용
  • entity manager와 persistence는 mapping한 entity를 실제로 사용하는 동적인 부분에 해당함




3.1 entity manager factory와 entity manager

entity manager factory

  • 역할
    • entity manager를 만드는 공장
  • 비용
    • 생성 비용이 매우 크기 때문에 한 개를 만들어서 application 전체에서 공유
    • 일반적으로 DB 하나만 사용할 경우 EntityManagerFactory 하나 생성
  • thread safe
    • entity manager factory는 thread safe
    • ∴ 여러 스레드에서 하나 공유
  • Persistence.createEntityManagerFactory() 호출시 META-INF/persistence.xml 에 있는 정보를 바탕으로 EntityManagerFactory 생성


entity manager

  • 역할
    • entity를 저장, 수정, 삭제, 조회하는 등 entity와 관련된 모든 일을 처리, 관리하는 관리자
    • entity를 저장하는 가상의 DB로 생각하면 됨
  • 비용
    • entity manager factory로 생성하는 entity manager는 생성 비용이 거의 들지 않음
    • 필요할 때마다 생성
  • thread safe
    • entity manager는 thread safe하지 않음
    • 동시성 문제 발생하기 때문에 스레드 간 공유하면 안됨
  • connection
    • entity manager는 DB 연결이 필요한 시점까지 connection 얻지 않음
      • 일반적으로 transaction을 시작할 때 connection 획득
    • JPA 구현체들은 EntityManagerFactory 생성시 persistence.xml의 DB 접속 정보를 이용하여 connection풀도 만듬 (J2SE 환경)
      • 스프링 프레임워크를 포함한 J2EE 환경에서는 해당 container가 제공하는 datasource를 사용함




3.2 persistence란?

  • entity를 영구 저장하는 환경
  • entity manager로 entity를 저장, 조회시 entity manager는 persistence로 entity를 보관, 관리
  • persistence는 entity manager 생성시 하나 만들어짐
  • entity manager를 통해 접근 및 관리 가능




3.3 entity의 생명 주기

  • entity의 4가지 상태
    1. 비영속(new/transient): persistence와 전혀 관계가 없는 상태
    2. 영속(managed): persistence에 저장된 상태
    3. 준영속(detached): persistence에 저장되었다가 분리된 상태
    4. 삭제(removed): 삭제된 상태


비영속 (new/transient)

  • entity 객체 생성 후 아직 저장하지 않아 persistence context, DB와는 관련 없는 순수한 객체 상태
코드
// 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");


영속 (managed)

  • entity manager를 통해 persistence에 저장되어 persistence에 의해 관리하는 entity
  • em.find() 또는 JPQL을 사용해서 조회한 entity도 persistence가 관리하는 영속 상태가 됨
코드
// 객체를 저장한 상태(영속)
em.persist(member);


준영속(detached)

  • persistence가 관리하던 entity가 persistence에 의해 관리되지 않으면 준영속 상태가 됨
  • 다음을 이용하여 entity를 준영속 상태로 변경할 수 있음
    • em.detach() 를 호출
    • em.close() 로 persistence를 종료
    • em.clear() 로 persistence를 초기화
코드
// persistence에서 분리된 상태(준영속)
em.detach(member);


삭제

  • entity를 persistence 및 DB에서 삭제
코드
// 객체를 삭제한 상태(삭제)
em.remove(member);




3.4 persistence의 특징

  1. persistence와 식별자 값
    • persistence는 entity를 식별자 값(@Id로 table의 기본 키와 mapping한 값)으로 구분
    • ∴ 식별자 값은 non-null
  2. persistence와 DB 저장
    • JPA는 보통 transaction commit시 persistence에 새로 저장된 entity를 DB에 반영 (flush)
  3. persistence에 의한 entity 관리시 장점
    • 1차 cache
    • identity 보장
    • transaction을 지원하는 쓰기 지연
    • 변경 감지
    • 지연 로딩



3.4.1 entity 조회

1차 cache

  • persistence 내부의 cache
  • 영속 상태의 entity는 이곳에 저장
  • key는 @Id로 DB pk와 mapping된 식별자 값
    • ∴ persistence에 데이터 저장, 조회하는 모든 기준은 DB 기본 키 값
  • value는 entity instance

entity 조회

  1. 우선 1차 cache에서 식별자 값으로 entity 조회
  2. 1차 cache에 entity 없을 경우 DB를 조회해서 entity 생성
  3. 1차 cache에 저장 후 영속 상태의 entity 반환
  • 이처럼 먼저 메모리상의 1차 cache부터 조회하여 성능상 이점을 누릴 수 있음

영속 entity의 identity 보장

코드
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

System.out.println(a == b);    // identity 비교 결과: true

  • em.find() 반복 호출시에도 persistence는 1차 cache에 있는 같은 entity instance를 반환
  • ∴ entity의 identity을 보장

identity과 equality
동일성(identity): 실제 instance가 같음
동등성(equality): instance는 다를 수 있음. instance가 가지고 있는 값이 같음

REPEATABLE READ
JPA는 1차 cache를 통해 반복 가능한 읽기 등급(REPEATABLE READ)의 transaction 격리 수준을 DB가 아닌 application 차원에서 제공



3.4.2 entity 등록

transaction을 지원하는 쓰기 지연 (transactional write-behind)

  • entity manager는 transaction commit 전까지 DB에 entity 저장하지 않고 쓰기 지연 SQL 저장소에 쿼리문 저장
  • transaction commit시 모아둔 쿼리를 DB에 보냄 (flush)
    • persistence의 변경 사항을 DB 에 동기화하는 작업
코드
EntityManager em = emf.createEntityManager();
EntityTransaction et = em.getTransaction();

// entity manager는 데이터 변경시 transaction 시작해야 함
et.begin();

/**
  * ----persistence----
  * 1차 cache 
  * [  ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  * 
  *
  * --------DB---------
  * [  ]
  * -------------------
  */

em.persist(member1);
/**
  * member1을 1차 cache에 저장, member1 INSERT 쿼리 생성
  *
  * ----persistence----
  * 1차 cache 
  * [ member1 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [ INSERT member1 ]
  * -------------------
  * 
  * 
  * --------DB---------
  * [  ]
  * -------------------
  */

em.persist(member2);
/**
  * member2를 1차 cache에 저장, member2 INSERT 쿼리 생성
  *
  * ----persistence----
  * 1차 cache 
  * [ member1, member2 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [ INSERT member1, INSERT member2 ]
  * -------------------
  * 
  *
  * --------DB---------
  * [  ]
  * -------------------
  */
  
// commit하는 순간 DB에 INSERT 쿼리 보냄
et.commit();
/**
  * member1, member2 INSERT 쿼리 DB로 보냄
  *
  * ----persistence----
  * 1차 cache 
  * [ member1, member2 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  * |                |
  * | INSERT member2 |
  * | INSERT member1 |
  * v                v
  * --------DB---------
  * [  ]
  * -------------------
  */

/**
  * persistence와 DB 동기화 후 실제 DB transaction commit
  *
  * ----persistence----
  * 1차 cache 
  * [ member1, member2 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1, member2 ]
  * -------------------
  */
  

transaction을 지원하는 쓰기 지연이 가능한 이유

  1. entity 저장할 때마다 INSERT 쿼리를 DB로 보내고 commit 호출 시 DB transaction commit하는 경우
  2. 쓰기 지연으로 쿼리문 모아두었다가 commit 호출 시 모아둔 쿼리 DB로 한번에 보내고 DB transaction commit하는 경우
  • 위 두가지 경우의 결과는 동일
  • 어차피 DB commit 안하면 미리 DB에서 쿼리 실행해도 의미 없기 때문에 쓰기 지연을 사용하여 효율적으로 관리할 수 있음



3.4.3 entity 수정

SQL 수정 쿼리의 문제점

  • SQL 을 사용하는 경우엔 수정 쿼리를 직접 작성해야 함
  • 프로젝트가 커질수록 수정 쿼리들도 점점 추가됨
  • 관리하기도 복잡해지며, 직/간접적으로 비즈니스 로직이 SQL에 의존하게 됨


변경 감지 (dirty checking)

  • JPA 는 entity를 persistence에 보관할 때 최초 상태를 보관해서 snapshot으로 저장함
  • transaction commit시 entity manager 내부에서 먼저 flush 호출되고, 이 때 snapshot과 entity 비교해서 변경 감지
  • 변경 감지된 entity 있으면 업데이트 쿼리 생성해서 쓰기 지연 SQL 저장소에 보관
  • 쓰기 지연 SQL 저장소에 있는 쿼리들 보낼 때 같이 보내져서 DB 에 반영
  • 변경 감지는 영속 상태의 entity에게만 적용됨
코드
EntityManager em = emf.createEntityManager();
EntityTransaction et = em.getTransaction();
et.begin();

Member member1 = em.find(Member.class, "member1");
Member member2 = em.find(Member.class, "member2");

/**
  * 최초 상태 snapshot으로 저장됨
  *
  * ----persistence----
  * 1차 cache 
  * [    @id    | Entity  |  snapshot   ]
  * [ "member1" | member1 | member1 ]
  * [ "member2" | member2 | member2 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1, member2 ]
  * -------------------
  */

member1.setAge(10);
/**
  * member1 변경됨
  *
  * ----persistence----
  * 1차 cache 
  * [    @id    |  Entity  |  snapshot   ]
  * [ "member1" | member1' | member1 ]
  * [ "member2" | member2  | member2 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1, member2 ]
  * -------------------
  */

et.commit();
/**
  * transaction commit시 flush 호출됨
  * flush 호출되면 entity와 snapshot 비교하여 변경된 entity 감지
  * 변경된 entity 있으면 UPDATE 쿼리를 쓰기 지연 SQL 저장소로 보냄
  *
  * ----persistence----
  * 1차 cache 
  * [    @id    |  Entity  |  snapshot   ]
  * [ "member1" | member1' | member1 ] => diff
  * [ "member2" | member2  | member2 ] => eq
  *
  * 쓰기 지연 SQL 저장소 
  * [ UPDATE member1 ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1, member2 ]
  * -------------------
  */

/**
  * 쓰기 지연 SQL 저장소의 쿼리들 DB 로 전송
  *
  * ----persistence----
  * 1차 cache 
  * [    @id    |  Entity  |  snapshot   ]
  * [ "member1" | member1' | member1 ]
  * [ "member2" | member2  | member2 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  * |                |
  * | UPDATE member1 |
  * v                v
  * --------DB---------
  * [ member1, member2 ]
  * -------------------
  */

/**
  * member1 변경사항 DB에 반영됨
  *
  * ----persistence----
  * 1차 cache 
  * [    @id    |  Entity  |  snapshot   ]
  * [ "member1" | member1' | member1 ]
  * [ "member2" | member2  | member2 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1', member2 ]
  * -------------------
  */

변경된 entity의 snapshot은 어느 시점에서 수정될까?

변경 감지 UPDATE 쿼리 기본 전략

  • 변경된 필드 뿐만 아니라 entity의 모든 필드를 업데이트 함
    • 단점: DB 로 전송되는 데이터량이 증가
    • 장점:
      • 수정 쿼리가 항상 같으므로 application 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용 가능
      • 동일한 쿼리를 보내면 DB에서는 이전에 파싱된 쿼리를 재사용 가능
  • 위와 같은 장점 때문에 기본 전략은 전체 필드 업데이트
    • 필드가 많거나 저장되는 내용이 너무 클 경우 수정된 데이터만 사용해서 동적으로 UPDATE 쿼리 생성하는 전략 선택 가능
      • 필드수가 대략 30개 이상인 경우에 수정된 데이터만 업데이트 하는 동적 쿼리 생성 전략이 더 빠를 수 있다고 함
    • hibernate의 경우 @org.hibernate.annotations.DynamicUpdate 어노테이션을 entity 클래스 앞에 붙이면 됨



3.4.4 entity 삭제

  • entity를 삭제하려면 먼제 대상을 조회
  • em.remove() 호출하여 entity 삭제
    • 이 때 persistence에서 entity가 제거되는데, 이러한 entity는 재사용하지 말고 GC 대상이 되도룩 두어야 함
  • INSERT, UPDATE 와 마찬가지로 쓰기 지연 SQL 저장소에 등록 후 flush 시점에서 DB 에 반영됨
코드
EntityManager em = emf.createEntityManager();
EntityTransaction et = em.getTransaction();
et.begin();

/**
  * ----persistence----
  * 1차 cache 
  * [  ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  * 
  * 
  * --------DB---------
  * [ member1 ]
  * -------------------
  */

Member member1 = em.find(Member.class, "member1");
/**
  * member1 조회
  *
  * ----persistence----
  * 1차 cache 
  * [ member1 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  * 
  * 
  * --------DB---------
  * [ member1 ]
  * -------------------
  */

em.remove(member1);
/**
  * member1 entity 삭제
  *
  * ----persistence----
  * 1차 cache 
  * [  ]
  *
  * 쓰기 지연 SQL 저장소 
  * [ DELETE member1 ]
  * -------------------
  * 
  *
  * --------DB---------
  * [ member1 ]
  * -------------------
  */
  
et.commit();
/**
  * member1 DELETE 쿼리 DB로 전송
  *
  * ----persistence----
  * 1차 cache 
  * [  ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  * |                |
  * | DELETE member1 |
  * v                v
  * --------DB---------
  * [ member1 ]
  * -------------------
  */

/**
  * DB에서 member1 삭제
  *
  * ----persistence----
  * 1차 cache 
  * [  ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  *
  *
  * --------DB---------
  * [  ]
  * -------------------
  */
  




3.5 플러시

  • flush는 persistence의 변경 내용을 DB에 반영
  • 플러시 실행 과정은 다음과 같음
    1. 변경 감지가 동작, 수정된 entity의 UPDATE 쿼리 만들어 쓰기 지연 SQL 저장소에 등록
    2. 쓰기 지연 SQL 저장소에 보관된 쿼리들 DB 로 전송(INSERT, UPDATE, DELETE 쿼리들)
  • persistence를 플러시 하는 방법은 다음과 같음
    1. em.flush() 직접 호출
      • entity manager의 flush() 메소드를 직접 호출하여 persistence를 강제로 플러시
      • 테스트시, 혹은 다른 프레임워크 + JPA 조합으로 쓸 때를 제외하고는 거의 사용 X
    2. transaction commit시 자동 호출
      • DB transaction commit 전에 flush 호출해서 persistence와 DB 동기화해야 하기 때문에 JPA는 transaction commit시 자동으로 flush 실행
    3. JPQL 쿼리 실행시 자동 호출
      • JPQL이나 Criteria 같은 객체지향 쿼리 호출시에도 flush 자동 실행
코드
      em.persist(member1);
      em.persist(member2);
      em.persist(member3);

      // commit 전에 JPQL 실행
      query = em.createQuery("select m from Member m", Member.class);
      List<Member> memberList = query.getResultList();
      

</details> - 위와 같이 persistence에 있는 entity들이 DB에 반영되기 전에 JPQL 실행할 경우 영속 상태인 entity들이 DB에는 반영 전이라 조회가 안됨 - ∴ JPA는 JPQL 실행 전에 flush 해서 persistence와 DB 를 동기화 시킨 후 JPQL 실행함 - 식별자를 기준으로 조회하는 find() 메소드 호출시에는 flush 실행 X



3.5.1 플러시 모드 옵션

  • entity manager에 플러시 모드 직접 지정하려면
코드

javax.persistence.FlushModeType 사용하면 됨

  • FlushModeType.AUTO: commit이나 쿼리 실행시 flush (default)
  • FlushModeType.COMMIT: commit할때만 flush. 성능 최적화를 위해 사용할 수 있음
코드
em.setFlushMode(FlushModeType.COMMIT)




3.6 준영속

  • persistence가 관리하는 영속 상태의 entity가 persistence로부터 분리(detached) 된 것이 준영속 상태
  • ∴ 준영속 상태인 entity는 persistence가 제공하는 기능 사용 X
  • 영속 -> 준영속 상태 변환은 크게 다음과 같음
    1. em.detach(entity): 특정 entity만 준영속상태로 전환
    2. em.clear(): persistence를 초기화
    3. em.close(): persistence를 종료



3.6.1 entity를 준영속 상태로 전환: detach()

  • detach() 를 호출하면 해당 entity를 준영속 상태로 전환
  • 준영속 상태로 전환된 entity는 1차 cache 뿐만 아니라 쓰기 지연 SQL 저장소까지 관련 정보가 persistence에서 모두 삭제됨
코드
EntityManager em = emf.createEntityManager();
EntityTransaction et = em.getTransaction();
et.begin();

Member member1 = em.find("member1");
Member member2 = new Member();
member2.setId("member2");
member2.setUsername("Bob");

em.persist(member2);
/**
  * member2가 영속상태가 되어 1차 cache에 저장
  * member2 INSERT 쿼리도 쓰기 지연 SQL 저장소에 등록
  *
  * ----persistence----
  * 1차 cache 
  * [ member1 ]
  * [ member2 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [ INSERT member2 ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1 ]
  * -------------------
  */

em.detach(member2);
/**
  * member2 준영속 상태로 전환
  * 1차 cache에서 삭제됨
  * 쓰기 지연 SQL 저장소에 등록되어 있던 member2 INSERT 쿼리도 삭제
  *
  * ----persistence----
  * 1차 cache 
  * [ member1 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1 ]
  * -------------------
  */

et.commit();
/**
  * flush 될때도 member2 는 persistence로부터 완전히 분리되었기 때문에 DB 반영 안됨
  *
  * ----persistence----
  * 1차 cache 
  * [ member1 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1 ]
  * -------------------
  */



3.6.2 persistence 초기화: clear()

  • em.clear() 호출시 해당 persistence를 초기화해서 모든 entity를 준영속 상태로 만듬
코드
/**
  * ----persistence----
  * 1차 cache 
  * [ member1 ]
  * [ member2 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [ INSERT member2 ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1 ]
  * -------------------
  */

em.clear();
/**
  * persistence em 전체 초기화
  * 1차 cache, 쓰기 지연 SQL 저장소 다 초기화됨
  * 해당 persistence에 의해 관리되던 entity는 모두 준영속으로 전환
  *
  * ----persistence----
  * 1차 cache
  * [  ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1 ]
  * -------------------
  */

member1.setUsername("Charlie");
member2.setUsername("David");

et.commit();
/**
  * 값 변경해도 반영 안됨
  *
  * ----persistence----
  * 1차 cache 
  * [  ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1 ]
  * -------------------
  */



3.6.3 persistence 종료: close()

  • em.close() 로 persistence 종료시 관리되던 영속 상태 entity가 모두 준영속 상태로 전환
  • 개발자가 detach, clear 등으로 직접 준영속 상태로 만드는 일은 드물고, 주로 persistence 종료와 함께 준영속 상태가 됨
코드
/**
  * ----persistence----
  * 1차 cache 
  * [ member1 ]
  * [ member2 ]
  *
  * 쓰기 지연 SQL 저장소 
  * [ INSERT member2 ]
  * -------------------
  *
  *
  * --------DB---------
  * [ member1 ]
  * -------------------
  */

em.close();
/**
  * persistence em 종료되어 영속 상태 entity 모두 준영속으로 전환
  *
  * ----persistence----
  *       종    료
  * -------------------
  *
  *
  * --------DB---------
  * [ member1 ]
  * -------------------
  */


3.6.4 준영속 상태의 특징

  1. 비영속 상태와 거의 유사
    • persistence가 제공하는 1차 cache, 쓰기 지연, 변경 감지, 지연 로딩 등의 기능 동작 X
  2. 식별자 값을 가짐
    • 비영속 상태의 경우 식별자 값이 없을 수 있지만, 준영속 상태는 영속 상태를 거쳤기 때문에 반드시 식별자 값이 존재

지연 로딩(lazy loading)?
실재 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 persistence를 통해 데이터를 불러오는 방법



3.6.5 병합: merge()

  • 병합을 사용하여 준영속 상태의 entity를 다시 영속 상태로 변경할 수 있음
    • 비영속 상태 entity도 병합으로 영속 상태 전환 가능
      • ∴ 병합은 save or update
  • merge() 메소드는 준영속 상태의 entity를 받아서 새로운 영속 상태의 entity를 반환

  • 병합 과정은 다음과 같음
    1. 준영속 entity x 를 파라미터로 merge() 실행
    2. x 의 식별자 값으로 1차 cache에서 entity 조회
      • 1차 cache에 entity 없으면 DB 에서 조회해서 저장
      • DB에도 없으면 새로 생성해서 저장
    3. 조회한 영속 entity yx 의 값 채워넣음
    4. y 반환
코드
EntityManager em1 = emf.createEntityManager();
EntityTransaction et1 = em1.getTransaction();
et1.begin();

Member member1 = new Member();
member1.setId("member1");
member1.setUsername("Alice");

em.persist(member1);

/**
  * member1 영속 상태로 전환
  * 
  * member1 = { id: "member1", userName: "Alice" } => 영속
  *
  *
  * ----persistence (em1)----
  * 1차 cache 
  * [ { id: "member1", userName: "Alice" } ]
  *
  * 쓰기 지연 SQL 저장소 
  * [ INSERT { id: "member1", userName: "Alice" } ]
  * -------------------------
  *
  *
  * --------DB---------
  * [  ]
  * -------------------
  */

et1.commit();
/**
  * member1 DB에 저장
  * 
  * member1 = { id: "member1", userName: "Alice" } => 영속
  *
  *
  * ----persistence (em1)----
  * 1차 cache 
  * [ { id: "member1", userName: "Alice" } ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------------
  *
  *
  * --------DB---------
  * [ { id: "member1", userName: "Alice" } ]
  * -------------------
  */

em.close();
/**
  * persistence em1 종료되어 member1 entity 준영속으로 전환
  * 
  * member1 = { id: "member1", userName: "Alice" } => 준영속
  *
  *
  * ----persistence (em1)----
  *       종    료
  * -------------------------
  *
  *
  * --------DB---------
  * [ { id: "member1", userName: "Alice" } ]
  * -------------------
  */
  
member1.setUsername("Bob");
/**
  * member1 값 변경해도 준영속 상태라 반영 X
  * 
  * member1 = { id: "member1", userName: "Bob" } => 준영속
  *
  *
  * ----persistence (em1)----
  *       종    료
  * -------------------------
  *
  *
  * --------DB---------
  * [ { id: "member1", userName: "Alice" } ]
  * -------------------
  */

EntityManager em2 = emf.createEntityManager();
EntityTransaction et2 = em2.getTransaction();
et2.begin();

Member mergedMember = (em2.merge(member1));
/**
  * merge 되면서 member1의 식별자로 persistence em2에서 entity 조회
  * 
  * member1 = { id: "member1", userName: "Bob" } => 준영속
  *
  *
  * ----persistence (em2)----
  * 1차 cache 
  * [  ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------------
  *
  *
  * --------DB---------
  * [ { id: "member1", userName: "Alice" } ]
  * -------------------
  */

/**
  * 1차 cache에 entity 없으므로 DB 에서 가져와서 저장
  * 
  * member1 = { id: "member1", userName: "Bob" } => 준영속
  *
  *
  * ----persistence (em2)----
  * 1차 cache 
  * [                 Entity               |                 snapshot                 ]
  * [ { id: "member1", userName: "Alice" } | { id: "member1", userName: "Alice" } ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------------
  *
  *
  * --------DB---------
  * [ { id: "member1", userName: "Alice" } ]
  * -------------------
  */

/**
  * 가져온 entity에 준영속 상태이던 member1 값 채워넣고 반환
  * 기존 준영속 상태이던 entity는 계속 준영속 상태
  * 
  * member1 = { id: "member1", userName: "Bob" } => 준영속
  * mergedMember = { id: "member1", userName: "Bob" } => 영속
  * 
  *
  * ----persistence (em2)----
  * 1차 cache 
  * [                Entity              |                 snapshot                 ]
  * [ { id: "member1", userName: "Bob" } | { id: "member1", userName: "Alice" } ]
  *
  * 쓰기 지연 SQL 저장소 
  * [  ]
  * -------------------------
  *
  *
  * --------DB---------
  * [ { id: "member1", userName: "Alice" } ]
  * -------------------
  */

et2.commit();
/**
  * commit하면 변경사항 DB에 반영됨
  *
  * ----persistence----
  * 1차 cache 
  * [                Entity              |                 snapshot                 ]
  * [ { id: "member1", userName: "Bob" } | { id: "member1", userName: "Alice" } ] => diff
  *
  * 쓰기 지연 SQL 저장소 
  * [ UPDATE member1 ]
  * -------------------------
  * |                |
  * | UPDATE member1 |
  * v                v
  * --------DB---------
  * [ { id: "member1", userName: "Bob" } ]
  * -------------------
  */