JPA) 연관관계 관리 - 프록시

Date:    Updated:

카테고리:

프록시를 사용해야하는 이유


Member와 Team이라는 테이블이 있다고 가정하자. (Member는 자신이 소속되어 있는 Team이 있다. = N:1의 관계)

이때 Member와 Team 객체를 출력하는 비즈니스 로직이 있다고 할 때 다음과 같이 작성할 수 있다.

private static void printMemberAndTeam(Member member){
    System.out.println("username = "+ member.getUsername());
    System.out.println("username = "+ member.getTeam().getName());
}

...

Member member = entityManager.find(Member.class, 1L); // member와 team 조회 join 쿼리 수행
printMemberAndTeam(member);

JPA 에서는 member의 이름과 team 이름을 구하기 위해 join 쿼리를 날리게 된다.

printMemberAndTeam 메서드를 수행하기엔 적합한 쿼리였지만 Member의 정보만 필요한 경우에는 join 쿼리는 오히려 낭비가 될 것이다.

어떻게하면 때에 따라 낭비 없이 최적화를 할 수 있을까?

JPA에서는 프록시를 통한 지연로딩이라는 개념으로 해결할 수 있다.

프록시

프록시는 실제 객체의 대리인으로써 사용자의 요청을 대신 받아서 처리해주는 역할을 수행한다.

JPA는 리소스 낭비 최소화를 위해 프록시의 특징을 활용하여 실제 객체가 필요한 시점에만 쿼리를 수행할 수 있게끔 하는 일명 지연로딩 기능을 제공한다.

프록시의 특징은 다음과 같다.

  • Hibernate에 의해 프록시 객체가 실제 클래스를 상속 받아서 만들어진다.
    • 그렇기 때문에 프록시 객체는 실제 클래스와 메서드 등 겉 모양이 똑같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분할 수 없기 때문에 실제 객체를 사용하듯 사용하면 된다.
  • 프록시 객체는 실제 객체의 참조(target)를 보관한다.
    • 참조 객체는 실제 엔티티를 가리키고 있으며 초기에는 null 이다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출하게 된다.
    • 예를 들면 프록시 객체를 통해 getName()을 호출 하면 target의 getName()을 대신 호출해주게 된다.


EntityManager는 이러한 프록시를 활용한 지연로딩 조회를 위해 getReference() 기능을 제공한다.

EntityManager의 find()와 getReference()

  • entityManager.find() - 즉시로딩
    • 데이터베이스를 통해서 실제 엔티티 객체를 조회한다.
  • entityManager.getReference() - 지연로딩
    • 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.
      • 실제 엔티티 객체를 불러오는게 아닌 Proxy라는 클래스로 감싸진 가짜 객체를 반환한다.
// 이 시점에는 쿼리를 날리지 않고 가짜(프록시) member 객체를 가져온다
Member findMember = entityManager.getReference(Member.class, 1L);

System.out.println("findMember.id = "+ findMember.getId());         // getId() 값은 이미 알고 있으므로 쿼리가 수행되지 않는다.
System.out.println("findMember.name = "+ findMember.getUsername()); // member의 username은 데이터베이스에서 가져와야 하므로 해당 시점에 쿼리를 수행하여 값을 가져온다.

프록시 객체의 초기화 과정

  1. Member의 프록시 객체를 가져와서 target을 통해 getName()을 호출한다.
  2. target의 값이 초기에는 null이므로 Member 객체 초기화를 위해 영속성 컨텍스트에 요청한다.
  3. 영속성 컨텍스트는 Member 객체를 초기화하기 위해 데이터베이스를 조회한다.
    • 이 시점에 쿼리가 수행된다.
  4. 영속성 컨텍스트를 통해 Member 객체가 초기화된다.
  5. 이제 Member의 프록시 객체의 target이 실제 Member 객체를 바라보게 되고 target을 통해 실제 getName()을 호출하게 된다.
    • 객체 초기화 이후 target은 실제 Member를 바라보게 되므로 실제 엔티티를 보는것과 같다.

정리

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

  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
    • 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근을 하는거지 바뀌는게 아님.
  • 프록시 객체는 원본 엔티티를 상속 받는다. 따라서 타입 체크시 주의해야함.
    • 원본 엔티티를 상속을 받아서 사용하기 때문에 == 로 비교하면 항상 같다고 나옴.
    • 프록시와 엔티티 타입 체크를 하려면 instance of 로 비교해야함.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환하게 된다.
    • 엔티티가 이미 초기화가 되어있으므로 지연로딩이 의미가 없다.
      • 그렇기 때문에 프록시로 가져와봤자 아무 이점이 없기 때문이다.
    • 그리고 JPA는 트랜잭션 안에서 초기화 한 엔티티에 대해서 동일성을 보장해야 하기 때문에 프록시에서 가져오더라도 실제 엔티티를 반환 한다.
    • 반대의 경우도 마찬가지다.
      • 예를들어, 처음에 em.getReference()로 호출을 하고 바로 아래에서 em.find()로 다시 호출하여 영속성 컨텍스트를 통해 초기화를 했다고 가정하자.
      • 이때 두개의 객체를 비교 하게되면 JPA는 엔티티 동일성을 보장해야하기 때문에 첫번째로 호출한 em.getReference()에 맞춰 em.find()로 호출한 경우에도 프록시 객체로 나오게 된다.

        Member refMember = em.getReference(Member.class, 1L); // proxy
        Member findMember = em.find(Member.class, 1L);        // Member
        System.out.println("refMember == findMember : " + (refMember == findMember)) // true
        
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화 하면 문제 발생
    • 아래와 같이 프록시 객체가 영속성 컨텍스트에 의해 초기화 되기전에 영속성 컨텍스트를 닫는 경우 영속상태가 될 수 없다.

      Member refMember = em.getReference(Member.class, 1L); // proxy
      
      em.close(); // or em.detach(refMember) or em.clear()
      
      System.out.println("refMember = " + refMember.getUsername()); // org.hibernate.LazyInitializationException caused!
      
    • 이럴경우 Hibernate는 org.hibernate.LazyInitializationException 예외를 반환하게 된다.

프록시 객체를 확인하기 위한 방법들

  • 프록시 인스턴스의 초기화 여부 확인하기
    • PersistenceUnitUtil.isLoaded(Object entity)

      Member refMember = em.getReference(Member.class, 1L); // proxy
      PersistenceUnitUtil puu = emf.getPersistenceUnitUtil();
      System.out.println("isLoaded = " + puu.isLoaded(refMember)); // false
      System.out.println("refMember = " + refMember.getUsername()); // 프록시 초기화
      System.out.println("isLoaded = " + puu.isLoaded(refMember)); // true
      
  • 프록시 클래스 확인 방법
    • entity.getClass() 출력

      Member refMember = em.getReference(Member.class, 1L); // proxy
      System.out.println("refMember = " + refMember.getClass()); // class jpa.Member$HibernateProxy$...
      
  • 프록시 강제 초기화
    • org.hibernate.Hibernate.initialize(entity)

      Member refMember = em.getReference(Member.class, 1L); // proxy
      System.out.println("refMember = " + refMember.getClass()); // class jpa.Member$HibernateProxy$...
      Hibernate.initialize(refMember) // 프록시 강제 초기화
      
  • JPA 표준은 강제 초기화가 없음.
    • member.getName() 등을 통해 직접 메서드를 실행하여 강제 호출로 초기화 하기

📣 Reference

본 포스팅은 김영한님의 강의를 듣고 스스로 정리 및 추가한 내용입니다.

자바 ORM 표준 JPA 프로그래밍 - 기본편

댓글남기기