본문 바로가기

Spring/JPA

JPA- 프록시

프록시란

 프록시는 가짜 객체를 의미한다. em.find()를 하면 DB나 영속성 컨텍스트에서 실제 엔티티 객체를 찾아서 반환하는 것과는 달리, 프록시는 실제 객체가 아니다.

  - em.getReference() 

 프록시를 조회하기 위해선 em.getReference() 메서드를 이용한다.

비교를 위해서 em.find()문 사용시 쿼리를 보자.

 

em.find()로 Member 객체 조회 

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("hello");
            em.persist(member);

            // DB에 쿼리 날리고 영속성 컨텍스트 비움
            em.flush();
            em.clear();
			
            //em.find로 Member 조회
            Member findMember = em.find(Member.class, member.getId());

            tx.commit();
        } catch (Exception e) {
            tx.rollback(); //오류 발생 시 롤백
        } finally {
            //종료
            em.close();
        }
        emf.close();
    }
}

결과

Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (createdBy, createdDate, modifiedBy, modifiedDate, TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_1_0_,
        member0_.createdBy as createdb2_1_0_,
        member0_.createdDate as createdd3_1_0_,
        member0_.modifiedBy as modified4_1_0_,
        member0_.modifiedDate as modified5_1_0_,
        member0_.TEAM_ID as team_id7_1_0_,
        member0_.USERNAME as username6_1_0_,
        team1_.TEAM_ID as team_id1_4_1_,
        team1_.createdBy as createdb2_4_1_,
        team1_.createdDate as createdd3_4_1_,
        team1_.modifiedBy as modified4_4_1_,
        team1_.modifiedDate as modified5_4_1_,
        team1_.name as name6_4_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?

당연히 em.find()를 한 경우 Member 객체를 찾기 위해서  위와 같이 select 문이 나갈 것이다.

그렇다면 이번에는 em.getReference() 메서드를 이용해서 조회를 해보자.

 

em.getReference()를 통한 Member 조회

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("hello");
            em.persist(member);

            //DB에 쿼리 날리고 영속성 컨텍스트 비움
            em.flush();
            em.clear();

            Member refMember = em.getReference(Member.class, member.getId());

            tx.commit();
        } catch (Exception e) {
            tx.rollback(); //오류 발생 시 롤백
        } finally {
            //종료
            em.close();
        }
        emf.close();
    }
}

결과

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (createdBy, createdDate, modifiedBy, modifiedDate, TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?, ?)

 select문이 나가지 않았다. 결론을 말하자면 em.getReference()를 사용하는 경우, 조회한 값(프록시)가 실제로 사용되는 시점 이전에는 DB에 쿼리를 날리지 않는다.

다시 말하자면, em.getReference()로 조회한 refMember를 가지고 refMember.getUserName()과 같이 refMember를 직접 사용하지 않는 이상 DB에서의 조회를 하지 않는다.

이것을 "지연 로딩"이라고 한다.

 

em.find()와 em.getReference()

 

  프록시 객체 확인

Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember.getClass() = " + refMember.getClass());


출력>>
refMember.getClass() = class hellojpa.Member$HibernateProxy$y3CrJCu3  //프록시 객체

 getClass()로 refMember를 보면 일반 클래스가 아닌 "HibernateProxy", 즉 Hibernate가 만들어준 가짜 객체라는 것을 알 수 있다.

프록시 객체 추가 설명

 프록시는 실제 클래스를 상속받아서 생성되기 때문에 모습은 완전히 동일하다. 초기에 프록시 객체 내부에 값이 들어있지 않다는 것만 제외하면 일반 엔티티 객체와 동일하다.

 

 그리고 프록시는 DB에 저장 되어있는 실제 객체의 참조를 가지고 있다가, 프록시가 호출되는 경우 프록시가 참조하고 있던 타겟의 메소드를 반환하게 된다.

 

