✏️ JPA 프록시(Proxy)

박상민·2023년 10월 7일
0

JPA

목록 보기
12/24
post-thumbnail

⭐️ 프록시

먼저 생각을 해보자.

  • Member 엔티티를 조회할 때 Team도 함께 조회해야 할까?

실제로 필요한 비즈니스 로직에 따라 다르다.
비즈니스 로직에서 필요하지 않을 때가 있는데, 항상 Team을 함께 가져와서 사용할 필요는 없다. 낭비가 발생하게 된다.
JPA는 이 낭비를 하지 않기 위해, 지연로딩과 프록시라는 개념으로 해결한다.

📌 프록시 기초

지연 로딩을 이해하려면, 프록시의 개념에 대해서 명확하게 이해해야 한다.

  • em.find() vs em.getReference()

    • JPA에서 em.find() 말고, em.getReference()라는 메서드도 제공 된다. 이름 그대로 참조를 가져오는 것이다.
  • em.find(): DB를 통해서 실제 엔티티 객체 조회

  • em.getReference(): DB의 조회를 미루는 가짜(프록시) 엔티티 객체를 조회

가짜 엔티티 객체를 조회한다고?
이게 무슨 얘기냐면 DB의 쿼리는 안나가는데 객체가 조회가 되는 것이다.
아래의 코드로 이해하자.

getReference로 가져온 엔티티의 클래스 확인

Member member = new Member();
member.setUsername("hello");
em.persist(member);

em.flush(); //영속성 컨텍스트의 1차 캐시에 있는 정보를 db로 강제 전송
em.clear(); //영속성 컨텍스트 초기화

Member refMember = em.getReference(Member.class, member.getId());
System.out.println("refMember = " + refMember.getClass());

tx.commit()

member 객체를 하나 만들고 저장한후 플러쉬, 클리어로 영속성 컨텍스트를 깨끗하게 만들었다.
em.getReference(Member.class, member.getId());를 하면 쿼리가 어떻게 나갈까? 아래 사진으로 결과를 확인해보자.

쿼리를 보니까 인서트 쿼리만 나가고 셀렉트 쿼리는 안나갔다.

이때 refMember.getUsername()을 하면 셀렉트 쿼리가 나간다.

GetReference를 호출하는 시점에는 DB에 쿼리를 안보낸다. 그런데 이 값이 실제 사용되는 시점에 DB에 쿼리가 나간다.

클래스를 확인해보니 $HibernateProxy~~라고 나온다. 이게 뭐냐면 Hibernate가 강제로 만든 가짜 클래스라는 얘기다.


em.getReference(Member.class, member.getId());라고 하면 진짜 멤버 객체를 반환해주는 것이 아니다. Hibernate가 자기 내부에 라이브러리를 사용해서 가짜(프록시) 엔티티 객체를 만들어서 주는 것이다.
프록시 엔티티 객체를 위 사진처럼 생겼다. 껍데기는 똑같은데 안이 텅텅 빈 것이다.
그리고 내부에는 Target이라는 게 있는데 이게 진짜 Reference를 가르킨다.


프록시 특징

  • 실제 클래스를 상속 받아서 만들어짐
    • Hibernate가 내부적으로 라이브러리를 사용해서 알아서 작업 함.
  • 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)

  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
    • getName을 호출하면 실제 target에 이쓴 객체의 getName을 대신 호출해준다.

📌 프록시 객체의 초기화


1.em.getReference()로 프록시 객체를 가져온 다음에, getName() 메서드를 호출 하면
2. MemberProxy 객체에 처음에 target 값이 존재하지 않는다. JPA가 영속성 컨텍스트에 초기화 요청을 한다.
3. 영속성 컨텍스트가 DB에서 조회해서
4. 실제 Entity를 생성해준다.
5. 그리고 프록시 객체가 가지고 있는 target(실제 Member)의 getName()을 호출해서 결국 member.getName()을 호출한 결과를 받을 수 있다.
6. 프록시 객체에 target이 할당 되고 나면, 더이상 프록시 객체의 초기화 동작은 없어도 된다.

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화

    • 한 번 초기화 되면 계속 사용한다.
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님

    • 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
    • 정확히 말하면 target에 값이 채워지는 것이다.
    • em.getReference()로 조회한 클래스를 getClass()로 보면, HibernateProxy 클래스였던 것을 위에서 학습했다.
  • 프록시 객체는 원본 엔티티를 상속받는다. 프록시 객체와 원본 객체의 타입이 다르다.

    • == 비교는 실패한다. JPA에서 타입 비교는 가능하면 instanceOf를 사용해야 한다
    • JPA에서는 같은 인스턴스의 == 비교에 대해서 같은 영속성 컨텍스트 안에서 조회하면 항상 같다라고 나와야 한다.
