jpa 프록시
프록시와 엔티티 객체의 조회
- em.find() : db로 조회한다.
- em.reference() : db를 사용하지 않고 객체를 조회한다. 정확하게 프록시 엔티티 객체를 조회한다.
@Entity
@Setter
@Getter
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;
}
Member member = new Member();
member.setName("kim");
em.persist(member);
em.flush();
em.clear();
final Member findMember = em.getReference(Member.class, member.getId()); // 프록시로 맴버를 가져 온다.
// 프록시. 실제 엔티티를 상속받아서 만들며, 실제 엔티티 객체는 target으로 존재하며 그 값은 존재(null)하지 않는다.
System.out.println("findMember = " + findMember.getClass());
// 프록시 상태에서 해당 객체의 특정 값을 호출할 때, 프록시는 초기화된다. 초기화 될 때, 프록시는 실제 객체 엔티티(target)을 초기화 및 주입한다.
// 다만, 엔티티 객체를 호출하기 위해서는 id가 있어야 하므로, id를 호출할 때까지는 프록시를 유지한다.
System.out.println("findMember.id = " + findMember.getId());
// id이외의 데이터를 호출 할 때, 프록시는 해당 값을 가지지 못하므로, db에서 데이터를 가져와 엔티티 객체를 초기화 한다. 이 때 select 쿼리가 발생한다.
/* 쿼리 발생!! SELECT .... FROM MEMBER .... 쿼리 발생!! */
System.out.println("findMember.name = " + findMember.getName());
// 두 번째 select 쿼리는 발생하지 않는다. 왜냐하면 앞서 엔티티 객체가 초기화 되었기 때문이다.
System.out.println("findMember.name = " + findMember.getName());
// 엔티티 객체를 생성하더라도 프록시 객체는 유지가 된다.
System.out.println("findMember after= " + findMember.getClass());
tx.commit();
- 프록시의 용도는 모든 데이터를 반드시 호출해야 하냐는 쟁점으로부 발생한다. 그러니까 Member가 Team 객체를 가지고 있다고 가정한다. 가장 이상적인 형태는 Member가 team 객체를 계속 가지고 있는 상태일 것이며 이 경우 select .. join … 을 통해 두 개를 동시에 호출할 것이다.
- 하지만 Member만 필요한 경우 굳이 join을 통해 두 테이블을 조회할 필요는 없다. 이 때 프록시를 삽입한다. 프록시의 id를 호출할 때는 프록시를 유지하지만, 그 이상의 데이터를 요청할 때는 proxy 가 entity 객체를 호출하고 초기화한다.
- 위의 이미지처럼 프록시는 프록시가 상속한 대상이자 실제 데어티인 target을 null 로 가지고 있는다.
- 실제로 필요로한 순간(getName())에 select 쿼리로 db를 통해 데이터를 호출한다. 그리고 엔티티 객체를 넣는다. 프록시는 해당 엔티티객체에서 name을 출력하여 리턴한다.
객체는 어떻게 비교하는가?
하나의 트랜잭션(영속성 컨텍스트)에서 동일한 객체는 언제나 비교시 같아야 한다.
- JPA는 스펙을 통해 하나의 트랜잭션에서 같은 객체(레코드)를 비교할 경우 동일하다고 나와야 한다.
- 그러므로 getReference()로 호출한 객체에 대해서는 이후 find로 호출한다 하더라도 언제나 프록시로 리턴한다.
- 반대로 find로 호출한 객체에 대해서 차후에 getReference로 호출하더라도 항상 원래의 엔티티 클래스로 리턴한다.
Member member = new Member();
member.setName("kim");
em.persist(member);
em.flush();
em.clear();
final Member refMember = em.getReference(Member.class, member.getId());
final Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("refMember = " + refMember.getClass());
findMember.getName();
refMember.getName();
System.out.println("findMember after = " + findMember.getClass());
System.out.println("refMember After= " + refMember.getClass());
System.out.println("findMember == refMember : "+(findMember==refMember));
tx.commit();
Hibernate:
/* insert jpa6_proxy.a_proxy.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
Hibernate:
select
member0_.id as id1_0_0_,
member0_.name as name2_0_0_
from
Member member0_
where
member0_.id=?
findMember = class jpa6_proxy.a_proxy.Member$HibernateProxy$MEM5XBnM
refMember = class jpa6_proxy.a_proxy.Member$HibernateProxy$MEM5XBnM
findMember after = class jpa6_proxy.a_proxy.Member$HibernateProxy$MEM5XBnM
refMember After= class jpa6_proxy.a_proxy.Member$HibernateProxy$MEM5XBnM
findMember == refMember : true
- 위는 가장 먼저
em.getReference
으로 해당 객체를 출력하였고, 그것의 클래스가Member$HibernateProxy$MEM5XBnM
클래스임을 확인할 수 있다. - 그런데 아래의 쿼리는 em.find로 먼저 출력하였고 나머지 코드는 같다. 그러나 결괏값은 다름을 확인할 수 있다.
Member member = new Member();
member.setName("kim");
em.persist(member);
em.flush();
em.clear();
final Member findMember = em.find(Member.class, member.getId()); // 위 아래의 위치만 바꿨다!!
final Member refMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("refMember = " + refMember.getClass());
findMember.getName();
refMember.getName();
System.out.println("findMember after = " + findMember.getClass());
System.out.println("refMember After= " + refMember.getClass());
System.out.println("findMember == refMember : "+(findMember==refMember));
tx.commit();
Hibernate:
/* insert jpa6_proxy.a_proxy.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
Hibernate:
select
member0_.id as id1_0_0_,
member0_.name as name2_0_0_
from
Member member0_
where
member0_.id=?
findMember = class jpa6_proxy.a_proxy.Member
refMember = class jpa6_proxy.a_proxy.Member
findMember after = class jpa6_proxy.a_proxy.Member
refMember After= class jpa6_proxy.a_proxy.Member
findMember == refMember : true
동일한 데이터 타입간 비교는 어떻게 하는가?
- 동일한 객체(레코드) 간 비교는 JPA 스펙 상 무조건 class 가 일치함을 확인할 수 있다.
- 그렇다면 동일한 데이터 타입이지만 다른 객체일 경우 어떻게 하는가? 이 경우 프록시와 구현 클래스 중 무엇을 가지고 올지 알 수 없다. 그러므로 이 경우 == 이 아닌 instanceof를 통해 비교해야 한다. 프록시는 해당 클래스를 상속받기 때문에 instanceof Member를 사용하면 동일한 값(true)를 출력한다.
Member member = new Member();
member.setName("kim");
em.persist(member);
Member member2 = new Member();
member2.setName("lee");
em.persist(member2);
em.flush();
em.clear();
final Member m1 = em.find(Member.class, member.getId());
final Member m2 = em.getReference(Member.class, member2.getId());
System.out.println("m1 class : "+m1.getClass());
System.out.println("m2 class : "+m2.getClass());
System.out.println("m1 == m2 "+ (m1==m2));
System.out.println("m1 instanceof Member : " + (m1 instanceof Member));
System.out.println("m2 instanceof Member : " + (m2 instanceof Member));
tx.commit();
m1 class : class jpa6_proxy.a_proxy.Member
m2 class : class jpa6_proxy.a_proxy.Member$HibernateProxy$aMuI72wR
m1 == m2 false
m1 instanceof Member : true
m2 instanceof Member : true
준영속성 상태에서 프록시를 초기화할 수 없다.
- 만약 아래와 같이 영속성 컨텍스트를 종료하거나 해당 객체를 준영속 상태로 만들 경우 어떻게 될까?
Member member = new Member();
member.setName("kim");
em.persist(member);
em.flush();
em.clear();
final Member refMember = em.getReference(Member.class, member.getId());
em.detach(refMember);
refMember.getName();
tx.commit();
LazyInitializationException
예외를 발생하며, 해당 데이터를 위한 세션이 없다고 예외 처리한다.could not initialize proxy [jpa6_proxy.a_proxy.Member#1] - no Session
- 트랜잭션을 종료 후 데이타를 출력하는 경우가 있으므로, 실무 중 자주 만나는 예외라 한다.
기타
- 프록시의 초기화 여부(실제 엔티티의 초기화 여부)를 확인할 수 있다.
PersistenceUnitUtil.isLoaded(Object entity)
이는 엔티티 매니저 팩토리를 통해 가져온다. - 프록시의 초기화를 id를 제외한 필드의 조회를 통해 해왔다(getName()). 이를 매서드를 통해 할 수 있으며 그것은 다음과 같다.
org.hibernate.Hibernate.initialize(entity);
- 지연로딩 기법이란 Member 에서의 team을 프록시로 만들어 놓고, 나중에 필요할 때 엔티티 객체를 초기화하는 방법을 의미한다. 프록시를 기반으로 한다.