[JPA] 프록시, 원본 엔티티의 영속성 컨텍스트 관리 여부 확인

Profile-exe·2024년 3월 2일
0

JPA

목록 보기
1/1
post-thumbnail

JPA 강의를 수강하면서 궁금한 점이 생겼다.

JPA는 동일성 보장을 위해 프록시 객체를 먼저 조회하면, 나중에 find()를 호출하더라도 프록시 객체인 동일한 인스턴스를 반환한다는 사실을 알게되었다.

  • 예제 코드
Member m1 = em.getReference(Member.class, 1L);
Member m2 = em.find(Member.class, 1L);

System.out.println("m1.getClass() = " + m1.getClass());
System.out.println("m2.getClass() = " + m2.getClass());
System.out.println("(m1 == m2) = " + (m1 == m2));
  • 출력 결과
m1.getClass() = class hellojpa.Member$HibernateProxy$FobBtKKE
m2.getClass() = class hellojpa.Member$HibernateProxy$FobBtKKE
(m1 == m2) = true

em.find() 시점에서 프록시 객체가 초기화되며 원본 엔티티의 인스턴스가 메모리에 올라갈텐데, 이 인스턴스는 어디에 있는걸까?

원본 엔티티는 어디에 있는거지??

1차 캐시에 원본 엔티티와 프록시 객체 둘 중 하나가 존재하는 것일까?
아니면 둘 다 존재하는 것일까?


가설 세우기

나는 후술할 메커니즘을 근거로 프록시와 원본 모두 1차 캐시에 존재하여 영속성 컨텍스트가 관리할 것이라 생각했다.

변경 감지(Dirty-Checking)

1차 캐시에 저장되는 내용

  • 영속화된 엔티티
  • 스냅샷: 엔티티가 영속화된 시점의 상태

JPA는 트랜잭션 commit() 등으로 flush()가 일어나면 영속성 컨텍스트의 1차 캐시에서 엔티티와 스냅샷을 비교한다.

비교를 통해 다른 부분이 있다면 UPDATE 쿼리를 쓰기 지연 SQL 저장소에 넣고 데이터베이스로 쿼리를 보낸다.

프록시 객체를 통해 메서드를 호출하여 원본 엔티티의 내용이 바뀌면, 변경 감지를 통해 데이터베이스에 쿼리를 보내야 한다.

-> 변경 감지를 위해서는 1차 캐시에 원본 엔티티가 존재해야 한다!!


동일성 보장

EntityManagergetReference(), find() 등으로 조회 시 1차 캐시를 우선 확인하여 해당 인스턴스가 있다면 반환한다.

이를 통해 JPA는 어플리케이션에서 REPEATABLE READ 수준의 트랜잭션 격리 수준을 제공한다.

getReference()로 프록시 객체를 조회한 후 find()를 호출하면 1차 캐시를 확인하여 프록시 객체가 반환된다.

마찬가지로 find()로 원본 엔티티를 조회한 후 getReference()를 호출하면 1차 캐시를 확인해 원본 엔티티가 반환된다.

-> 동일성 보장이 이루어지려면 1차 캐시에 프록시 객체가 존재해야 한다!!


필요한 유틸 함수들

Hibernate.unproxy() - 원본 엔티티 꺼내기

Hibernate.unproxy() - Hibernate JavaDoc

unproxy

public static Object unproxy​(Object proxy)

If the given object is not a proxy, return it. But, if it is a proxy, ensure that the proxy is initialized, and return a direct reference to its proxied entity object.

주어진 객체가 프록시 객체일 경우 해당 프록시를 초기화하고, 프록시가 가리키는 원본 엔티티 객체에 대한 직접 참조를 반환한다.

프록시 객체가 아닌 경우에는 객체 자체를 그대로 반환한다.

프록시가 가리키는 원본 엔티티 인스턴스는 $$_hibernate_interceptor 내부 target 필드에 저장되어 있다.

원본 엔티티에 this를 반환하는 메서드를 두면 안될까?

프록시는 원본 엔티티 자신을 리턴하는 경우 프록시 객체를 리턴하도록 구현되어 return this 로 원본 엔티티의 인스턴스를 얻을 수 없다.


em.contains() - 영속성 컨텍스트 관리(1차 캐시에 존재하는지) 확인

EntityManager.contains() - JAKARTA EE SPECIFICATIONS

