Post

[Spring Boot] JPA와 프록시 객체

[Spring Boot] JPA와 프록시 객체

📌 프록시 객체란?

JPA 공부를 하다 보면 프록시 객체라는 개념이 등장한다. 지연 로딩에서 프록시 객체를 로드하는데, 실제로 데이터가 필요할 때까지 실제 객체를 로드하지 않는다. 조금 더 자세히 알아보자.

특징

프록시 객체는 실제 엔티티 객체를 감싸는 가상의 객체로, 실제 데이터가 필요할 때까지 DB 조회를 지연시키는 역할을 한다. 쉽게 표현하면 프록시 객체란 실제 객체의 겉모습을 흉내내는 가짜 객체이다.

프록시 객체는 실제 엔티티 클래스를 상속받아 만들어진다. 같은 인터페이스를 가지기 때문에 사용자 입장에서 실제 객체와 프록시 객체를 구분하지 않고 사용할 수 있다. 또한 프록시 객체는 실제 객체에 대한 참조를 내부적으로 보관하며, 프록시 객체의 메서드를 호출하면 보관하고 있는 참조를 사용하여 실제 객체의 메서드를 호출한다.

만약 객체 접근 시 영속성 컨텍스트에 이미 실제 객체가 있다면 프록시 객체가 아닌 실제 객체를 리턴한다.

📌 find()와 getReference()

em.find() 는 DB를 즉시 조회하여 실제 객체를 리턴하는 반면, em.getReference()프록시 객체를 리턴한다. em.find()는 조회 시 항상 SELECT 쿼리가 실행되지만 em.getReference()는 ID 이외 속성에 접근하는 경우에만 SELECT 쿼리가 실행된다.

1
2
Member findMember = em.find(Member.class, id); // 실제 객체
Member refMember = em.getReference(Member.class, id); // 실제 객체

영속성 컨텍스트에 이미 실제 객체가 있다면 getReference()를 호출해도 실제 객체가 리턴된다.

1
2
Member refMember = em.getReference(Member.class, id); // 프록시 객체
Member findMember = em.find(Member.class, id); // 프록시 객체

프록시 객체가 먼저 생성되었다면 find()를 호출해도 프록시 객체가 리턴된다.

단, getReference()로 생성된 프록시 객체를 다른 트랜잭션에 전달할 경우 Unknown Entity 예외가 발생할 수 있다. 또한 동일한 트랜잭션 내에서는 == 를 통한 실제 객체와 프록시 객체 간 비교 연산이 가능하나 다른 트랜잭션에서는 instanceof 를 사용하는 것이 안전하다.

📌 프록시 객체 초기화

프록시 객체가 실제 데이터를 로드하는 과정을 살펴보자.

  1. 프록시 객체가 객체의 메서드를 호출한다.
  2. 프록시 객체가 영속성 컨텍스트에게 실제 객체 생성을 요청한다.
  3. 영속성 컨텍스트는 DB를 조회하여 실제 객체를 생성한다.
  4. 프록시 객체는 실제 객체의 참조는 내부에 보관한다.
  5. 프록시 객체가 실제 객체의 메서드를 호출하여 결과를 리턴한다.

주의해야 할 점은 프록시 객체를 초기화할 때 프록시 객체가 실제 객체로 바뀌는 것이 아니라는 것이다. 실제 객체의 참조를 보관하는 것이다.

📌 언프록시

프록시 객체의 unproxy 는 프록시 객체에서 실제 객체를 추출하는 과정이다.

Hibernate 5.2.10 버전부터 Hibernate.unproxy() 를 사용하여 실제 객체를 추출할 수 있다. 주어진 객체가 프록시인 경우 해당 프록시를 초기화하고 프록시가 가리키는 실제 객체에 대한 참조를 리턴한다. 주어진 객체가 프록시 객체가 아닌 경우 객체 자체를 그대로 리턴한다.

1
2
3
4
5
// 프록시 객체 조회
Member memberRef = em.getReference(Member.class, 1L);

// unproxy
Member realMember = (Member) Hibernate.unproxy(memberRef);

Hibernate 5.2.10 이전 버전에는 아래와 같은 방법으로 실제 객체를 추출했다.

1
2
3
4
5
6
Object unproxiedEntity = null;
if (proxy instanceof HibernateProxy) {
    HibernateProxy hibernateProxy = (HibernateProxy) proxy;
    LazyInitializer initializer = hibernateProxy.getHibernateLazyInitializer();
    unproxiedEntity = initializer.getImplementation();
}

📌 프록시 객체 확인 방법

객체가 프록시 객체인지 확인하는 방법은 다음과 같다.

  1. 클래스 이름을 확인하기

.getClass().getName() 으로 클래스 이름을 출력해본다. 프록시 객체는 클래스명 뒤에 $HibernateProxy$ 라는 문자열이 있다.

1
2
3
Member member = em.getReference(Member.class, 1L);
System.out.println("memberProxy = " + member.getClass().getName());
// memberProxy = package명.Member$HibernateProxy$misHOeS1
  1. instanceof 사용하기

HibernateProxy 인터페이스의 인스턴스인지 확인하여 프록시 객체 여부를 확인할 수 있다.

1
2
3
if (entity instanceof HibernateProxy) {
    System.out.println("이 객체는 프록시입니다.");
}

프록시 객체가 초기화되었는지 확인하는 방법은 다음과 같다.

  1. PersistenceUnitUtil 사용하기

.getPersistenceUnitUtil().isLoaded() 를 통해 프록시 객체의 초기화 여부를 확인할 수 있다.

1
2
3
4
5
6
7
boolean isLoaded = em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(entity);
// or
boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(entity);

System.out.println("초기화 여부: " + isLoaded);
// true: 이미 초기화됨 or 프록시가 아닌 인스턴스
// false: 초기화되지 않은 프록시
  1. Hibernate 유틸리티 사용하기

Hibernate.isInitialized() 를 통해 프록시 객체의 초기화 여부를 확인할 수 있다.

1
2
3
4
Member refMember = em.getReference(Member.class, id);

System.out.println("초기화 여부: " + Hibernate.isInitialized(refMember));

📌 참고

https://whitepro.tistory.com/414

https://pixx.tistory.com/397

https://velog.io/@whereami2048/JPA-프록시

This post is licensed under CC BY 4.0 by the author.