반응형

 

프록시

  • Member와 Team이 연관관계에 있을 때, Member를 조회할 때 Team도 같이 조회해야 하는가?
    • Member만 사용되는 경우에는 Team 까지 같이 조회한다면 불필요한 Join이 발생하여 비용이 추가로 발생한다.
    • 반면에 Member와 Team이 모두 다 사용되는 경우에는 한 번 호출로 필요한 정보를 모두 얻어와서 추가적으로 호출을 보내지 않는다.
  • JPA는 위와 같은 상황을 프록시를 이용해서 해결한다.
  • em.find()
    • 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference()
    • 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
    • 값이 실제로 사용되는 시점에 데이터베이스에 쿼리를 보낸다.

 

프록시 특징

  • 실제 클래스를 상속 받아서 만들어 지며 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 알 수 없다. 구분하지 않고 사용하면 된다.
  • 프록시 객체는 실제 객체의 참조(target)를 가지고 있다.
    • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 참조를 이용해서 메소드를 호출한다.
  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  • 프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 초기화 되면 프록시 객체 내부의 참조(target)가 실제 엔티티를 가리키며 이를 이용해 실제 엔티티의 값을 얻어온다.
  • 프록시 객체는 원본 엔티티를 상속 받는다. 따라서 타입 체크 시 주의해야 한다.
    • 타입 비교는 == 비교 대신에 instance of 키워드로 비교해야 한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference() 를 호출해도 실제 엔티티를 반환한다. 반대로 em.getReference() 를 호출한 경우라면 em.find() 를 호출해도 프록시 객체를 반환한다.
    • JPA에서는 같은 영속성 컨텍스트 안에서 동일한 Key라면 동일한 인스턴스임을 보장한다.
    • 컬렉션 인스턴스에서 동일한 키를 꺼내는 것과 같다.
  • 같은 영속성 컨텍스트에서 프록시 객체를 여러 개 사용 하더라도 동일한 프록시 객체를 사용한다.
    • 같은 영속성 컨텍스트 안에서 동일한 인스턴스임을 보장하기 위함이다.
    Member findMember1 = em.getReference(Member.class, 1L);
    Member findMember2 = em.getReference(Member.class, 2L);
    Member findMember3 = em.getReference(Member.class, 3L);
    System.out.println("findMember1 = " + findMember1.getClass());
    System.out.println("findMember2 = " + findMember2.getClass());
    System.out.println("findMember3 = " + findMember3.getClass());
    [결과]
    findMember1 = class hellojpa.Member$HibernateProxy$Ku4ACJ8e
    findMember2 = class hellojpa.Member$HibernateProxy$Ku4ACJ8e
    findMember3 = class hellojpa.Member$HibernateProxy$Ku4ACJ8e
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생한다.
    • 프록시 객체는 영속성 컨텍스트와 상호작용을 통해서 동작한다. 영속성 컨텍스트에서 관리되지 않으면 org.hibernate.LazyInitializationException 에러가 발생한다.

 

  • 예) 프록시 객체 동작
Member member = em.getReference(Member.class, "id1");
member.getName();
  1. 프록시 객체 조회 시, 프록시가 모르는 값에 대해서 요청을 하면
  1. 프록시 객체는 영속성 컨텍스트에 초기화 요청을 한다.
  1. 영속성 컨텍스트는 데이터베이스를 조회해서
  1. 실제 엔티티 객체를 생성하여 프록시 객체의 target에 참조를 연결한다.
  1. 프록시 객체는 실제 엔티티의 참조를 이용해서 원래 요청을 수행한다.

 

프록시 확인 방법

  • PersistenceUnitUtil.isLoaded(Object entity) : 프록시 인스턴스의 초기화 여부 확인
  • entity.getClass().getName() : 프록시 클래스 확인
  • org.hibernate.Hibernate.initialize(entity) : 프록시 강제 초기화
    • JPA 표준에는 강제 초기화가 없다.
    • 강제 초기화를 지원하지 않으면 강제 호출 방식으로 프록시 초기화를 유도할 수 있다.
  • 예)
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();
    
    Member findMember1 = em.getReference(Member.class, 1L);
    System.out.println("findMember1 = " + findMember1.getClass()); //entity.getClass().getName()
    
    boolean beforeLoaded = emf.getPersistenceUnitUtil().isLoaded(findMember1);
    System.out.println("before Loaded = " + beforeLoaded);// false
    
    Hibernate.initialize(findMember1); //프록시 강제초기화
    //findMember1.getName(); // 강제 호출
    
    boolean afterLoaded = emf.getPersistenceUnitUtil().isLoaded(findMember1);
    System.out.println("after Loaded = " + afterLoaded); //true

 

