1. 개요
이 튜토리얼에서 우리는 Hibernate 프록시 를 실제 엔티티 객체 로 변환하는 방법을 배울 것 입니다. 그 전에 Hibernate가 언제 프록시 객체를 생성하는지 이해할 것입니다. 그런 다음 Hibernate 프록시가 유용한 이유에 대해 이야기 할 것입니다. 마지막으로 객체 프록시를 해제해야하는 시나리오를 시뮬레이션합니다.
2. Hibernate는 언제 Proxy 객체를 생성합니까?
Hibernate는 프록시 객체를 사용하여 지연로드 를 허용 합니다. 시나리오를 더 잘 시각화하기 위해 PaymentReceipt 및 Payment 엔터티를 살펴 보겠습니다 .
@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 프록시
앞서 이것에 대해 조금 배웠습니다. 더 많은 의미를 부여하기 위해 PaymentReceipt 및 Payment 엔티티 모두에서 지연로드 메커니즘을 제거해 보겠습니다 .
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 프록시가 작동하는 방식과 이것이 유용한 이유에 대해 논의했습니다. 그런 다음 객체의 프록시를 해제해야하는 상황을 시뮬레이션했습니다.
마지막으로 몇 가지 통합 테스트를 실행하여 예제를 시연하고 솔루션을 확인했습니다.