Member find = em.find(Member.class, member1.getId());
Member reference = em.getReference(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1 == m2));
--------------
## 결과 ##
m1 == m2 : False
System.out.println("find : " + (find instanceof Member));
System.out.println("reference : " + (reference instanceof Member));
--------------
## 결과 ##
find : True
reference : True
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환, 반대도 마찬가지다.
    • 이미 영속성 컨텍스트에 올려둔 객체를 굳이 다시 프록시로 감싸서 반환하는게 의미도 없고 이점도 없다.
Member member = new Member();
member.setUsername("hello");
em.persist(member);

em.flush(); //영속성 컨텍스트의 1차 캐시에 있는 정보를 db로 강제 전송
em.clear(); //영속성 컨텍스트 초기화

Member m1 = em.find(Member.class, member.getId());
System.out.println("m1.getClass() = " + m1.getClass());

Member reference = em.getReference(Member.class, member.getId());
System.out.println("reference.getClass() = " + reference.getClass());

tx.commit(); // 트랜잭션을 커밋하는 시점에서 영속성 컨텍스트에 있는 DB의 쿼리가 날라감

참고

Member m1 = em.getReference(Member.class, member.getId());
System.out.println("m1.getClass() = " + m1.getClass());
Member reference = em.find(Member.class, member.getId());
System.out.println("reference.getClass() = " + reference.getClass());
-------------------------
## 결과 ##
m1.getClass() = class hellojpa.Member$HibernateProxy$Cl8uGtEy
reference.getClass() = class hellojpa.Member$HibernateProxy$Cl8uGtEy

이전에 말했듯이 JPA에서는 같은 인스턴스의 == 비교에 대해서 같은 영속성 컨텍스트 안에서 조회하면 항상 같다라고 나와야 한다. m1에서 getReference로 Hibernate Proxy로 조회가 됐다. 이때 == 비교에 대해서 같다고 보장해야하기 때문에 em.find한 결과도 실제 member 객체가 아닌 Hibernate Proxy가 반환된다.
결론적으로, 내부적으로 복잡한 처리 등은 JPA가 알아서 해주니 우리가 개발할 때 프록시인지 진짜 객체인지는 중요하지 않다.

  • 실무에서 많이 만나게 되는 문제: 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
    (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
Member member = new Member();
member.setUsername("hello");
em.persist(member);

em.flush(); //영속성 컨텍스트의 1차 캐시에 있는 정보를 db로 강제 전송
em.clear(); //영속성 컨텍스트 초기화
            
Member refMember = em.getReference(Member.class, member.getId());
System.out.println("ref.getClass() = " + refMember.getClass());

em.detach(refMember); //준영속 상태로 만듦

System.out.println("refMember = " + refMember.getUsername()); //준영속 상태이기 때문에 데이터를 못가져옴 (오류 발생)

tx.commit(); // 트랜잭션을 커밋하는 시점에서 영속성 컨텍스트에 있는 DB의 쿼리가 날라감

오류

getReference로 프록시 객체를 가져왔다. 프록시에 대한 초기화 요청은 영속성 컨텍스트를 통해서 일어나는데 만약 영속성 컨텍스트를 꺼버린다면 어떻게 될까?

em.detach(refmember)를 해서 프록시 객체를 준영속 상태로 만들었다. 준영속 상태로 만들고 refMember.getUsername()을 하니까 위 사진처럼 프록시를 초기화 할 수 없다는 오류가 발생한다.

이전에 설명한 것처럼 프록시 객체는 refMember.getUsername()을 하면 영속성 컨텍스트에 초기화 요청을 한다. 그런데 em.detach(refmember)로 준영속 상태로 만들었기에 영속성 컨텍스트에 초기화 요청을 하지 못하게 되었고 오류가 발생한다.

📌 프록시 유틸리티 메소드

프록시 확인을 도와주는 Util성 메소드들이 있다.

  • 프록시 인스턴스의 초기화 여부 확인
    • PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법
    • entity.getClass().getName() 출력(..javasist.. or HibernateProxy…)
  • 프록시 강제 초기화
    • org.hibernate.Hibernate.initialize(entity);
  • 참고: JPA 표준은 강제 초기화 없음, Hibernate가 지원한다.
    • 강제 호출: member.getName()

출처
자바 ORM 표준 JPA 프로그래밍 강의
게시글 속 자료는 모두 위 강의 속 자료를 사용했습니다.
https://ict-nroo.tistory.com/131

profile
스프링 백엔드를 공부중인 대학생입니다!

0개의 댓글