JPA & Hibernate Proxy

JPA Proxy

RDB 와 다르게 java에서는 참조를 통해 객체의 주소값을 가지고 일대다의 경우 List를 가질 수 있다.
Entity조회 할 때 모두 다 가져와야 한다면 성능적인 이슈가 나올 수밖에 없다.
따라서 JPA에서는 연관된 객체들을 모두 처음부터 데이터베이스에서 조회하는 것이 아니라, 실제 사용하는 시점에 데이터베이스에서 조회할 수 있다.
이와 관련 된 기술이 프록시 인데, 사용시점에 데이터베이스를 조회하는걸 지연로딩 (LAZY)이고 처음 조회할 때 연관된 객체까지 함께 조회하는것을 즉시로딩 (EAGER)라고 한다.

Member와 Order로 확인해보자.

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Orders> orders = new HashSet<>();

    // 연관관계 편의 메소드
    public void addOrder(Orders orderB) {
        this.orders.add(orderB);
        orderB.setMember(this);
    }
    ••• //getter setter
}
@Entity
public class Orders {
    @Id @GeneratedValue
    private Long id;

    private LocalDateTime orderDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
    
    ••• //getter setter
}

ManyToOne, OneToOne은 기본 Fetch 전략이 EAGER이어서 항상 Join을 하지만 성능상의 이슈로 LAZY하기를 권장한다.
LAZY는 지연로딩으로 해당 객체의 실제 사용 시 데이터베이스에서 객체를 불러온다는 뜻이다.
이 지연로딩을 JPA에서는 이것을 어떻게 구현할까? 답은 Proxy이다.
proxy
해당 그림처럼 JPA 에서는 Order를 호출할 때 안에 Member 대신값에 Proxy를 넣어 놓는다.
order.getMember() 까지는 실제 Member 객체 대신 Proxy가 들어있다가 Member 객체 사용시점에 영속성 컨텍스트에서 조회한다.
ex)

Orders order1 = new Orders(LocalDateTime.now(), Status.PREPARE);
em.persist(order1);

Member member = new Member();
member.addOrder(order1);
em.persist(member);

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

Orders orders = em.find(Orders.class, order1.getId());

System.out.println("orders = " + orders.getClass().getName());
System.out.println("orders.getMember() = " + orders.getMember().getClass().getName());

em.find로 Order객체를 불렀을 때 select 쿼리가 나갔을 것이다. Fetch 전략이 LAZY이기 때문에 이 때 Member와 join은 하지 않는다.
실제로 Class를 확인해보면 이렇게 나온다.

💻console

orders = com.longlong.jpastudy.vo.Orders   
orders.getMember() = com.longlong.jpastudy.vo.Member$HibernateProxy$54qYULum

Order에서 Member를 join하지 않았으니 Proxy로 둘러싼 객체를 호출하는 것이다.
orders.getMember().getName() 처럼 member가 실제로 사용할 때 영속성 컨텍스트가 member를 찾아온다.
(Hibernate에서는 Hibernate.initialize(entity) 를 통해 초기화를 지원한다.)

Jpa에서는 컬렉션을 사용하는 것처럼 == (주소값) 비교도 허용하므로 처음에 프록시 호출한 튜플과 같은 튜플을 불러오면 같은 instance로 불러와진다.
ex)

Orders orders = em.find(Orders.class, order1.getId());
final Member proxyMember = orders.getMember(); // 프록시
final Member findMember = em.find(Member.class, member.getId()); // 이미 영속성 컨텍스트에 있기 때문에 프록시 호출

System.out.println("proxyMember = " + proxyMember.getClass().getName());
System.out.println("findMember = " + findMember.getClass().getName());
System.out.println("proxyMember == findMember : " + (findMember == proxyMember));

💻console

proxyMember = com.longlong.jpastudy.vo.Member$HibernateProxy$C2SQ7AEK
findMember = com.longlong.jpastudy.vo.Member$HibernateProxy$C2SQ7AEK
proxyMember == findMember : true

여기서 findMember와 orders의 순서만 바꾸면 둘 다 Member객체 호출이 된다.

•••
final Member findMember = em.find(Member.class, member.getId());
Orders orders = em.find(Orders.class, order1.getId());
final Member proxyMember = orders.getMember(); // 프록시 일까??
•••

💻console

proxyMember = com.longlong.jpastudy.vo.Member
findMember = com.longlong.jpastudy.vo.Member
proxyMember == findMember : true

이렇게 JPA에서는 == 비교를 지원한다.

LazyInitializationException

