프로그래밍/Spring

[ JPA ] 프록시와 getReference()

Yanoo 2022. 1. 17. 00:26
728x90
반응형

<인프런의 김영한님의 강의를 보고 정리한 내용입니다>

 

 

🎈 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()는 잘 사용하지 않지만 즉시 로딩이나 지연 로딩을 이해하기 위해 필요하기 때문이라고 한다.

 


 

728x90
반응형