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이다.
해당 그림처럼 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());
👍결론 :
- 같은 session과 thread
- 엔티티가 영속상태
- 영속성 컨텍스트안에 프록시 객체가 있어야 정상 작동한다.
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;
}
댓글남기기