프록시와 연관관계 관리

현시기얌·2021년 12월 5일
0

JPA

목록 보기
12/14

프록시

Question) Member를 조회할 때 Team도 함께 조회해야 할까?

회원과 팀 함께 출력

Member member = em.find(Member.class, memberId);
Team team = member.getTeam();

System.out.println("회원 이름 : " + member.getUsername());
System.out.println("소속팀 : " + team.getName());

회원만 출력

Member member = em.find(Member.class, memberId);
Team team = member.getTeam();

System.out.println("회원 이름 : " + member.getUsername());

회원만 출력하면 되는데 회원과 팀이 연관관계가 맺어져 있다고 팀 정보까지 DB에서 가져오면 불필요한 정보까지 가져오게 되는 거라 성능에 최적화에 문제가 생긴다.

JPA는 지연로딩이라는 것을 이용해 이러한 문제를 방지한다.

프록시 기초

  • em.find() vs em.getReference()
  • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 객체 조회

em.find()로 회원 조회하기

            final Member member = new Member();
            member.setName("hello");

            em.persist(member);

            em.flush();
            em.clear();


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

실행 결과

em.find()를 호출 하는 시점에 DB에 쿼리를 조회해서 값을 가져온다.
또한 getClass() 메소드를 통해 실제 회원 엔티티를 가져오는 것을 알 수 있다.

em.getReference()로 회원 조회하기

            final Member member = new Member();
            member.setName("hello");

            em.persist(member);

            em.flush();
            em.clear();


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

실행 결과

getReference()를 호출 하는 시점에는 DB에 쿼리를 조회하지 않는다.
그리고 나서 회원에 대한 정보가 필요할 때 그 때 DB에 쿼리를 조회해서 값을 가져오는 것을 확인할 수 있다.
또한 getClass() 메소드를 통해 실제 객체가 아닌 프록시 객체를 가져오는 것을 알 수 있다.

프록시 특징

  • 실제 클래스를 상속 받아서 만들어진다.
  • 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.

  • 프록시 객체는 실제 객체의 참조(target)를 보관한다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

프록시 객체의 초기화

  1. 회원 프록시 객체에서 getName() 메소드를 호출하면 프록시 객체는 실제 객체의 getName()메소드를 호출해야 하는데 아직 실제 객체가 어떤 것인지 알지 못하는 상태이다. (target = null)
  2. 따라서 영속성 컨텍스트에 실제 객체가 누구인지 요청하면 영속성 컨텍스트는 DB에서 실제 객체를 조회해 실제 객체를 생성한다.
  3. 프록시 객체에서 target을 실제객체로 초기화 한 후 (target = member)에target.getName()을 통해 실제 객체의 getName() 메소드를 호출한다.

프록시 정리

  • 프록시 객체는 처음 사용할 때 한번만 초기화한다.
  • 프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것이 아니라 초기화되면 프록시 객체를 통해 실제 엔티티에 접근이 가능하게 되는 것이다.
  • 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야 한다.(== 비교 실패, 대신에 instance of 사용)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다. (a == a : 항상 true)
    • JPA에서는 같은 트랜잭션(영속성 컨텍스트) 레벨안에서 같은 인스턴스를 조회하면 항상 같다고 나와야 한다.
    • 때문에 em.getReference()로 프록시를 조회 한 후 em.find()로 실제 객체를 조회해도 JPA에서는 ==을 맞추기 위해 실제 객체가 아닌 프록시 객체를 반환한다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때 프록시를 초기화하면 문제가 발생한다.(하이버네이트는 LazyInitializationException 예외를 터트린다.)
 final Member member = new Member();
 member.setName("hello");

 em.persist(member);

 em.flush();
 em.clear();


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

 // 1. em.detach(reference);
 // 2. em.close();
 
 System.out.println("reference.getName() = " + reference.getName());

실행 결과

reference.getName() 메소드를 호출하면 영속성 컨텍스트의 도움을 받아 실제 값을 가져와야 하는데 detach(reference)로 영속성 컨텍스트가 관리하지 않는 준영속 상태가 되면 값을 가져오지 못하기 때문에 값을 초기화할 수 없는 상태가 된다.

마찬가지로 em.close()로 영속성 컨텍스트를 닫아버리면 실제 값을 가져오지 못하기 때문에 값을 초기화할 수 없게 된다.

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object Entity)
  • 프록시 클래스 확인 방법
entity.getClass().getName()
  • 프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity);
  • 참고 : JPA 표준은 강제 초기화가 없다.
    강제 호출 : member.getName();
profile
현시깁니다

0개의 댓글