find와 getReference()의 차이를 학습하기위해 프록시에 대해 정리해봤습니다.
결론부터 둘의 차이를 알아보면,
먼저 테스트에 사용할 Member엔티티이다.
Member의 기본 정보가 있고, Team 엔티티를 매핑하고 있지만 사용하지 않을 예정
@Test
@DisplayName("프록시 객체 테스트")
public void proxyTest() {
Member member = new Member("member1", 10);
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember : " + refMember.getClass());
System.out.println("refMember : " + refMember.getId());
System.out.println("username : " + refMember.getUsername());
}
테스트 결과 getReference로 조회하면 프록시 객체를 조회하고, 호출할때 이미 id를 가지고 있어서 쿼리가 나가지 않는다.
엔티티의 정보를 조회하면, 프록시 초기화를 하면서 데이터를 가져오기 때문에 select 쿼리가 나간다.
이 프록시는 id값만 가지고 있고 target이 null로 된, 내부에는 비어있는 가짜 객체이다. 이때 target은 실제 객체를 가르키는 값이다.
실제 클래스를 상속받아 만들어져서, 실제 클래스와 겉모양이 같다.
사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지않고 사용하면 된다.
초기 프록시 객체의 target은 null 이기때문에 프록시 객체에 값을 요청하면 값을 알 수 없다.
그래서 영속성 컨텍스트에 초기화를 요청하고, 영속성 컨텍스트는 DB를 조회해서 실제 엔티티 객체를 생성한이후 프록시객체의 target을 실제 객체에 연결시켜준다.
target으로 실제 객체와 연결되면, 실제 객체의 메서드를 호출한다.
1. 실제 Entity 조회 후 프록시 객체를 조회하는 경우
@Test
@DisplayName("엔티티 조회 후 프록시 조회 테스트")
public void proxyTest() {
// given
Member member = new Member("member1", 10);
em.persist(member);
em.flush();
em.clear();
// when
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember : " + findMember.getClass());
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember : " + refMember.getClass());
// then
assertThat(findMember.equals(refMember));
}
findMember .getClass() → domain.Member (실제 객체)
reference.getClass() → domain.Member (실제객체)
findMember == reference → true
→ 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티가 반환된다.
이미 영속성 컨텍스트에 올라가있기때문에 굳이 프록시를 가져올 이유가없다.
성능최적화 입장에서도 영속성 컨텍스트에 올라간 실제 엔티티를 가져오면 된다.
2. 프록시 객체를 조회 후 실제 Entity를 조회하거나, 프록시객체를 초기화 하는 경우
@Test
@DisplayName("프록시 조회 후 엔티티를 조회")
public void proxyTest2() {
// given
Member member = new Member("member1", 10);
em.persist(member);
em.flush();
em.clear();
// when
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember : " + refMember.getClass());
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember : " + findMember.getClass());
// then
assertThat(refMember.equals(findMember));
}
refMember .getClass() → domain.MemberHibernateProxy (프록시 객체)
findMember .getClass() → domain.Member$HibernateProxy (프록시 객체)
→ 1번과 반대의 상황으로, 프록시가 먼저 초기화됐으면, em.find()를 호출해도 프록시 객체가 반환된다
* 실제 Entity를 조회하면서 select 쿼리는 나가지만, 프록시 객체이다.
결론
이처럼 JPA는 한 트랜잭션 내에서 실제 엔티티 객체와 프록시 객체의 비교연산 동작의 완전성을 보장하기 위해 두 객체의 클래스 타입이 동일하다.
but, 동일한 트랜잭션이 아닌경우, ==을 사용한다면 상황에 따라 결과가 달라질 수 있기 때문에 instanceof 를 사용하는것이 좋다.
Proxy이든 실제 엔티티이든 상관없이 개발에 문제없게 개발하는것이 중요하다.
❗ 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
프록시 초기화시 영속성 컨텍스트를 통해 초기화하게 되는데, 초기화 하기전에 영속성컨텍스트와 연결할 수 없는 경우
@Test
@DisplayName("프록시 초기화 전 영속성컨텍스트 에러")
public void proxyTest3() {
// given
Member member = new Member("member1", 10);
em.persist(member);
em.flush();
em.clear();
// when
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember : " + refMember.getClass());
em.detach(refMember); // 준영속상태로 변경
// em.close(); // 영속성 컨텍스트 종료
// em.clear(); // 영속성 컨텍스트 clear
// then
assertThatThrownBy(() -> refMember.getUsername())
.isInstanceOf(LazyInitializationException.class);
}
위 코드와 같이 em.detach()나 em.close(), em.clear()로 영속성 컨텍스트에서 detach시키거나 닫아서 준영속 상태에서 getUsername을 호출하면 LazyInitializationException 예외(could not initialize proxy)가 발생한다.
참고로 JPA 표준은 강제 초기화가 없어서 강제로 호출해야한다. 강제 호출 ex) member.getName()