1. 개요

이 튜토리얼에서 우리는 Hibernate 프록시 를 실제 엔티티 객체 로 변환하는 방법을 배울 것 입니다. 그 전에 Hibernate가 언제 프록시 객체를 생성하는지 이해할 것입니다. 그런 다음 Hibernate 프록시가 유용한 이유에 대해 이야기 할 것입니다. 마지막으로 객체 프록시를 해제해야하는 시나리오를 시뮬레이션합니다.

2. Hibernate는 언제 Proxy 객체를 생성합니까?

Hibernate는 프록시 객체를 사용하여 지연로드 를 허용 합니다. 시나리오를 더 잘 시각화하기 위해 PaymentReceiptPayment 엔터티를 살펴 보겠습니다 .

@Entity
public class PaymentReceipt {
    ...
    @OneToOne(fetch = FetchType.LAZY)
    private Payment payment;
    ...
}
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Payment {
    ...
    @ManyToOne(fetch = FetchType.LAZY)
    protected WebUser webUser;
    ...
}

예를 들어, 이러한 엔티티 중 하나를로드하면  Hibernate가 FetchType.LAZY 와 관련된 필드에 대한 프록시 객체를 생성합니다 .

시연하기 위해 통합 테스트를 만들고 실행 해 보겠습니다.

@Test
public void givenPaymentReceipt_whenAccessingPayment_thenVerifyType() {
    PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
    Assert.assertTrue(paymentReceipt.getPayment() instanceof HibernateProxy);
}

테스트에서, 우리는로드 한 PaymentReceipt을 하고 있다는 것을 확인 지불 객체의 인스턴스가 아닌 가 CreditCardPayment - 그것은이다 HibernateProxy의 객체 .

반대로 지연로드가 없으면 반환 된 결제 개체가 CreditCardPayment 의 인스턴스이므로 이전 테스트는 실패합니다 .

또한 Hibernate가 프록시 객체를 생성하기 위해 바이트 코드 계측 을 사용하고 있다는 점을 언급 할 가치가 있습니다.

이를 확인하기 위해 통합 테스트의 단언 문 줄에 중단 점을 추가하고 디버그 모드에서 실행할 수 있습니다. 이제 디버거가 보여주는 내용을 살펴 보겠습니다.

paymentReceipt = {PaymentReceipt@5042} 
 payment = {Payment$HibernateProxy$CZIczfae@5047} "com.baeldung.jpa.hibernateunproxy.CreditCardPayment@2"
  $$_hibernate_interceptor = {ByteBuddyInterceptor@5053} 

디버거에서 Hibernate가 런타임에 동적으로 Java 클래스를 생성하는 라이브러리 인 Byte Buddy를 사용하고 있음을 알 수 있습니다 .

3. Hibernate Proxy가 유용한 이유는 무엇입니까?

3.1. 지연 로딩을위한 Hibernate 프록시

앞서 이것에 대해 조금 배웠습니다. 더 많은 의미를 부여하기 위해 PaymentReceiptPayment 엔티티 모두에서 지연로드 메커니즘을 제거해 보겠습니다  .

public class PaymentReceipt {
    ...
    @OneToOne
    private Payment payment;
    ...
}
public abstract class Payment {
    ...
    @ManyToOne
    protected WebUser webUser;
    ...
}

이제 PaymentReceipt를 빠르게 검색 하고 로그에서 생성 된 SQL을 확인 하겠습니다 .

select
    paymentrec0_.id as id1_2_0_,
    paymentrec0_.payment_id as payment_3_2_0_,
    paymentrec0_.transactionNumber as transact2_2_0_,
    payment1_.id as id1_1_1_,
    payment1_.amount as amount2_1_1_,
    payment1_.webUser_id as webuser_3_1_1_,
    payment1_.cardNumber as cardnumb1_0_1_,
    payment1_.clazz_ as clazz_1_,
    webuser2_.id as id1_3_2_,
    webuser2_.name as name2_3_2_ 
from
    PaymentReceipt paymentrec0_ 
left outer join
    (
        select
            id,
            amount,
            webUser_id,
            cardNumber,
            1 as clazz_ 
        from
            CreditCardPayment 
    ) payment1_ 
        on paymentrec0_.payment_id=payment1_.id 
left outer join
    WebUser webuser2_ 
        on payment1_.webUser_id=webuser2_.id 
where
    paymentrec0_.id=?

로그에서 볼 수 있듯이 PaymentReceipt에  대한 쿼리   에는 여러 조인 문이 포함되어 있습니다. 

이제 lazy loading을 사용하여 실행 해 보겠습니다.

select
    paymentrec0_.id as id1_2_0_,
    paymentrec0_.payment_id as payment_3_2_0_,
    paymentrec0_.transactionNumber as transact2_2_0_ 
from
    PaymentReceipt paymentrec0_ 
where
    paymentrec0_.id=?

