JPA 프록시

3 minute read

김영한 : 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 공부한 내용입니다.


프록시

프록시의 사전적 정의는 “대리인”이다.
JPA의 프록시는 진짜 Entity를 대리하는 가짜 Entity를 의미한다.


em.find(), em.getReference()

JPA는 em.find() 말고도 em.getReference()가 있다.

  • em.find() : 데이터베이스를 통해 실제 엔티티 객체를 조회한다.
  • em.getReference() : 데이터베이스 조회를 미루는 가짜 (프록시) 엔티티 객체를 조회한다.
public static void main(String[] args) {

    Member member = new Member();
    member.setName("hello");

    em.persist(member);

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

    Member findMember = em.find(Member.class, member.getId());

    System.out.println("findMember.getClass = " + findMember.getClass());
}

public static void main(String[] args) {

    Member member = new Member();
    member.setName("hello");

    em.persist(member);

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

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

    System.out.println("findMember.getClass = " + findMember.getClass());
}

결과적으로 둘의 행위는 값을 찾아온다는 의미로 (SELECT) 동일하다.

그러나 getClass를 통해 조회를 해보면 전혀 다른 성질을 가진다는 것을 알 수 있다.

findMember.getClass() = class me.gicheol.Member
findMember.getClass() = class me.gicheol.Member$HibernateProxy$oU7VU5wF

em.getReference()는 proxy 클래스임을 알 수 있다.

조회 결과도 조금 다르다.

em.find()를 사용한다면 바로 SELECT문을 통해 객체를 조회한다.

Hibernate: 
    /* insert me.gicheol.Member
        */ insert 
        into
            Member
            (createdAt, createdBy, lastUpdatedAt, lastUpdatedBy, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_3_0_,
        member0_.createdAt as createdA2_3_0_,
        member0_.createdBy as createdB3_3_0_,
        member0_.lastUpdatedAt as lastUpda4_3_0_,
        member0_.lastUpdatedBy as lastUpda5_3_0_,
        member0_.USERNAME as USERNAME6_3_0_,
        member0_.TEAM_ID as TEAM_ID7_3_0_,
        team1_.TEAM_ID as TEAM_ID1_7_1_,
        team1_.name as name2_7_1_ 
    from
        Member member0_ 
    left outer join
        Team team1_ 
            on member0_.TEAM_ID=team1_.TEAM_ID 
    where
        member0_.MEMBER_ID=?

그러나 em.getReference()는 SELECT를 하지 않는다.
em.getReference()를 통해 찾아온 객체의 값을 사용할 때 비로소 값을 조회하게 된다.

즉 em.getReference()를 통해 조회한 객체는 가짜 객체라는 것을 알 수 있다.


프록시 객체의 초기화

실제 클래스를 상속 받아서 만들어진다.
위에서 잠깐 나왔던 getClass()를 통해 알 수 있었다.

findMember.getClass() = class me.gicheol.Member$HibernateProxy$oU7VU5wF

그렇기 때문에 실제 클래스와 겉 모양이 동일하며,
내부적으로 실제 객체의 참조를 target이란 객체를 통해 가지고 있다.
target을 통해 실제 객체의 메서드를 호출하는 것이다.

error

그런데 처음 getReference()를 통해 프록시 객체를 만들었다면,
아직까지 DB를 통해 조회한 엔티티가 없기 때문에 target은 null일 것이다.
이를 위해 프록시 객체의 초기화를 해야한다.
JPA는 아래와 같은 방식으로 프록시 객체를 초기화 한다.

error

이러한 로직을 가지기 때문에 프록시 객체를 조회할때는 SELECT문이 발생하지 않고,
getName()과 같은 메서드를 호출 할 시 SELECT문을 사용하는 것이다.


프록시의 특징
  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  • 프록시 객체를 초기화 했을 때 실제 엔티티로 바뀌는 것이 아니라, 프록시 객체를 통해 실제 엔티티에 접근 가능한 것이다.
  • 그렇기 때문에 타입체크를 할 때 == 비교를 하면 안된다. (instanceof를 사용해야 함)
Member m1 = em.getReference(Member.class, member1.getId());
Member m2 = em.find(Member.class, member2.getId());

System.out.println("(m1.getClass() == m2.getClass()) = " + (m1.getClass() == m2.getClass()));
System.out.println("m1 instanceof Member = " + (m1 instanceof Member));
System.out.println("m2 instanceof Member = " + (m2 instanceof Member));

/*
   false
   true
   true
 */
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있다면, em.getReference()를 통해 호출을 하더라도 실제 엔티티를 반환한다.
    • JPA는 같은 트랜잭션 안의 같은 객체는 == 비교를 했을 때 항상 true가 나와야
      하기 때문이다.
      사실 위의 상황 때문에 굉장히 헷갈리는 부분이다.
      다른 점이라면 위는 다른 객체를 찾는 것이고 지금은 같은 객체를 찾는 것이라는 점이다.
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, m1.getId());

System.out.println("m1 == m2 = " + (m1 == m2));

// true
  • 이러한 특징을 살리기 위해 반대 경우에도 성립해야 하기 때문에
    먼저 getReference()를 통해 조회하고, find()를 조회하면
    find()를 통해 조회한 객체도 프록시가 된다.
Member m1 = em.getReference(Member.class, member1.getId());
Member m2 = em.find(Member.class, m1.getId());

System.out.println("m1.getClass() = " + m1.getClass());
System.out.println("m2.getClass() = " + m2.getClass());

// m1.getClass() = class me.gicheol.Member$HibernateProxy$A6oKMxmx 
// m2.getClass() = class me.gicheol.Member$HibernateProxy$A6oKMxmx

프록시 확인
  • 프록시 인스턴스의 초기화 여부 확인
Member m1 = em.getReference(Member.class, member1.getId());
System.out.println("m1.getClass() = " + m1.getClass());
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(m1));

// false

emf는 EntityManagerFactory이다.
PersistenceUnitUtil의 isLoaded()를 사용하면 인스턴스의 초기화 상태를 알 수 있다.

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

// true
  • 프록시 강제 초기화
    위 처럼 강제로 초기화 할때 m1.getName()을 사용할 수 있지만,
    하이버네이트가 지원하는 강제 초기화 메서드가 있다.
Member m1 = em.getReference(Member.class, member1.getId());
System.out.println("m1.getClass() = " + m1.getClass());
Hibernate.initialize(m1);
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(m1));

// true

참고로 Hibernate.initialize()는 하이버네이트가 지원하는 것으로,
JPA는 m1.getName()과 같은 방식으로 강제 초기화 해줘야 한다.


Tags:

Categories:

Updated:

Leave a comment