JPA 프록시

2025. 2. 22. 21:56JPA

JPA 프록시

-> 실제 엔티티의 데이터를 로딩하기 전까지 대리역할을 하는 객체

-> em.find()를 호출하면 데이터베이스를 통해 실제 엔티티를 조회하지만 em.getReference()를 호출하면 실제 엔티티가 아닌 프록시 엔티티 객체를 조회한다.

-> 실제 클래스를 상속받아 만들어진다.

-> 사용하는 입장에서는 해당 객체가 프록시인지 실제 객체인지 구분하지 않고 사용한다.

-> 프록시 객체는 실제 객체의 참조(target)을 보관한다.

-> 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드를 호출한다.

 

◎ 프록시 객체의 초기화 과정

-> 아래 코드를 실행했을 때의 과정이다.

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

-> em.getReference()를 통해 반환받은 member는 Member의 프록시 객체이다.

1. getName()을 호출하면 Member target에 아직 아무 값이 없는 것을 확인한다.

2. JPA가 영속성 컨텍스트에 실제 Member 객체를 요청한다,.

3. 영속성 컨텍스트는 DB를 조회한다.

4. DB를 조회해서 실제 Member 객체를 생성한다.

5. Member Proxy의 Member target(Member 변수)와 실제 Member 객체를 연결한다.

- 따라서 get.Name()을 했을 때 target과 연결된 실제 getName()이 호출되어 결과가 반환된다.

-> 아래는 위 과정을 코드로 작성한 것이다.

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

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

            Member findMember = em.getReference(Member.class, member.getId());
            System.out.println("findMember = " + findMember.getClass()); // 프록시 객체 조회
            System.out.println("findMember.getId() = " + findMember.getId()); 
            System.out.println("findMember.getUsername() = " + findMember.getUsername()); // 영속성 컨텍스에 요청해서 실제 객체를 받아옴
            System.out.println("findMember.getUsername() = " + findMember.getUsername()); // 이후에는 초기화되어 프록시 객체의 target에 실제 엔티티와 연결되었기 떄문에 초기화는 일어나지 않는다.

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        em.close();
        emf.close();
    }

-> 결과

findMember = class hellojpa.Member$HibernateProxy$cTPG3U6G
findMember.getId() = 1
Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.createdBy,
        m1_0.createdDate,
        m1_0.lastModifiedBy,
        m1_0.lastModifiedDate,
        t1_0.TEAM_ID,
        t1_0.createdBy,
        t1_0.createdDate,
        t1_0.lastModifiedBy,
        t1_0.lastModifiedDate,
        t1_0.name,
        m1_0.name 
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.TEAM_ID 
    where
        m1_0.MEMBER_ID=?
findMember.getUsername() = user -> 최초 getUsername()호출 시점에는 초기화가 진행되므로 select 쿼리가 발생한다.
findMember.getUsername() = user -> 이미 초기화되었기 때문에 바로 기존 값을 호출한다.

 

◎ 프록시 특징

-> 프록시 객체는 처음 사용할 때 한 번만 초기화된다.

-> 프록시 객체를 초기화할 때 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화가 되면 프록시 객체를 통해 실제 엔티티에 접근할 수 있게 되는 것이다.

-> 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입체크를 할 때 주의해야 한다.(== 비교 대신 instance of 사용할 것)

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

            Member member2 = new Member();
            member2.setUsername("hello");
            em.persist(member2);

            // 영속성 컨텍스트 초기화
            em.flush();
            em.clear();
             Member m1 = em.find(Member.class, member1.getId());
            Member m2 = em.getReference(Member.class, member2.getId()); // 이 경우 m1 == m2 는 false
            logic(m1, m2);
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        em.close();
        emf.close();
    }
    
    // 매개변수 타입이 프록시인지 실제 객체인지 알 수 없음 따라서 == 비교시 주의
    private static void logic(Member m1, Member m2) {
        System.out.println("============= logic() ===========");
        System.out.println("m1.getClass() == m2.getClass() : " + (m1.getClass() == m2.getClass()));
        System.out.println("m1 == m2 : " + (m1 == m2));
        System.out.println("m1 instanceof m2 " + (m1 instanceof Member));
        System.out.println("m2 instanceof m1 " + (m2 instanceof Member));
    }

 -> 결과

