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차 캐시에 존재하여 영속성 컨텍스트가 관리할 것이라 생각했다.
1차 캐시에 저장되는 내용
JPA는 트랜잭션
commit()
등으로flush()
가 일어나면 영속성 컨텍스트의 1차 캐시에서 엔티티와 스냅샷을 비교한다.비교를 통해 다른 부분이 있다면
UPDATE
쿼리를쓰기 지연 SQL 저장소
에 넣고 데이터베이스로 쿼리를 보낸다.
프록시 객체를 통해 메서드를 호출하여 원본 엔티티의 내용이 바뀌면, 변경 감지를 통해 데이터베이스에 쿼리를 보내야 한다.
-> 변경 감지를 위해서는 1차 캐시에 원본 엔티티가 존재해야 한다!!
EntityManager
의getReference()
,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 overrideshashCode()
. 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차 캐시에 존재한다(영속성 컨텍스트에서 관리된다)!!
em.contains()
동작 방식프록시와 원본 엔티티는 서로 다른 특성을 갖고있기 때문에 EntityManager
의 contains()
함수도 동작이 다를 것이라 생각해 코드를 확인해보았다.
특히 의문이 들었던 점은 초기화가 되지 않은 프록시는 무엇을 근거로 영속성 엔티티에 관리된다고 판단할 수 있는가? 였다.
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 anHibernateProxy
. 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();
}
EntityEntry
를 얻는다. 만약 null
이면 해당 엔티티가 영속성 컨텍스트에서 관리되지 않는다는 것이다.
null
이 아닌 경우, 해당 세션에서 관리되는 엔티티이며 EntityEntry
는 Hibernate
내부 로직을 위한 엔티티의 상태인 Status
를 지닌다.
Status
가 DELETED
또는 GONE
이 아니면 true
를 반환한다.
-> 해당 엔티티가 영속성 컨텍스트에서 관리되는지를 확인하고 상태가 DELETE
또는 GONE
이 아니라면 true
를 반환한다.
contains()
정리초기화되지 않은 프록시
현재 세션과 동일한 세션에 있는지를 반환
초기화된 프록시
원본 엔티티를 가져오고 이후 원본 엔티티의 경우와 동일하게 진행
원본 엔티티
EntityEntry
를 얻는 시도를 통해 영속성 컨텍스트에 관리되는지(관리 안되면 null
얻음)와 엔티티의 대한 정보를 확인하고 DELETE
나 GONE
상태가 아니면 true
반환