즉시 로딩과 지연 로딩

  • FetchType.LAZY
    • 지연 로딩
    • 프록시 객체로 조회한다.
    • 실제 엔티티를 사용하는 시점에 프록시 객체가 초기화된다.
      • 예)
      @Entity
      @Getter @Setter
      public class Member {
          @Id
          @GeneratedValue
          private Long id;
      
          @Column(name = "USERNAME")
          private String name;
      
          @ManyToOne(fetch = FetchType.LAZY) // FetchType.LAZY으로 설정
          @JoinColumn(name = "TEAM_ID")
          private Team team;
      
      }
      
      Team team = member.getTeam(); // 단순히 프록시 객체를 얻어옴
      team.getName(); // 실제 엔티티를 사용하는 시점
  • FetchType.EAGER
    • 즉시 로딩
    • 실제 엔티티 객체로 조회한다.
    • 엔티티를 조회 하는 시점에 연관된 엔티티를 조인을 사용해서 함께 가져온다.
    • JPA 구현체에서 즉시 로딩을 구현하는 방법
      • 조인을 사용해서 함께 연관된 데이터도 가져오는 방법
      • 먼저 사용할 엔티티(Member)를 조회하고 이 엔티티의 연관관계 엔티티(Team)가 즉시 로딩 설정되어 있다면 연관관계 엔티티(Team)에 대한 조회 쿼리를 추가적으로 보내서 데이터를 가져오는 방법

 

프록시와 즉시로딩 주의점

  • 실무에서는 모든 연관관계에 지연 로딩(FetchType.LAZY)을 사용해야 한다.
    • 즉시 로딩(FetchType.EAGER)을 사용하면 예상하지 못한 SQL이 발생된다
  • 즉시 로딩(FetchType.EAGER)은 JPQL에서 N+1 문제를 일으킨다.
    • N+1 문제란 최초 쿼리를 1번 날렸는데 추가적인 쿼리 N번이 발생하는 경우를 말한다.
    • 즉시 로딩(FetchType.EAGER)은 데이터를 조회할 때 모든 값이 세팅되어 있어야 한다. 따라서 Member를 조회했을 때 Team이 즉시 로딩(FetchType.EAGER)라면 Team의 값을 세팅하기 위해 추가적인 쿼리가 발생한다.
    • 예)
    Team teamA = new Team();
    teamA.setName("TeamA");
    em.persist(teamA);
    
    Team teamB = new Team();
    teamB.setName("TeamB");
    em.persist(teamB);
    
    Member member = new Member();
    member.setName("aaa");
    member.setTeam(teamA);
    em.persist(member);
    
    Member member2 = new Member();
    member2.setName("bbb");
    member2.setTeam(teamB);
    em.persist(member2);
    
    em.flush();
    em.clear();
    
    List<Member> members = em.createQuery("select m from Member m", Member.class)
            .getResultList();
    
    
    [결과]
    /* select m from Member m */ //JPQL 쿼리 1번 호출
    select
        member0_.id as id1_3_,
        member0_.USERNAME as USERNAME2_3_,
        member0_.TEAM_ID as TEAM_ID3_3_ 
    from
        Member member0_
    ==========================================
    select //Member에 해당하는 Team의 값을 세팅하기 위해 발생한 추가적인 쿼리
        team0_.id as id1_5_0_,
        team0_.name as name2_5_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
    ==========================================
    select //Member에 해당하는 Team의 값을 세팅하기 위해 발생한 추가적인 쿼리
        team0_.id as id1_5_0_,
        team0_.name as name2_5_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
  • @ManyToOne, @OneToOne 은 기본 값이 즉시 로딩(FetchType.EAGER)이다. 이를 지연 로딩(FetchType.LAZY) 으로 설정해야 한다.

 


[참고자료]

자바 ORM 표준 JPA 프로그래밍 - 기본편, 김영한

 

반응형

+ Recent posts