- m1은 em.find()로 호출한 실제 Member 객체이고, m2는 em.getReference()로 호출한 프록시 객체이다. 이 둘을 logic() 메서드를 통해 비교를 하게되면 logic()메서드의 파라미터로 들어오는 m1과 m2는 프록시 객체인지 실제 객체인지 알 수 없다.

- m1과 m2을 == 비교하면 false가 나온다.

- 실제 객체와 프록시 객체는 상속관계임을 이용해 instatnce of 를 통해 객체 비교를 해야 한다.

-> 영속성 컨텍스트에 이미 찾는 엔티티가 있다면 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("hello");
            em.persist(member1);


            // 영속성 컨텍스트 초기화
            em.flush();
            em.clear();
            
            /*
            member1이 한 번 실제 객체로 호출되고 이후에 member1을 getReference()하면 실제 객체가 호출된다.
             */
            Member m1 = em.find(Member.class, member1.getId());
            System.out.println("m1.getClass() = " + m1.getClass());

            Member reference = em.getReference(Member.class, member1.getId());
            System.out.println("reference.getClass() = " + findMember.getClass());
           
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }

        em.close();
        emf.close();
    }

- 결과

 

-> 프록시로 만들어진 엔티티에 대해 조회가 발생하면 엉속성 컨텍스트는 동일성을 보장하기 위해 이후에도 프록시 엔티티를 반환한다.

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

Member m2 = em.find(Member.class, member1.getId());
System.out.println("m2.getClass() = " + m2.getClass());

- 결과

m1.getClass() = class hellojpa.Member$HibernateProxy$JJNmmOQQ
select
        m1_0.MEMBER_ID,
        ...
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.TEAM_ID 
    where
        m1_0.MEMBER_ID=?
m2.getClass() = class hellojpa.Member$HibernateProxy$JJNmmOQQ

 

-> 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.

영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
             */
            Member refMember = em.getReference(Member.class, member1.getId());
            System.out.println("refMember.getClass() = " + refMember.getClass()); // proxy
			
            // 영속성 컨텍스트에서 관리하지 않도록 설정
            em.detach(refMember);
            em.close();
            refMember.getUsername();
            System.out.println("refMember = " + refMember.getUsername());

 - 결과

- 위와 같은 예외가 발생한다.

- 영속성 컨텍스트로 더 이상관리되지 않기 때문에 해당 프록시 객체를 접근하면 문제가 발생한다.

 

◎ 프록시 확인 기능

-> 프록시 인스턴스 초기화 여부 확인

PersistenceUnitUtil.isLoaded(Object entity)

Member member1 = new Member();
member1.setUsername("hello");
em.persist(member1);
// 영속성 컨텍스트 초기화
em.flush();
em.clear();

Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember.getClass() = " + refMember.getClass());
/*
프록시 확인
 */
// 초기화 전
System.out.println("============= refMember 초기화 전 ==============");
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
// 초기화 후
System.out.println("============= refMember 초기화 후 ==============");
refMember.getUsername();
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));
tx.commit();

 - 결과

refMember.getClass() = class hellojpa.Member$HibernateProxy$A3ibyNBZ
============= refMember 초기화 전 ==============
isLoaded = false
============= refMember 초기화 후 ==============
Hibernate: 
    select
        m1_0.MEMBER_ID,
        ...
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.TEAM_ID 
    where
        m1_0.MEMBER_ID=?
isLoaded = true

 

-> 프록시 클래스 확인

entity.getClass()

 

-> 프로깃 강제 초기화

Hibernate.initialize()

Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember.getClass() = " + refMember.getClass());
Hibernate.initialize(refMember); // 강제 초기화

- 결과

refMember.getClass() = class hellojpa.Member$HibernateProxy$yGRuEqxK
Hibernate: 
    select
        m1_0.MEMBER_ID,
        ...
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.TEAM_ID=m1_0.TEAM_ID 
    where
        m1_0.MEMBER_ID=?

 

 

★ 참고

자바 ORM 표준 JPA 프로그래밍 - 기본편

'JPA' 카테고리의 다른 글

영속성 전이 CASCADE & 고아 객체  (0) 2025.02.27
즉시 로딩 & 지연 로딩  (0) 2025.02.26
@PrePersist  (0) 2023.05.17
준영속 상태  (0) 2023.05.07
flush  (0) 2023.05.07