-> 블로그 이전

[JPA] 영속성 관리

2022. 7. 1. 13:28Language`/JPA

EntityManagerFactoroy & EntityManager

EntityManagerFactory는 이름 그대로 "EntityManager"를 생산하는 공장이라고 보면 된다
일반적으로 DB를 하나만 사용하는 Application은 EntityManagerFactory 또한 하나만 생성한다

EntityManagerFactory는 여러 thread가 동시에 접근해도 thread-safe하므로, 서로 다른 thread간에 공유가 가능하다.
하지만 EntityManager는 내부에 "DataSource(Connection Pool)"를 유지하면서 DB와 통신하기 때문에 서로 다른 thread간에 절대로 공유하면 안된다

  • EntityManager는 User의 Request별로 하나씩 생성이 된다

Inflearn : 자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)

 


영속성 컨텍스트 (Persistence Context)

영속성 컨텍스트란 "Entity를 영구적으로 저장하는 환경"이라는 의미이다
영속성 컨텍스트는 EntityManager를 통해서 접근하고 관리한다


Entity 생명주기

Entity에는 다음 4가지 생명주기가 존재한다

  1. 비영속 (new/transient)
  2. 영속 (managed)
  3. 준영속 (detached)
  4. 삭제 (removed)

Inflearn : 자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)

 

1. 비영속 (new/transient)

"비영속"이란 Entity를 생성만 한 상태를 의미한다. 이 Entity는 "순수한 객체 상태"이며 아직 영속성 컨텍스트의 관리를 받고 있지 않는 객체를 의미한다

Member member = new Member("Son Heung Min", 29);

이 Member라는 객체는 지금 {영속성 컨텍스트 & DB}와 전혀 관련이 없다

Inflearn : 자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)

 

2. 영속 (managed)

Entity가 영속성 컨텍스트에 의해 관리되는 그 순간부터 Entity의 상태는 "영속 상태"가 된다.
일반적으로 비영속 상태의 객체에 대해서 em.persist()를 하게되면 해당 Entity는 영속화가 된거라고 보면 된다

  • em.persist가 아니더라도 em.find()를 통해서 DB로부터 Entity를 가져오거나 JPQL을 사용해서 조회한 Entity또한 "영속 상태"가 된다
// (1) em.persist
Member member = new Member("Son Heung Min", 29);
em.persist(member);

// (2) em.find
Member findMember = em.find(Member.class, 1L);

// (3) JPQL
Member findMember2 = em.createQuery("select m from Member m where m.id=:id", Member.class)
        .setParameter("id", 1L)
        .getSingleResult();

Inflearn : 자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)

여기서 알아야하는 사실은 em.persist를 한다고해서 query가 DB로 날라가는 것은 절대 아니다
em.persist를 하면 insert query가 영속성 컨텍스트 내부의 "쓰기 지연 SQL 저장소"에 보관이 되고, 나중에 em.flush()나 em.commit() 시점에 쓰기 지연 SQL 저장소의 모든 query가 DB로 날라가게 된다

  • 물론 JPQL을 사용해서 query를 생성할때는 위와 관련없이 바로 DB로 query가 날라가게 된다

 

3. 준영속 (detached)

영속성 컨텍스트가 관리하던 영속 상태의 Entity를 영속성 컨텍스트가 관리하지 않는 상태가 되면 해당 Entity는 "준영속 상태"가 된 것이다
일반적으로 특정 엔티티를 준영속 상태로 만들려면 em.detach()를 호출하면 된다

  • em.close()를 통해서 영속성 컨텍스트를 닫거나, em.clear()를 통해서 영속성 컨텍스트를 초기화하면 그 때 관리하던 Entity들은 모두 "준영속 상태"가 된다

준영속 상태가 된 Entity는 더이상 영속성 컨텍스트가 지원하는 {Lazy Loading, Dirty Checking, ...}등의 혜택을 받을 수 없다

// (1) 영속 -> 준영속 : detach
em.detach(member);

// (2) 영속성 컨텍스트 초기화
em.clear();

// (3) 영속성 컨텍스트 닫기
em.close();

 

4. 삭제 (removed)

em.remove를 통해서 Entity를 삭제하게되면 해당 Entity는 영속성 컨텍스트에서 삭제될뿐만 아니라, DB에서도 해당되는 instance가 삭제된다


영속성 컨텍스트의 장점

1. Entity 조회 (1차 캐시)

영속성 컨텍스트 내부에는 Entity들을 관리하는 "1차 캐시"라는 것이 존재한다.

Member member1 = new Member("Son Heung Min", 29);
em.persist(member);

이렇게 member1을 em.persist를 통해서 영속화시키면 영속성 컨텍스트 내부의 1차 캐시에 해당 Entity가 놓이게되고 이제부터 관리가 시작된다

1차 캐시는 기본적으로 {@Id, Entity}로 구성되어 있고 각 Entity는 @Id에 의해 구분된다

  • 나중에 Dirty Checking을 위한 "스냅샷 객체"도 1차 캐시에 함께 저장된다
  • "스냅샷 객체"의 경우 @Transactional(readOnly = true)로 설정될 경우 생성되지 않는다
    • 왜냐하면 readOnly=true면 조회만 한다는 의미이고 트랜잭션 내부에서 변경이 되지 않기 때문에 스냅샷 객체가 있을 필요가 없다

 

Member findMember = em.find(Member.class, 1L);

이렇게 em.find를 통해서 어떤 Entity를 조회하게되면 먼저 "1차 캐시"에 해당되는 Entity가 있나 살펴본다
만약 있다면 해당 Entity를 바로 return해준다. 이것이 1차 캐시의 장점이라고 할 수 있다

  • 1차 캐시에 존재한다면 굳이 불필요하게 DB에 쿼리를 날릴 필요가 없어진다


만약 1차 캐시에 없다면 어떻게 될까?

Member findMember = em.find(Member.class, 2L);

1차 캐시에 없다면 DB로부터 해당 Entity를 가져온 다음, 영속화시켜버린다
다음부터 해당 Entity를 조회할때는 DB에서 가져오지 않고, 1차 캐시에서 가져올 수 있다

 

※ Example

현재 "member"라는 table에는 위와 같이 데이터가 있고, 영속성 컨텍스트는 초기화된 상태라고 하자

Member findMemberA = em.find(Member.class, 1L);
System.out.println("memberA 찾기 첫번째 : " + findMemberA);

위와 같이 em.find를 통해서 (PK : item_id = 1)인 Entity를 조회하면 어떠한 query가 발생할까

Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.age as age2_0_0_,
        member0_.user_name as user_nam3_0_0_ 
    from
        member member0_ 
    where
        member0_.id=?
memberA 찾기 첫번째 : Member(id=1, username=Son Heung Min, age=29)

찾으려는 Entity는 1차 캐시에 존재하지 않기 때문에 일단 DB로 "select query"를 날려서 찾아온 다음에 1차 캐시에 저장한다

이 다음부터는 DB가 아니라 1차 캐시로부터 return되기 때문에 "select query"가 발생하지 않는다

Member findMemberA = em.find(Member.class, 1L);
System.out.println("memberA 찾기 첫번째 : " + findMemberA);

System.out.println("===================================================");

Member findMemberA2 = em.find(Member.class, 1L);
System.out.println("memberA 찾기 두번째 : " + findMemberA2);
Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.age as age2_0_0_,
        member0_.user_name as user_nam3_0_0_ 
    from
        member member0_ 
    where
        member0_.id=?
memberA 찾기 첫번째 : Member(id=1, username=Son Heung Min, age=29)
================================================================
memberA 찾기 두번째 : Member(id=1, username=Son Heung Min, age=29)


em.persist를 한 Entity를 em.find로 조회하는것도 마찬가지로 해당 엔티티는 1차 캐시에 존재하기 때문에 별도의 select query없이 1차 캐시에서 바로 return된다

Member memberB = new Member("Lee Seung Woo", 23);
em.persist(memberB);

Member findMemberB = em.find(Member.class, 2L);
System.out.println(findMemberB);
Hibernate: 
    /* insert PersistenceContext.domain.Member
        */ insert 
        into
            member
            (age, user_name) 
        values
            (?, ?)
Member(id=2, username=Lee Seung Woo, age=23)
여기서 insert query가 나간 이유는 현재 PK 생성 전략을 "IDENTITY"로 설정하였고 DB는 MySQL이므로 PK값은 auto_increment로 자동 생성된다.
영속성 컨텍스트에서 관리가 되어야 하는 엔티티는 "반드시 식별자(PK)"를 가지고 있어야만 한다

따라서 IDENTITY 전략에서만 예외적으로 persist를 하자마자 insert query가 나가고 내부적으로 메소드를 통해서 생성된 PK값을 가져옴으로써 영속성 컨텍스트에서 관리가 시작된다
>> SEQUENCE나 TABLE 전략은 애초에 PK값을 시퀀스나 테이블상에서 가져오기 때문에 persist를 한다고해서 따로 insert query가 나가지 않는다

2. Entity의 동일성 보장

현재 영속성 컨텍스트 1차 캐시는 위와 같은 상태라고 하자

Member findA = em.find(Member.class, 1L);
Member findB = em.find(Member.class, 1L);

System.out.print(findA == findB);

과연 boolean식의 결과는 true일까 false일까

>> 정답은 true이다


아무리 반복해서 호출한다해도 영속성 컨텍스트는 1차 캐시에 존재하는 해당 Entity를 계속 return해주기 때문에 true가 나온다

영속성 컨텍스트는 Entity의 "동일성"을 보장한다

 


3. 트랜잭션을 지원하는 쓰기 지연

Member memberA = new Member("Son Heung Min", 29);
em.persist(memberA);

위의 쿼리를 실행하면 영속성 컨텍스트는 다음과 같은 변화가 일어난다

em.persist를 통해서 영속화하면 1차 캐시에 저장될 뿐만 아니라, "쓰기 지연 SQL 저장소"에 해당 객체를 실제 DB에 저장하기 위한 "INSERT QUERY"가 생성되어서 저장된다

Member memberB = new Member("Son Heung Min2", 29);
em.persist(memberB);


이러한 상태에서 트랜잭션 커밋을 하였다고 하자

커밋하는 순간 쓰기 지연 SQL 저장소의 모든 쿼리가 DB로 "이 순간" 날라가게 되고, 이 때 DB에 데이터가 저장되는 것이다


4. 변경 감지 (Dirty Checking)

일반적으로 DB에 존재하는 instance에 대한 update를 하고 싶다면 update query를 DB로 직접 날리는게 일반적이다.
하지만 JPA는 "영속성 컨텍스트"라는 개념이 존재하기 때문에 instance를 update하는 방식이 약간 다르다

일단 1차 캐시에는 {@Id, Entity}말고 하나를 더 관리하는데 이것은 "스냅샷 객체"이다

영속성 컨텍스트는 Entity에 대한 관리를 시작할 때 "그 시점의 Entity 모습" : 스냅샷을 찍어서 1차 캐시에서 함께 관리한다
그리고 해당 Entity에 대해서 값을 변경하면 당연히 스냅샷과 다른 데이터를 보유하게 될 것이다.

이러한 특징을 살려서 영속성 컨텍스트는 나중에 commit시점에 스냅샷과 Entity를 비교해서 다른점들에 대한 변경 쿼리를 쓰기 지연 SQL 저장소에 넣어두고 flush를 하게 된다

Member memberA = new Member("MemberA", 30);
em.persist(memberA);

MemberA라는 Entity를 영속성 컨텍스트가 관리하기 시작할 때 MemberA의 Snapshot을 찍어놓고 같이 관리를 하고 있다

memberA.setAge(60);

여기서 Entity에 대해서 setter를 통해서 age값을 변경했다고 하자

 

memberA.setUsername("MemberA-Update");

그리고 또 memberA의 이름을 변경하였다

이후 모든 로직이 끝나고 tx.commit()을 하게되면 다음 과정이 이루어진다

Hibernate: 
    /* insert PersistenceContext.domain.Member
        */ insert 
        into
            member
            (age, user_name) 
        values
            (?, ?)
Hibernate: 
    /* update
        PersistenceContext.domain.Member */ update
            member 
        set
            age=?,
            user_name=? 
        where
            id=?

Member memberA = new Member("MemberA", 30);
em.persist(memberA);

memberA.setAge(60);

이렇게 memberA의 age 필드만 변경했을 경우 query 는 어떻게 날라갈까

Hibernate: 
    /* insert PersistenceContext.domain.Member
        */ insert 
        into
            member
            (age, user_name) 
        values
            (?, ?)
Hibernate: 
    /* update
        PersistenceContext.domain.Member */ update
            member 
        set
            age=?,
            user_name=? 
        where
            id=?

update query를 보면 age만 변경했음에도 불구하고 user_name도 같이 변경쿼리가 나간것을 확인할 수 있다

>> JPA의 기본 전략은 엔티티의 모든 필드를 update하는 것이므로 당연한 결과이다


이렇게 모든 필드를 사용하면 DB에 보내는 데이터 전송량이 증가하는 단점이 있다
하지만 모든 필드를 사용할 때 얻을 수 있는 장점이 다음과 같기 때문에 기본 전략으로 활용한다

  1. 모든 필드를 사용하면 update query가 항상 같다. 따라서 Application Loading시점에 update query를 미리 생성해두고 재사용할 수 있다
  2. DB에 동일한 쿼리를 보내면 DB는 이전에 미리 parsing된 쿼리를 재사용할 수 있다


만약 모든 필드를 사용하고 싶지 않다면 "@DynamicUpdate" 애노테이션을 사용하면 된다
이 애노테이션은 변경 대상 Entity 위에다 붙여주면 된다

update뿐만 아니라 데이터를 저장할 때 NULL이 아닌 필드만 insert하는 "@DynamicInsert"도 존재한다

@Data
@Entity
@Table(name = "member")
@NoArgsConstructor
@AllArgsConstructor
@DynamicUpdate
@DynamicInsert
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "user_name")
    private String username;

    @Column(name = "age")
    private Integer age;
    
    ....
    ....
 }
Member memberA = new Member("MemberA");
em.persist(memberA);

memberA.setUsername("MemberA-Update");
Hibernate: 
    /* insert PersistenceContext.domain.Member
        */ insert 
        into
            member
            (user_name) 
        values
            (?)
Hibernate: 
    /* update
        PersistenceContext.domain.Member */ update
            member 
        set
            user_name=? 
        where
            id=?

@DynamicUpdate와 @DynamicInsert를 사용하니까 설정하지 않은 "age"에 대해서는 값이 들어가지 않음을 확인할 수 있었다

여기서 이전에 Member의 age를 "int age"로 설정하였는데 자바에서 primitive type에는 null이 들어갈 수 없기 때문에 age를 설정하지 않았음에도 불구하고 자동적으로 "0"이 들어가서 update query에서 age가 들어갔었다.
이를 해결하기 위해서 int age -> Integer age로 변경해서 @DynamicUpdate를 정상적으로 확인할 수 있었다


일반적으로 테이블의 컬럼이 30개이상이면 @DynamicUpdate가 더 빠르다고 알려져있다
하지만 테이블의 컬럼이 30개 이상이라는 의미는 애초부터 테이블 설계를 잘못하였고 정규화가 제대로 이루어지지 않았다는 의미이므로 테이블의 설계부터 먼저 확인하자

 

5. 지연 로딩 (LAZY Loading)

 

[JPA] 즉시로딩 & 지연로딩

선수 Entity와 팀 Entity가 존재하고 서로 다대일 관계이다. @Entity @Table(name = "member") public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "member_i.." dat..

cs-ssupport.tistory.com


Flush

Flush는 영속성 컨텍스트의 변경 내용을 DB에 반영한다는 의미이고 메소드이다

em.flush()를 호출하게 되면 다음과 같은 일이 발생한다

  1. Dirty Checking을 통해서 {Entity & Snapshot}간의 변화를 감지해서 수정된 Entity를 찾고, 그에 대한 update query를 생성해서 쓰기 지연 SQL 저장소에 등록한다
  2. (1) 과정이 끝나면 드디어 쓰기 지연 SQL 저장소의 모든 query가 DB로 flush된다


영속성 컨텍스트를 flush하는 방법은 총 3가지이다

1. em.flush()

em.flush()를 직접 호출해서 영속성 컨텍스트를 "강제로" flush한다
일반적으로 이 강제 호출은 테스트나 다른 프레임워크 + JPA를 혼용해서 사용하는 경우 이외에는 잘 사용하지 않는다

  • 영속성 컨텍스트에서 영속화되고 있는 Entity들에 대한 성능 이점에 매우 크기 때문에 굳이 필요하지 않으면 강제로 flush할 이유가 없다

 

2. Transaction.commit()

commit()을 한다는 것은 변경된 내역에 대한 최종 승인을 하겠다는 것이다.
따라서 commit()을 하면 쓰기 지연 SQL 저장소의 모든 쿼리들이 flush됨에 따라 DB에는 변경된 데이터들이 저장된다

만약 commit전에 flush를 하지 않는다면 영속성 컨텍스트의 변경 내용이 DB에 반영되지 않는다. 이러한 문제를 예방하기 위해서 JPA는 트랜잭션을 commit할 때 자동으로 flush를 해준다

 

 

3. JPQL query

Member memberA = new Member("MemberA", 20);
Member memberB = new Member("MemberB", 30);

em.persist(memberA);
em.persist(memberB);

List<Member> result = em.createQuery(
        "select m from Member m",
        Member.class
).getResultList();

for (Member member : result) {
    System.out.println(member);
}

System.out.println("commit전 =====================================================");

tx.commit();

System.out.println("commit후 =====================================================");

중간에 JPQL쿼리를 통해서 Member를 찾아오는 로직이 보이고, commit 전 후로 구분자를 설정해놓았다
과연 이 코드들의 결과는 어떻게 될까

Hibernate: 
    /* insert PersistenceContext.domain.Member
        */ insert 
        into
            member
            (age, user_name) 
        values
            (?, ?)
            
Hibernate: 
    /* insert PersistenceContext.domain.Member
        */ insert 
        into
            member
            (age, user_name) 
        values
            (?, ?)
            
Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.id as id1_0_,
            member0_.age as age2_0_,
            member0_.user_name as user_nam3_0_ 
        from
            member member0_
            
Member(id=1, username=MemberA, age=20)
Member(id=2, username=MemberB, age=30)
commit전 =====================================================
commit후 =====================================================

commit을 하기전에 이미 모든 쿼리들이 flush된 것을 확인할 수 있다

JPQL에 대한 자세한 내용은 이후 포스팅에서 할 예정이지만, JPQL을 활용하면 commit하기 전에 모든 query가 flush된다는 사실을 알고 있으면 된다

  • JPQL select는 DB로부터 직접 데이터를 조회하기 때문에 DB & 영속성 컨텍스트 간에 데이터 정합성 문제가 있어서는 안된다. 따라서 JPQL query를 날리는 순간 flush가 먼저 이루어지고 DB와 영속성 컨텍스트 간에 데이터 동기화를 수행해준다

 

Flush Mode Option

EntityManager에 Flush Mode를 직접 지정하려면 "FlushModeType"을 사용하면 된다

em.setFlushMode(FlushModeType.AUTO);
em.setFlushMode(FlushModeType.COMMIT);

FlushModeType.AUTO는 commit이나 JPQL을 실행할 때 flush한다는 default value이다
FlushMode.COMMIT은 commit할 때만 flush를 하는 것이다

대부분의 경우 그냥 default를 사용한다. COMMIT mode는 "성능 최적화"를 위해 사용할 수 있다

<Flush 주의>
flush를 한다고해서 영속성 컨텍스트에서 관리되고있는 모든 Entity가 날라간다는 것은 절대 아니다
flush란 "영속성 컨텍스트의 변경 내역"을 DB에 동기화 시키는 것이다

 


준영속

영속 상태의 Entity가 영속성 컨텍스트로부터 분리된 것을 "준영속 상태"라고 한다
따라서 준영속 상태의 Entity는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다

  • 1차 캐시
  • 쓰기 지연
  • 변경 감지
  • 지연 로딩
  • ....


영속 상태의 Entity를 준영속 상태로 만드는 방법은 크게 3가지가 있다

  1. em.detach(entity) : 특정 엔티티만 준영속 상태로 전환
  2. em.clear() : 영속성 컨텍스트 초기화
  3. em.close() : 영속성 컨텍스트 종료


영속 상태의 Entity를 준영속 상태로 만들면 영속성 컨텍스트가 더이상 관리하지 않음과 동시에 "쓰기 지연 SQL 저장소에 존재하는 해당 Entity와 관련된 모든 query를 삭제"한다

병합 : merge()

준영속 상태의 Entity를 다시 영속 상태로 만드는 방법은 merge()를 하면 된다

public <T> T merge(T entity);
>> Member mergeMember = em.merge(member);

member는 준영속 상태인데 em.merge(member)를 통해서 새로운 영속 상태의 Entity인 "mergeMember"가 return된다

member에 대해서 em.merge()를 하더라도 member Entity가 영속화되는것이 아니고, member를 새로운 영속 상태로 만든 "mergeMember"가 영속화 된다

물론 준영속 상태의 Entity가 아닌 "비영속 상태의 Entity"도 merge를 통해서 영속 상태로 전환할 수 있다

※ Example

1. 비영속 상태

// 비영속 상태
Member member = new Member("memberA", 30);
System.out.println("===== 비영속 상태 =====");
System.out.println("member = " + member);
System.out.println("em.contains(member)? = " + em.contains(member) + "\n");
===== 비영속 상태 =====
member = Member(id=null, username=memberA, age=30)
em.contains(member)? = false

비영속상태 이므로 당연히 id(PK)는 null이고 영속성 컨텍스트에서 관리조차 되고 있지 않는 것을 확인할 수 있다

 

2. 비영속 -> 영속 (persist)

// 비영속 -> 영속 상태
em.persist(member);
System.out.println("===== 영속 상태 =====");
System.out.println("member = " + member);
System.out.println("em.contains(member)? = " + em.contains(member) + "\n");
Hibernate: 
    /* insert PersistenceContext.domain.Member
        */ insert 
        into
            member
            (age, user_name) 
        values
            (?, ?)
===== 영속 상태 =====
member = Member(id=1, username=memberA, age=30)
em.contains(member)? = true

비영속상태의 member Entity를 영속 상태로 만들었다
여기서 의문점은 em.flush()나 em.close()나 JPQL을 사용하지 않았음에도 불구하고 flush를 통해서 쿼리가 나갔다는 점이다

영속성 컨텍스트에서 관리가 되려면 해당 Entity는 "식별자"가 반드시 필요하다
하지만 현재 MySQL을 사용하고 있고 PK 생성 전략을 IDENTITY로 하였기 때문에 PK는 DB로 query가 날라가야 얻을 수 있다
근데 여기서 또 반복적으로 드는 의문은 {em.flush(), em.close(), JPQL}을 사용하지 않았기 때문에 flush가 발생할 수 없다는 것이다

<@GeneratedValue(strategy = GenerationType.IDENTITY)>
IDENTITY 생성 전략에서는 "예외적으로" persist를 하는 순간 즉시 query가 날라간다.
이 이유는 영속성 컨텍스트에서 관리가 되려면 "식별자"가 반드시 필요하고 이 식별자를 위해서 어쩔수없이 query를 날려서 자동 생성된 식별자를 가져와야 하기 때문이다

IDENTITY가 아닌 다른 전략(sequence, table)은 PK 생성을 위한 테이블을 별도로 만들기 때문에 query를 날리지 않아도 별도 테이블에서 식별자를 가져올 수 있다

"IDENTITY만 예외적으로 persist하는 순간 query가 날라가게 된다"

 

3. 영속 -> 준영속 (detach)

// 영속 -> 준영속 상태
em.detach(member);
System.out.println("===== 준영속 상태 =====");
System.out.println("member = " + member);
System.out.println("em.contains(member)? = " + em.contains(member) + "\n");
===== 준영속 상태 =====
member = Member(id=1, username=memberA, age=30)
em.contains(member)? = false

detach를 통해서 member를 영속성 컨텍스트 관리 대상에서 제외시켰다

 

4. 준영속 상태 Entity 데이터 변경 & DB 조회

이제 준영속 상태인 member Entity에 대해서 데이터를 변경해보고, 이 데이터 변경이 반영되었는지 DB에서 조회해보자

member.setUsername("memberA-Update");
member.setAge(60);
System.out.println("===== 준영속 상태에서 데이터 변경 =====");
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember = " + findMember + "\n");
===== 준영속 상태에서 데이터 변경 =====
Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.age as age2_0_0_,
        member0_.user_name as user_nam3_0_0_ 
    from
        member member0_ 
    where
        member0_.id=?
findMember = Member(id=1, username=memberA, age=30)

select query는 데이터가 반영되었나 확인하기 위해서 em.find를 사용했기 때문에 발생한 query이다
조회된 findMember를 보면 준영속 상태에서 변경한 데이터가 DB에 반영되지 않음을 확인할 수 있다

 

5. 준영속 -> 영속 (merge)

// 준영속 -> merge -> 영속 전환
Member mergeMember = em.merge(member);
System.out.println("===== 준영속 -> 영속 전환(merge) =====");
System.out.println("member = " + member);
System.out.println("mergeMember = " + mergeMember);
System.out.println("em.contains(member)? = " + em.contains(member));
System.out.println("em.contains(mergeMember)? = " + em.contains(mergeMember) + "\n");

tx.commit();
===== 준영속 -> 영속 전환(merge) =====
member = Member(id=1, username=memberA-Update, age=60)
mergeMember = Member(id=1, username=memberA-Update, age=60)
em.contains(member)? = false
em.contains(mergeMember)? = true

Hibernate: 
    /* update
        PersistenceContext.domain.Member */ update
            member 
        set
            age=?,
            user_name=? 
        where
            id=?

준영속 상태인 member에 대해서 merge를 하니까 "이 member를 영속 상태로 전환한 새로운 Entity" : mergeMember가 return되었다

이 과정은 다음과 같이 이루어진다

  1. 파라미터로 넘어온 "준영속 상태 member"의 식별자를 통해서 DB에서 조회를 해온다
  2. 조회한 mergeMember에 "member Entity의 값"을 채워넣는다
    • 여기서 member("memberA-Update", 60)이 mergeMember에 그대로 반영이 된다
  3. 최종적으로 mergeMember를 1차 캐시에 등록해서 영속화를 시켜준다


결과를 보면 member는 em의 관리 대상이 아니고, mergeMember가 em의 관리대상임을 확인할 수 있다

>> 따라서 member는 여전히 준영속 상태이고 더이상 사용할 필요가 없다. 반면 mergeMember는 영속 상태이고 이제 이 Entity를 사용하면 된다


그리고 commit() 후에 update query는 일단 mergeMember를 처음 조회하면 {"memberA", 30}의 데이터를 가지고 있을 것이다
이후에 과정 (2)에서 member Entity의 값인 {"memberA-Update", 60}을 mergeMember에 채워넣었기 때문에 "스냅샷과 Entity가 달라진다"

따라서 commit시점에 스냅샷과 Entity를 비교해서 발생한 update query가 나감을 확인할 수 있다

준영속 엔티티를 참조하던 변수 member는 더이상 사용할 필요가 없다
하지만 혼란으로 인해 비즈니스 로직에서 member를 사용할 가능성이 존재하기 때문에 위의 코드는 약간의 불안함을 가지고 있다
따라서 mergeMember라는 새로운 참조를 생성하지 말고 원래 존재하던 member 변수의 참조를 바꿔주면 된다
member = em.merge(member);