[ JPA ] 프록시와 getReference()
<인프런의 김영한님의 강의를 보고 정리한 내용입니다>
🎈 find()와 getReference()의 차이
- find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
- getReference() : 데이터베이서 조회를 미루는 가짜(프록시) 엔티티 객체 조회
먼저 find를 보면
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setUsername("user1");
em.persist(member);
em.flush();
em.clear();
System.out.println("==========================");
Member reference = em.find(Member.class, member.getId());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
의 결과를 보면,
조회하는 select 쿼리가 나가는 것을 알 수 있다.
하지만 getReference() 같은 경우는,
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setUsername("user1");
em.persist(member);
em.flush();
em.clear();
System.out.println("==========================");
Member reference = em.getReference(Member.class, member.getId());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
select 쿼리가 나가지 않는다.
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setUsername("user1");
em.persist(member);
em.flush();
em.clear();
Member reference = em.getReference(Member.class, member.getId());
System.out.println("==========================");
System.out.println("reference.getId() = " + reference.getId());
System.out.println("reference.getUsername() = " + reference.getUsername());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
이 코드의 결과는 어떻게 될까?
id값은 기존에 알고 있으니 쿼리가 나가지 않지만, 그 외에 reference에 대한 것 중 DB를 조회해야 한다면 select 쿼리를 보내게 된다.
그 전에 이 reference의 정체는 뭘까?
reference.getClass()
를 통해 출력해보면,
Member 객체가 아니라 프록시 객체라는 것을 알 수 있다. 그림으로 설명하면,
껍데기는 같지만, 속은 비어있다고 생각하면 되고, 여기서 target은 진짜 reference를 가리킨다. 그래서 getReferce()만을 했을 때는 null 값이지만 실제 객체가 생성되면 그 객체를 가리키게 된다. 이 프록시 객체는 실제 클래스는 상속 받아서 만들어짐.
이런 상태인데 만약 프록시의 getName()을 호출한다면 실제 엔티티에 있는 getName()을 대신 호출한다.
좀 더 자세히 봐보면,
Member reference = em.getReference(Member.class, member.getId());
reference.getName();
이 코드를 실행한다고 했을 때
첫 호출시에 이 순서를 가지게되고 한 번 초기화 되면 그 이후부터는 바로 Member 객체를 확인한다.
이제 이 프록시 객체의 특징은
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니고 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 프록시 객체는 원본 엔티티를 상속 받음, 따라서 타입 체크시 주의해야 한다. ( ==(동등연산자) 비교 대신, instance of를 사용한다)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있다면, em.getReference()를 호출해도 실제 엔티티를 반환한다.
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생.
특징 중에서
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니고 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
이라는 특징의 예시를 보면,
System.out.println("reference.getClass() = " + reference.getClass());
System.out.println("reference.getUsername() = " + reference.getUsername());
System.out.println("reference.getClass() = " + reference.getClass());
프록시인 상태에서 select 쿼리를 조회하는 getUsername()을 호출 하더라도 확인해 보면 그대로 프록시 객체라는 것이다.
특징 중에서 instance of를 사용하는 것도 같은 이유이다.
반대로 find부터 호출하면 어떻게 될까?
Member findMember = em.find(Member.class, member.getId());
System.out.println("==========================");
System.out.println("findMember.getClass() = " + findMember.getClass());
Member reference = em.getReference(Member.class, member.getId());
System.out.println("reference.getClass() = " + reference.getClass());
getReference()로 하더라도 프록시가 아니라 원래 객체인 것을 확인할 수 있다. 즉 JPA는 같은 객체를 가리키는 객체들은 같은 값임이 보장이 되야한다.
System.out.println("a == a: " + (findMember == reference));
즉 이 값이 true임이 보장 되어야 한다.
이 말은 다시 말해서
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setUsername("user1");
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("==========================");
System.out.println("findMember.getClass() = " + refMember.getClass());
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.getClass() = " + findMember.getClass());
System.out.println("refMember == findMember: " + (refMember == findMember));
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
코드에서 refMember == findMember가 true가 되어야 하기에
첫 실행된 프록시 객체로 통일된 것을 알 수 있다.
이제 특징 중
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생.
의 예시를 봐보면,
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setUsername("user1");
em.persist(member);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember.getClass() = " + refMember.getClass());
em.detach(refMember);
refMember.getUsername();
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
위 코드는 getReference()를 진행한 후 영속성 컨텍스트에서 제거하고 다시 refMember에 대한 이름을 호출하는 경우이다. 결과를 보면,
에러가 발생함을 알 수 있다.
즉 detach로 영속성 컨텍스트에서 제거 되어 refMember.getUsername()은 영속성 컨텍스트의 도움을 받을 수 없게 되어 이런 에러가 발생하게 된다.
이 에러는 실무에 실제로 많이 볼 수 있으니 유의해야한다고 한다.
마지막으로 이런 프록시를 지원하는 메서드들이 있는데
- 프록시 인스턴스의 초기화 여부 확인
emf.getPersistenceUnitUtil().isLoaded(refMember);
- 프록시 강제 초기화
Hibernate.initialize(refMember);
이런 방법이 있다.
이 프록시 메커니즘을 알아야 하는 이유는 getReference()는 잘 사용하지 않지만 즉시 로딩이나 지연 로딩을 이해하기 위해 필요하기 때문이라고 한다.