프록시

  • 연관된 객체를 자유롭게 탐색하기 위해 사용하는 기술입니다.
  • 엔티티를 조회할 때 연관된 엔티티들이 항상 같이 조회되는 것은 아닙니다.
  • 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 지연 로딩이라고 하는데 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 그것을 프록시 객체라고 합니다.
  • 프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라 실제 사용하는 시점에서 데이터 베이스를 조회할 수 있습니다.

공통 예제 코드  ※

@Entity
@Getter @Setter
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name ="GROUP_ID")
    private Group group;
    
}

@Entity
@Getter @Setter
@Table(name = "GROUPS")
public class Group {
    @Id
    @GeneratedValue
    private Long id;
    
    private String groupName;
}
  • Group 엔티티에 @Table해주는 이유는 group라는 예약어가 있기 때문입니다.

 

프록시 기초

  • JPA에서 식별자로 엔티티를 조회할 때 EntityManager.find(반환 객체, 식별자(PK))를 사용하는데 영속성 컨텍스트에 엔티티가 없으면 데이터베이스에서 조회합니다.
  • find() 메서드를 사용하면 조회한 엔티티를 실제 사용하든 사용하지 않든 데이터베이스에서 조회를 하는데 조회를 미루고 싶으면 EntityManager.getReference(반환 객체, 식별자(PK))를 사용합니다.
  • getReference() 메서드를 이용하면 JPA는 데이터베이스를 조회하지 않고 실제 엔티티를 생성하지 않는 대신 데이터베이스 접근을 위임한 프록시 객체를 반환합니다.

 

프록시 초기화

  • 프록시 초기화는 프록시 객체를 실제 데이터 값을 사용할 때 조회해서 실제 엔티티 객체를 생성합니다.

 

 

 

프록시 기초와 프록시 초기화

import javax.persistence.*;

public class Jpa {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();
        try {
            transaction.begin();
            //영속성 컨텍스트에 저장
            Member member = new Member();
            member.setName("hoestory");
            em.persist(member);
            em.flush(); // 데이터베이스에 저장
            em.clear(); // 영속성 컨텍스트를 비우기
            
            // 조회한 엔티티를 실제 사용하든 않든 데이터베이스에서 조회하게됨
            Member findMember = em.find(Member.class,member.getId(); 
            
            //결과
            //findMember.getClass() = class Member
            System.out.println("findMember.getClass() = " + findMember.getClass());
            
            // 프록시 객체 : 사용하지 않는 객체는 데이터베이스에서 조회를 미룸.
            Member proxyMember = em.getReference(Member.class, member.getId();
            
            // 결과
            //proxyMember.getClass() = class Member$HibernateProxy$cbNUX9Ay
            System.out.println("proxyMember.getClass() = " + proxyMember.getClass());
            
            // 초기화 : 실제값을 조회
            proxyMember.getName();

            transaction.commit();

        } catch (Exception e) {
            e.printStackTrace();
            transaction.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

  • find() : 사용하고 싶든 사용하고 싶지 않든 데이터베이스에서 조회를 합니다.
  • getReference() : 데이터베이스의 조회를 미루고 실제 엔티티의 객체를 생성하고 싶으면 프록시를 초기화해주면 됩니다
  • proxyMember.getName() : 프록시 초기화

 

프록시 특징

  • 처음 사용할 때 한 번만 초기화됩니다.
  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것이 아니고 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있습니다.
  • 영속성 컨텍스트에 찾는 엔티티가 있으면 getReference()를 호출해도 프록시 객체가 아닌 실제 엔티티를 반환합니다.
  • 프록시 초기화는 영속성 컨텍스트의 도움을 받아야 되기 때문에 준영속 상태에서 프록시를 초기화할 경우 org.hibernate.LazyInitalizationException 예외가 발생합니다.

 

프록시 초기화 확인 및 강제 초기화

  • 프록시 초기화를 확인하는 방법은 JPA에서 제공하는 PersistenceUnitUtil.isLoaded(엔티티)를 사용하시면 됩니다.
  • 초기화가 안된 프록시 인스턴스는 false가 나오고 초기화가 되거나 프록시 객체가 아닌 엔티티는 true가 나옵니다.
  • 프록시를 강제로 초기화하는 방법은 하이버네이트에서 제공하는 org.hibernate.Hibernate.initialize(엔티티. getXxx)를 사용하시면 됩니다.

 

즉시 로딩과 지연 로딩

 

즉시 로딩 : 어떤 엔티티를 호출할 때 그 엔티티와 연관된 엔티티들도 함께 조회합니다.

설정 방법 : @ManyToOne(fetch = FetchType.EAGER)

 

@Entity
@Getter @Setter
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name ="GROUP_ID")
    private Group group;
    
}


import javax.persistence.*;

public class Jpa {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("playbook");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();
        try {
            transaction.begin();

            Group group = new Group();
            group.setName("group1");
            em.persist(group);

            Member member = new Member();
            member.setUsername("member1");
            member.setGroup(group);
            em.persist(member);
            em.flush();
            em.clear();

            Member findMember = em.find(Member.class, member.getId());
            
            transaction.commit();

        } catch (Exception e) {
            e.printStackTrace();
            transaction.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

}

 

즉시 로딩했을 때 쿼리문

 

 select
        member.id as id,
        member.GROUP_ID as GROUP_ID,
        member.username as username,
        group1.GROUP_ID as GROUP_ID,
        group1.name as name 
    from
        Member member
    left outer join
        GROUPS group
            on member.GROUP_ID = group.GROUP_ID 
    where
        member.id=?

 

회원을 조회하였는데 그룹도 같이 조인되어서 나왔습니다. 그 이유는 즉시 로딩을 최적화하기 위해 조인 쿼리를 사용한 것이기 때문입니다.

조인분을 자세히 보시면 외부 조인을 사용하였습니다. 왜냐하면 Member 테이블에서 외래키인 Group_ID가 NULL 값을 허용했기 때문입니다.

그래서 이럴 경우 내부 조인을 사용할 경우 그룹에 속하지 않은 회원과 그룹 중 아무것도 조회할 수 없기 때문에 외부 조인이 사용되었습니다.

내부 조인을 사용하고 싶으면 외래키 값을 NULL값을 허용해주면 안 됩니다. 

연관관계 주인에 있는 @JoinColumn 속성 중 nullable이 있습니다. nullable 속성을 false로 해주면 NULL 값을 허용을 안 해서 내부 조인을 사용할 수 있습니다. 아니면 @ManyToOne 속성 중 optional을 false로 해주면 됩니다.

 

 

지연 로딩 : 연관된 엔티티를 프록시로 조회를 하고 프록시 객체를 초기화하면서 데이터베이스를 조회합니다.

설정 방법 : @ManyToOne(fetch = FetchType.LAZY)

 

 

@Entity
@Getter @Setter
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name ="GROUP_ID")
    private Group group;
    
}
// Group 엔티티 생략

import javax.persistence.*;

public class Jpa {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("playbook");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();
        try {
            transaction.begin();

            Group group = new Group();
            group.setName("group1");
            em.persist(group);

            Member member = new Member();
            member.setUsername("member1");
            member.setGroup(group);
            em.persist(member);
            em.flush();
            em.clear();

            Member findMember = em.find(Member.class, member.getId());
            Group proxyGroup = findMember.getGroup();
            
            transaction.commit();

        } catch (Exception e) {
            e.printStackTrace();
            transaction.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

}

 

지연 로딩했을 때 쿼리문

 