contains

boolean contains(Object entity)

Check if the instance is a managed entity instance belonging to the current persistence context.

주어진 인스턴스가 현재 영속성 컨텍스트에 속하는 관리하는 인스턴스인지 확인한다.


System.identityHashCode() - 동일한 인스턴스인지 확인

System.identityHashCode() - JavaSE 8 document

identityHashCode

public static int identityHashCode(Object x)

Returns the same hash code for the given object as would be returned by the default method hashCode(), whether or not the given object's class overrides hashCode(). The hash code for the null reference is zero.

해당 객체의 hashCode() 재정의 유무와 무관하게 객체의 고유한 hashCode() 메서드 값(재정의 되기 전 hashCode() 함수 값)을 반환한다.

이 함수를 통해 프록시와 원본이 동일한 인스턴스인지 확인할 수 있다.


결과 확인

위 함수들을 이용하여 결과를 확인해보자.

  • 예제 코드
// 프록시 얻기
Member memberRef = em.getReference(Member.class, 1L);
System.out.println("Proxy  isManaged = " + em.contains(memberRef));
System.out.println("Proxy  hashCode  = " + System.identityHashCode(memberRef));

// 원본 얻기
Member entity = (Member) Hibernate.unproxy(memberRef);
System.out.println("Entity isManaged = " + em.contains(entity));
System.out.println("Entity hashCode  = " + System.identityHashCode(entity));
  • 출력 결과
Proxy  isManaged = true
Proxy  hashCode  = 270313690
Hibernate: 
    select
        m1_0.id,
        m1_0.name,
        m1_0.team_id 
    from
        Member m1_0 
    where
        m1_0.id=?
Entity isManaged = true
Entity hashCode  = 5987161

isManaged 가 모두 true 이며, hashCode 값이 서로 다르다.

unproxy()가 호출되며 SQL 쿼리가 나가고 프록시 객체가 초기화된 것을 볼 수 있다.

-> 프록시와 원본 모두 서로 다른 인스턴스이며 1차 캐시에 존재한다(영속성 컨텍스트에서 관리된다)!!


번외1 - em.contains() 동작 방식

프록시와 원본 엔티티는 서로 다른 특성을 갖고있기 때문에 EntityManagercontains() 함수도 동작이 다를 것이라 생각해 코드를 확인해보았다.

특히 의문이 들었던 점은 초기화가 되지 않은 프록시는 무엇을 근거로 영속성 엔티티에 관리된다고 판단할 수 있는가? 였다.

  • contains() 함수
public boolean contains(Object object) {
    ...
    if (object == null) {
        return false;
    } else {
        try {
            // HibernateProxy.extractLazyInitializer() : proxy 객체가 아니면 null을 반환한다!!!!!
            LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer(object);
            if (lazyInitializer != null) {                       // proxy 객체인 경우
                if (lazyInitializer.isUninitialized()) {         // proxy 객체가 초기화되지 않은 경우
                    return lazyInitializer.getSession() == this; // proxy 객체가 현재 세션에 속해있는지 확인
                }

                object = lazyInitializer.getImplementation();     // proxy 객체가 초기화된 경우, proxy 객체의 원본 엔티티를 가져온다.
            }

            // EntityEntry 객체는 엔티티의 생명주기 상태, 식별자, 버전 정보 등 엔티티와 관련된 다양한 메타데이터를 포함한다.
            EntityEntry entry = this.persistenceContext.getEntry(object);
            this.delayedAfterCompletion();
            if (entry == null) { // null이 반환된 경우 해당 엔티티가 영속성 컨텍스트에서 관리되지 않는다고 판단할 수 있다.
                ... 
                return false;
            } else {             // null이 아닌 경우 해당 엔티티가 영속성 컨텍스트에서 관리되고 있는 상태이다.
                // 엔티티의 상태 중 DELETE(삭제), GONE(데이터베이스에서 이미 삭제되었거나 더이상 접근이 불가능한 상태)가 아니라면 true를 반환한다.
                return !entry.getStatus().isDeletedOrGone();
            }
        } 
        ... catch 예외처리 ...
    }
}

예상대로 프록시인지, 원본 엔티티인지 분기를 두어 확인 후 각각 다르게 boolean 값을 반환했다.


프록시인 경우

HibernateProxy.extractLazyInitializer - Hibernate JavaDoc

