JPA 연관관계 - 프록시(Proxy)


JPA 프록시(Proxy) 란?


JPA 에서는 find() 명령어를 사용하여 조회 할때 항상 데이터베이스를 조회하는 것은 아닙니다. 아래 case1 과 case2를 살펴 보겠습니다.

  • case1
public void findMemberAndTeacher(String id){
    Member member = em.find(Member.class, id);
    Teacher teacher = member.getTeacher();
    
    System.out.println("회원의 강사 : " + teacher.getName());
}
  • case2
public void findMemberAndTeacher(String id){
    Member member = em.find(Member.class, id);
    System.out.println("회원 이름 : " + member.getName());
}

case1 에서는 멤버 엔티티와 강사 엔티티를 모두 조회 하여 데이터를 조회 하는 반면에 case2에서는 Member 엔티티만 조회를 합니다.
case2 와 같이 멤버 엔티티만 사용하는 경우에 강사 엔티티 까지 조회하는 것은 효율 적이지 못합니다.

이런 문제를 해결 하기 위해서 JPA 에서는 엔티티를 사용하기 전까지 데이터베이스를 조회 하지 않는 지연 로딩 을 제공 합니다.
프록시 패턴(Proxy Pattern) 에 대해서 간단히 살펴 보겠습니다.

  • 프록시 패턴(Proxy Pattern)

    프록시 패턴

    • Subject : 공통 인터페이스
    • RealSubject.doAction() : 데이터베이스를 조회하는 클래스
    • Proxy.doAction() : RealSubject 의 수행을 대신 수행 하는 위임받은 대리인 클래스
    • Client 에서 doAction() 을 호출 하면 Proxy.doAction() 이 수행 되고 Proxy 클래스의 필요에 의해서 RealSubject.doAction()이 호출 되게 됩니다.

이러한 프록시 패턴을 이용하여 멤버 엔티티를 조회할때 프록시 엔티티를 우선 제공 하고 실제로 엔티티를 호출 하면 데이터 베이스를 조회 하게 됩니다.


프록시 사용 기초


  • 프록시 사용 방법

    em.find() 로 엔티티를 조회 하는 경우에는 영속성 컨텍스트에 엔티티가 없는 경우 데이터베이스를 조회 하게 됩니다. 지연 로딩을 하고자 할때는 em.getReference() 를 사용 하여 엔티티가 사용되기 전까지 조회를 지연 시킬 수 있습니다.

      public void findMemberAndTeacher(String id){
          Member member = em.find(Member.class, id);  //즉시 로딩
      }
        
      public void findMemberAndTeacher(String id){
          Member member = em.getReference(Member.class, id);  //프록시 엔티티 반환(로딩 X)
          System.out.println("회원 이름 : " + member.getName());  //실제 사용 될때 로딩(SQL 수행)
      }
    
  • 프록시 초기화

    프록시의 초기화는 member.getName()가 호출 되어 실제 데이터를 조회하는 것을 초기화 라고 합니다. 프록시는 영속성컨텍스트가 생성 되어있을때만 사용이 가능 하기 때문에 em.close() 후 호출 하게 되면
    org.hibernate.LazyInitalizationException 이 발생 되게 됩니다.

      public void findMemberAndTeacher(String id){
          Member member = em.getReference(Member.class, id);
          em.close();   //영속성 컨텍스트 종료
          System.out.println("회원 이름 : " + member.getName());  //LazyInitalizationException 발생
      }
    
  • 프록시 확인

    boolean PersistenceUnitUtil.isLoaded(Object entity) 메소드를 사용 하여 초기화 여부를 확인 할 수 있습니다.

      private static void findTeacher(String id, EntityManager em) {
          Teacher teacher = em.getReference(Teacher.class, id);
          teacher.getId();
            
          boolean isLoad = em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(teacher); //초기화 여부 확인
          System.out.println("초기화 여부 : " + isLoad);       //결과 : true
      }
    


엔티티 프록시 사용(FetchType)


  • 즉시 로딩(FetchType.EAGER)

    즉시 로딩의 경우 멤버를 조회 할때 강사테이블 까지 JOIN 을 사용 하여 즉시 조회를 하게 됩니다. 아래 예를 살펴 보겠습니다.

    • 멤버 엔티티

        @Entity
        @Table(name="MEMBER")
        public class Member {
            @Id
            private String id;
                  
            @ManyToOne(fetch = FetchType.EAGER)   // 즉시 로딩 설정
            @JoinColumn(name = "TEACHER_ID")
            private Teacher teacher;
            // ..getter, setter
        }
      
    • 실행

        private static void findMemberAndTeacher(String memId, String teacherId, EntityManager em) {
            Member member = em.find(Member.class, memId);  //조회 발생(join)
            Teacher teacher = member.getTeacher();
            System.out.println(teacher.getName());
        }
      
    • hibernate 조회 쿼리

        Hibernate: 
            select
                member0_.id as id1_0_0_,
                member0_.name as name2_0_0_,
                member0_.TEACHER_ID as TEACHER_3_0_0_,
                teacher1_.TEACHER_ID as TEACHER_1_1_1_,
                teacher1_.name as name2_1_1_ 
            from
                MEMBER member0_ 
            left outer join
                Teacher teacher1_ 
                    on member0_.TEACHER_ID=teacher1_.TEACHER_ID 
            where
                member0_.id=?
      
  • 지연 로딩(FetchType.LAZY)

    지연 로딩의 경우 멤버를 우선 조회 하고 강사 엔티티가 필요한 경우 강사 엔티티를 조회 하기 때문에 조회 쿼리가 각각 수행 됩니다.
    아래 예를 살펴 보겠습니다.

    • 멤버 엔티티

        @Entity
        @Table(name="MEMBER")
        public class Member {
            @Id
            private String id;
                  
            @ManyToOne(fetch = FetchType.LAZY)   // 지연 로딩 설정
            @JoinColumn(name = "TEACHER_ID")
            private Teacher teacher;
            // ..getter, setter
        }
      
    • 실행

        private static void findMemberAndTeacher(String memId, String teacherId, EntityManager em) {
            Member member = em.find(Member.class, memId);
            Teacher teacher = member.getTeacher();
            System.out.println(teacher.getName());
        }
      
    • hibernate 조회 쿼리

        Hibernate: 
            select
                member0_.id as id1_0_0_,
                member0_.name as name2_0_0_,
                member0_.TEACHER_ID as TEACHER_3_0_0_ 
            from
                MEMBER member0_ 
            where
                member0_.id=?
        Hibernate: 
            select
                teacher0_.TEACHER_ID as TEACHER_1_1_0_,
                teacher0_.name as name2_1_0_ 
            from
                Teacher teacher0_ 
            where
                teacher0_.TEACHER_ID=?
      

마무리

오늘은 프록시에 대해서 정리 해보았습니다. 매번 데이터베이스를 조회 하여 모든 엔티티를 영속성 컨텍스트에 올려 놓는 것은
효율 적이지 못합니다. 오늘 정리 한 프록시의 지연로딩 과 즉시로딩을 적절히 활용 하면 효율적인 프로그래밍을 할 수 있을 것입니다.

Back to blog