  select
        member.MEMBER_ID as MEMBER_ID,
        member.GROUPS_ID as GROUPS_ID,
        member.username as username 
    from
        Member member 
    where
        member.MEMBER_ID=?

 

즉시 로딩 쿼리 문과 비교해보면 지연 로딩 쿼리문은 조회하려는 엔티티에 관한 쿼리문만 나왔습니다.

즉시 로딩은 Member엔티티와 Group엔티티를 한 번에 조회하였는데 지연 로딩은 Group 엔티티가 프록시 객체여서 조회가 미뤄졌습니다. Group 엔티티도 조회를 하고 싶으면 프록시를 초기화를 해주면 됩니다.

 

 Member findMember = em.find(Member.class, member.getId());
Group proxyGroup = findMember.getGroup();
// 추가
// 프록시 객체 초기화
proxyGroup.getName();

 

프록시 객체 초기화 후 쿼리문

  select
        member.MEMBER_ID as MEMBER_ID,
        member.GROUPS_ID as GROUPS_ID,
        member.username as username 
    from
        Member member 
    where
        member.MEMBER_ID=?

select
        group.GROUPS_ID as GROUPS_ID,
        group0_.name as name 
    from
        GROUPS group 
    where
        group.GROUPS_ID=?

 

※ 참고

@ManyToOne, @OneToOne의 fetch 기본값은 EAGER(즉시 로딩)

 

@OneToMany, @ManyToMany의 fetch 기본값은 LAZY(지연 로딩)

 

모든 연관관계를 지연 로딩으로 사용하는 것을 추천드립니다. 왜냐하면 컬렉션을 로딩할 때 비용이 많이 들고 잘못하면 너무 많은 데이터를 로딩할 수 있는 문제가 발생할 수 있기 때문입니다. 그래서 상황을 보고 즉시 로딩을 사용해야 할 경우만 즉시 로딩을 사용하시기를 바랍니다.

'JPA' 카테고리의 다른 글

[JPA] N+1 원인 및 해결방법  (0) 2023.01.18
[Querydsl] 스프링 부트에서 Querydsl 설정  (0) 2022.06.14
[JPA] 단방향, 양방향, 연관관계 주인  (0) 2022.01.26
[JPA] 엔티티 매핑  (0) 2022.01.25
[JPA]영속성 컨텍스트란?  (0) 2022.01.19