JPA를 잘 알지 못하고 사용하면 정말 많이 만나는 LazyInitializationException이다.
영속성 컨텍스트에 없는 프록시 객체를 Initialize 할 때 에러가 발생한다.
ex)

Orders orders = em.find(Orders.class, order1.getId());
final Member proxyMember = orders.getMember(); // 프록시

System.out.println("proxyMember = " + proxyMember.getClass().getName());
em.detach(proxyMember);
//LazyInitializationException 에러 발생 System.out.println("proxyMember address = " + proxyMember.getAddress());

👍결론 :

  1. 같은 session과 thread
  2. 엔티티가 영속상태
  3. 영속성 컨텍스트안에 프록시 객체가 있어야 정상 작동한다.

Hibernate Proxy

공식문서 : https://docs.jboss.org/hibernate/orm/5.2/javadocs/org/hibernate/proxy/package-summary.html
Hibernate는 Proxy를 어떻게 구현하는지 자세히 알아보겠다.
Hibernate Proxy는 Entity를 감싸고 적절한 시점에 Initialize 하여 Entity 정보를 설정하고 getImplementation로 Entity에 접근 할 수 있다.
Hibernate의 추상클래스인 AbstractLazyInitializer를 보자.

public abstract class AbstractLazyInitializer implements LazyInitializer {
    •••
    private String entityName;
    private Serializable id;
    private Object target;
    private boolean initialized;
    private boolean readOnly;
    private boolean unwrap;
    private transient SharedSessionContractImplementor session;
    private Boolean readOnlyBeforeAttachedToSession;
    private String sessionFactoryUuid;
    private boolean allowLoadOutsideTransaction;
    •••

entityName과 id(key) 를 가지고 있으며 초기 target은 null이다. target정보를 요청하는 시점에 session.immediateLoad를 하여 target에 entity를 채워 넣는다.
그밖에 readOnly, session, 속성 등은 초기 entityManager에서 설정해주며 원하는 대로 변경 가능하지만 readOnly=false인 Immutable Object 같은 논리적인 헛점이 생기면 Exception이 되므로 조심히 사용 해야한다.

Hibernate에서 객체를 가져올 때는 이전에 프록시로 가져왔으면 프록시로 아니면 Entity로 리턴해준다. ( == 비교를 위해)

//HibernateProxyHelper.class
public static Class getClassWithoutInitializingProxy(Object object) {
    if (object instanceof HibernateProxy) {
        HibernateProxy proxy = (HibernateProxy)object;
        LazyInitializer li = proxy.getHibernateLazyInitializer();
        return li.getPersistentClass();
    } else {
        // non proxy!!!
        return object.getClass();
    }
}

👍Tip: JPA 표준 스펙은 아니지만 hibernate에는 Hibernate.initialize(entity)로 초기화하고 Hibernate.unproxy(entity)라는 프록시 해제 method가 있다.

Hibernate.initialize

// 바이트 단계에서 initialize도 지원하지만 여기서는 넘어가겠다.
//Hibernate.class
(Hibernate) proxy.getHibernateLazyInitializer().initialize()
//AbstractLazyInitializer.class
this.target = session.immediateLoad(this.entityName, this.id);
this.initialized = true;
this.checkTargetState(session); // target이 없을 때 EntityNotFoundException 처리 위해

Hibernate.unproxy

Orders orders = em.find(Orders.class, order1.getId());
final Member findMember = orders.getMember();
final Member unproxyMember = Hibernate.unproxy(findMember, Member.class);

System.out.println("orders = " + orders.getClass().getName());
System.out.println("findMember = " + findMember.getClass().getName());
System.out.println("unproxyMember = " + unproxyMember.getClass().getName());
System.out.println("findMember == unproxyMember : " + (findMember == unproxyMember));

💻console

orders = com.longlong.jpastudy.vo.Orders
findMember = com.longlong.jpastudy.vo.Member$HibernateProxy$B6iuL2t5
unproxyMember = com.longlong.jpastudy.vo.Member
findMember == unproxyMember : false

unproxy의 동작과정은 LazyInitializer에서 초기화 후 target을 불러온다.

//Hibernate.class
public static Object unproxy(Object proxy) {
    if (proxy instanceof HibernateProxy) {
        HibernateProxy hibernateProxy = (HibernateProxy)proxy;
        LazyInitializer initializer = hibernateProxy.getHibernateLazyInitializer();
        return initializer.getImplementation();
    } else {
        return proxy;
    }
}
//AbstractLazyInitializer.class
public final Object getImplementation() {
    this.initialize();
    return this.target;
}

댓글남기기