분명히 생성 된 SQL은 불필요한 조인 문을 모두 생략하여 단순화됩니다.

3.2. 데이터 쓰기를위한 Hibernate Proxy

설명을 위해 결제 를 생성 하고 WebUser할당하는 데 사용하겠습니다 . 프록시를 사용하지 않고,이 두 개의 SQL 문 초래하십시오 SELECT  문은 검색  WEBUSER  과 INSERT에 대한 문  지불  생성.

프록시를 사용하여 테스트를 만들어 보겠습니다.

@Test
public void givenWebUserProxy_whenCreatingPayment_thenExecuteSingleStatement() {
    entityManager.getTransaction().begin();

    WebUser webUser = entityManager.getReference(WebUser.class, 1L);
    Payment payment = new CreditCardPayment(new BigDecimal(100), webUser, "CN-1234");
    entityManager.persist(payment);

    entityManager.getTransaction().commit();
    Assert.assertTrue(webUser instanceof HibernateProxy);
}

 프록시 객체를 얻기 위해 entityManager.getReference (…)사용하고 있다는 점을 강조 할 가치가 있습니다.

다음으로 테스트를 실행하고 로그를 확인하겠습니다.

insert 
into
    CreditCardPayment
    (amount, webUser_id, cardNumber, id) 
values
    (?, ?, ?, ?)

여기서 우리는 프록시를 사용할 때 Hibernate 지불 생성을 위한 INSERT  문  하나만 실행했음을 알 수 있습니다 . 

4. 시나리오 : 프록시 해제의 필요성

도메인 모델이 주어지면 PaymentReceipt를 검색한다고 가정 해 보겠습니다. 이미 알고 있듯이,  상속 전략이 Class-per-Class  이고 lazy fetch 유형 인 Payment 엔티티 와 연결되어 있습니다. 

우리의 경우 채워진 데이터를 기반으로 PaymentReceipt 의 관련  지불  은  CreditCardPayment  유형  입니다. 그러나 지연 로딩을 사용하고 있으므로 프록시 객체가됩니다.

이제 CreditCardPayment  엔티티를 살펴 보겠습니다  .

@Entity
public class CreditCardPayment extends Payment {
    
    private String cardNumber;
    ...
}

사실, 검색 할 수 없을 것 C의 ardNumber의  로부터 필드  가 CreditCardPayment의  unproxying없이 클래스를  지불  개체를. 그럼에도 불구하고 결제  개체를 CreditCardPayment 로  캐스팅하고 어떤 일이 발생하는지 살펴 보겠습니다  .

@Test
public void givenPaymentReceipt_whenCastingPaymentToConcreteClass_thenThrowClassCastException() {
    PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
    assertThrows(ClassCastException.class, () -> {
        CreditCardPayment creditCardPayment = (CreditCardPayment) paymentReceipt.getPayment();
    });
}

테스트를 통해 결제  개체를 CreditCardPayment 로  캐스팅해야한다는 것을 알았습니다  . 그러나 때문에 지불  객체가 여전히 최대 절전 모드 프록시 객체, 우리는 만난  ClassCastException이 .

5. 엔티티 객체에 대한 하이버 네이트 프록시

Hibernate 5.2.10부터 우리는 Hibernate 엔티티를 프록시 해제하기 위해 내장 된 정적 메소드를 사용할 수 있습니다 :

Hibernate.unproxy(paymentReceipt.getPayment());

이 접근 방식을 사용하여 최종 통합 테스트를 만들어 보겠습니다.

@Test
public void givenPaymentReceipt_whenPaymentIsUnproxied_thenReturnRealEntityObject() {
    PaymentReceipt paymentReceipt = entityManager.find(PaymentReceipt.class, 3L);
    Assert.assertTrue(Hibernate.unproxy(paymentReceipt.getPayment()) instanceof CreditCardPayment);
}

테스트에서 우리는 Hibernate 프록시를 실제 엔티티 객체로 성공적으로 변환했음을 알 수 있습니다.

반면에 Hibernate 5.2.10 이전의 솔루션은 다음과 같습니다.

HibernateProxy hibernateProxy = (HibernateProxy) paymentReceipt.getPayment();
LazyInitializer initializer = hibernateProxy.getHibernateLazyInitializer();
CreditCardPayment unproxiedEntity = (CreditCardPayment) initializer.getImplementation();

6. 결론

이 튜토리얼에서 우리는 Hibernate 프록시를 실제 엔티티 객체로 변환하는 방법을 배웠습니다. 그 외에도 Hibernate 프록시가 작동하는 방식과 이것이 유용한 이유에 대해 논의했습니다. 그런 다음 객체의 프록시를 해제해야하는 상황을 시뮬레이션했습니다.

마지막으로 몇 가지 통합 테스트를 실행하여 예제를 시연하고 솔루션을 확인했습니다.

항상 그렇듯이 기사의 전체 소스 코드는 GitHub에서 사용할 수 있습니다 .