Extract the LazyInitializer from the object, if and only if the object is actually an HibernateProxy. If not, null is returned.

프록시인 경우 LazyInitializer를 추출하고 프록시가 아니면 null을 반환한다.


LazyInitializer - Hibernate JavaDoc

Handles fetching of the underlying entity for a proxy.

프록시에 대한 원본 엔티티를 가져오는 것을 처리한다.


어떤 객체던 LazyInitializer 를 얻어서 1) 프록시인지 원본 엔티티인지를 확인하고, 프록시인 경우 lazyInitializer.isUninitialized() 함수를 통해 2) 초기화 유무를 확인한다.

초기화되지 않은 프록시의 경우

return lazyInitializer.getSession() == this

getSession()은 프록시가 연결된 세션을 가져오거나, 연결되지 않은 경우 null을 반환하며 이 세션을 this와 비교한다.

-> 현재 세션에 존재하는 프록시인지 여부를 반환한다.

초기화된 프록시의 경우

object = lazyInitializer.getImplementation();

getImplementation()은 프록시의 target인 원본 엔티티 인스턴스를 반환한다.

-> 원본 엔티티를 가져온 후 원본 엔티티를 넘겼을 때와 동일하게 진행한다.



원본인 경우

PersistenceContext.getEntry() - Hibernate Javadoc

Retrieve the EntityEntry representation of the given entity.

초기화된 프록시, 원본 엔티티를 contains()에 넘기면 프록시는 원본 엔티티를 꺼내오고 원본 엔티티에서 EntityEntry를 가져온다.


EntityEntry - Hibernate Javadoc

Information about the current state of a managed entity instance with respect to its persistent state.

EntityEntry는 영속성 컨텍스트에서 관리되는 엔티티 인스턴스의 현재 상태(Status)와 상태에 대한 정보다.


Status - Hibernate Javadoc

Represents the status of an entity with respect to this session.

엔티티의 세션에 대한 상태를 나타내는 데 사용되며 애플리케이션 수준에서 직접적으로 사용되거나 표시되는 개념이 아니다.

isDeletedOrGone 함수를 사용하므로 아래 두 상태만 소개했다.

DELETED: 엔티티가 삭제되어 데이터베이스에서 제거되는 상태
GONE: 엔티티가 더 이상 존재하지 않거나 접근할 수 없는 상태

// EntityEntry 객체는 엔티티의 생명주기 상태, 식별자, 버전 정보 등 엔티티와 관련된 다양한 메타데이터를 포함한다.
EntityEntry entry = this.persistenceContext.getEntry(object);
this.delayedAfterCompletion();
if (entry == null) { // null이 반환된 경우 해당 엔티티가 영속성 컨텍스트에서 관리되지 않는다고 판단할 수 있다.
    ... 
    return false;
} else {             // null이 아닌 경우 해당 엔티티가 영속성 컨텍스트에서 관리되고 있는 상태이다.
    // 엔티티의 상태 중 DELETE(삭제), GONE(데이터베이스에서 이미 삭제되었거나 더이상 접근이 불가능한 상태)가 아니라면 true를 반환한다.
    return !entry.getStatus().isDeletedOrGone();
}
  1. EntityEntry를 얻는다. 만약 null이면 해당 엔티티가 영속성 컨텍스트에서 관리되지 않는다는 것이다.

  2. null이 아닌 경우, 해당 세션에서 관리되는 엔티티이며 EntityEntryHibernate 내부 로직을 위한 엔티티의 상태인 Status를 지닌다.

  3. StatusDELETED 또는 GONE이 아니면 true를 반환한다.

-> 해당 엔티티가 영속성 컨텍스트에서 관리되는지를 확인하고 상태가 DELETE 또는 GONE이 아니라면 true를 반환한다.


contains() 정리

  • 초기화되지 않은 프록시
    현재 세션과 동일한 세션에 있는지를 반환

  • 초기화된 프록시
    원본 엔티티를 가져오고 이후 원본 엔티티의 경우와 동일하게 진행

  • 원본 엔티티
    EntityEntry를 얻는 시도를 통해 영속성 컨텍스트에 관리되는지(관리 안되면 null 얻음)와 엔티티의 대한 정보를 확인하고 DELETEGONE 상태가 아니면 true 반환

profile
컴퓨터공학과 학부생

0개의 댓글