프록시 객체가 호출되었을 때의 과정은 위 그림과 같다. 

  1. 프록시가 호출
  2. 영속성 컨텍스트에 초기화를 요청
  3. 영속성 컨텍스트가 DB에서 타깃을 조회
  4. 타깃 객체를 반환하여 실제 엔티티 생성
  5. 타깃의 메소드를 호출

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 member1 = new Member();
            member1.setUserName("member1");
            em.persist(member1);

            Member member2 = new Member();
            member2.setUserName("member2");
            em.persist(member2);

            em.flush();
            em.clear();

            Member findMember1 = em.find(Member.class, member1.getId());
            Member findMember2 = em.getReference(Member.class, member2.getId());

            // 실제 객체
            System.out.println("findMember1.class == " + findMember1.getClass());
            // 프록시 객체
            System.out.println("findMember2.class == " + findMember2.getClass());
            // "==" 비교 시
            System.out.println("findMember1.class == findMember2.class : " +
                    (findMember1.getClass() == findMember2.getClass()));
            // instanceof 비교시
            System.out.println("findMember1.class == Member's instance : " + (findMember1 instanceof Member));
            System.out.println("findMember2.class == Member's instance : " + (findMember2 instanceof Member));
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback(); //오류 발생 시 롤백
        } finally {
            //종료
            em.close();
        }
        emf.close();
    }
}

findMember1.class == class hellojpa.Member
findMember2.class == class hellojpa.Member$HibernateProxy$lB6r1CZJ
findMember1.class == findMember2.class : false
findMember1.class == Member's instance : true
findMember2.class == Member's instance : true

 지금 findMember와 refMember는 조회하는 엔티티가 다르다는 것을 잊지말자. findMember는 member1을 조회하고, refMember는 member2를 조회한다는 것이다. 그 후 아래 내용을 보자

 영속성 컨텍스트에 이미 찾는 내용이 있는 상황에서 em.getReference

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 member1 = new Member();
            member1.setUserName("member1");
            em.persist(member1);

            Member member2 = new Member();
            member2.setUserName("member2");
            em.persist(member2);
            
            // DB에 쿼리 날리고 영속성 컨텍스트 비움
            em.flush();
            em.clear();
			
            //find를 통해서 영속성 컨텍스트에 member가 저장
            Member findMember = em.find(Member.class, member1.getId());  
            Member refMember = em.getReference(Member.class, member1.getId());

            // 실제 객체
            System.out.println("findMember.class == " + findMember.getClass());
            // 프록시 객체
            System.out.println("refMember.class == " + refMember.getClass());
            // "==" 비교 시
            System.out.println("findMember.class == refMember.class : " +
                    (findMember.getClass() == refMember.getClass()));
            // instanceof 비교시
            System.out.println("findMember.class == Member's instance : " + (findMember instanceof Member));
            System.out.println("refMember.class == Member's instance : " + (refMember instanceof Member));

            tx.commit();
        } catch (Exception e) {
            tx.rollback(); //오류 발생 시 롤백
        } finally {
            //종료
            em.close();
        }
        emf.close();
    }
}

findMember.class == class hellojpa.Member
refMember.class == class hellojpa.Member
findMember.class == refMember.class : true
findMember.class == Member's instance : true
refMember.class == Member's instance : true

 지금 보면 프록시 객체와 실제 객체 모두 member1이라는 공통적인 엔티티를 조회하였다. 그리고  refMember의 클래스를 보면 위에서와는 달리 프록시가 아닌 실제 Member 클래스이라는 것을 알 수 있다.

이는 영속성 컨텍스트에 있는 엔티티를 조회 시에 프록시는 실제 엔티티를 반환받기 때문이다.

find 먼저 getReference 먼저?

 그리고 추가적으로 개발의 편의성("==" 비교의 편의성)을 위해서 jpa는

먼저 em.find로 조회한 경우 후에 em.getReference()로 조회하여도 실제 객체로 반환하고,

먼저 em.getReference()로 조회한 경우 후에 em.find()로 조회하여도 프록시로 반환한다.

 

즉, 먼저 조회한 거 따라간다.

// 실제 객체
System.out.println("findMember.class == " + findMember.getClass());
// 프록시 객체
System.out.println("refMember.class == " + refMember.getClass());

출력>>
findMember.class == class hellojpa.Member
refMember.class == class hellojpa.Member
// getReference() 먼저
Member refMember = em.getReference(Member.class, member1.getId());
// find() 나중
Member findMember = em.find(Member.class, member1.getId());

출력>>
findMember.class == class hellojpa.Member$HibernateProxy$TxcxaxBI
refMember.class == class hellojpa.Member$HibernateProxy$TxcxaxBI

'Spring > JPA' 카테고리의 다른 글

JPA- Cascade와 고아 객체  (0) 2023.03.21
JPA- 즉시 로딩, 지연 로딩  (0) 2023.03.16
JPA- @MappedSuperClass  (0) 2023.03.12
JPA- 상속관계 매핑  (0) 2023.03.12
JPA- 다대일, 일대일  (0) 2023.03.08