본문 바로가기

IT/JPA & Hibernate

[JPA] 프록시와 지연로딩

 

1. 프록시란?

프록시는 대리 응답, 중계 등 다양한 의미로 사용되는 용어입니다.
프록시는 JPA 기본 스펙은 아니지만 Hibernate에서 프록시라는 기능을 제공합니다.
프록시는 지연로딩(Lazy Loading) 기능을 위해 사용되며 지연로딩이란 연관 매핑 객체가 있을 때, 해당 객체의 조회를 미루고, 해당 객체를 사용할 때 조회하는 것을 의미합니다. 

 

2. 프록시 설명 및 사용 예제

원활한 테스트를 위해 미리 엔티티들을 생성해놓겠습니다. Member와 Team 엔티티가 있고 다대 일 관계입니다.
@Entity
public class Team {

    @Id
    @GeneratedValue
    private Integer id;

    private String name;
    
    //getter,setter 생략
}

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
    
    //getter, setter 생략
}

 

1) 프록시 객체 조회

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-persistence");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        try {

            Member member = new Member();
            member.setName("kim");
            em.persist(member);

            em.flush(); // 쓰기 지연 쿼리 실행
            em.clear(); // 영속성 컨텍스트 초기화

            //Member 프록시 객체 조회
            Member findMember = em.getReference(Member.class, member.getId());

            System.out.println("before");
            findMember.getName(); // name 조회 시 DB 조회 쿼리 실행
            System.out.println("after");

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
프록시 객체를 조회 할 때 em.getReference(Class, id) 메소드를 사용합니다. 
em.getReference 메소드를 실행하면 ID값만 갖고 있는 프록시 객체가 생성되고 영속성 컨테스트에 저장됩니다.
그리고 프록시 객체의 특정 데이터에 접근 할 때 DB로 데이터를 조회해 데이터를 채워넣습니다.
이러한 조회 방식을 LAZY 조회라고 합니다. 

위 코드는 Member 프록시 객체를 조회 한 후 member의 name을 호출하는 예제입니다. 그리고 name을 호출 할 때 member를 조회하는 것을 알 수 있습니다.
// findMember.getName(); 실행 시 member 객체를 조회한다. 

before
Hibernate: 
    select
        member0_.id as id1_0_0_,
        member0_.name as name2_0_0_,
        member0_.team_id as team_id3_0_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
after

 

2) 프록시 객체의 구조 및 초기화

프록시 객체의 구조

프록시 객체는 엔티티를 상속받습니다. 그리고 프록시 객체에 Entity target 멤버변수가 있는데, 이 target 변수로 원본 엔티티에 접근합니다. 프록시라는 이름이 붙어진 것도 이때문입니다.

Member 엔티티로 예를 들자면 em.getReference(Member.class, 1L); 실행 시 Member 프록시 객체를 반환하고, target은 null로 초기화 됩니다. 그리고 member.getName() 메소드 실행 시 member 데이터를 조회하고 영속성 컨텍스트에 저장됩니다. 그리고 target 변수는 영속성 컨텍스트의 member 객체를 참조하게 되고, target.getName()과 같이 name 데이터를 반환합니다.

프록시 객체 초기화

 

3) 프록시의 특징

1. 실제 클래스를 상속받아서 만들어진다.
2. 실제 클래스와 겉 모양이 같다.
3. 사용하는 입장에서 진짜 객체인지 프록시 객체인지 구분하지 않고 사용한다.
4. 프록시 객체는 실제 객체의 참조(target)를 보관한다.
5. 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
6. 프록시 객체는 처음 사용할 때 한번만 초기화한다.

 

4) 프록시 초기화 확인

public class JpaMain {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-persistence");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        try {

            //Member 프록시 객체 조회
            Member findMember = em.getReference(Member.class, 2L);

            // 초기화 유무 확인
            System.out.println(emf.getPersistenceUnitUtil().isLoaded(findMember));//false
            Hibernate.initialize(findMember);//프록시 객체 초기화
            System.out.println(emf.getPersistenceUnitUtil().isLoaded(findMember));//true

            tx.commit();
        } catch (Exception e) {
            System.out.println(e);
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

5) 지연 로딩

프록시 객체는 지연로딩 전략을 사용할 때 사용합니다.
사용 하는 방법은 연관 관계 매핑 객체의 Fetch 전략을 LAZY 속성으로 선언하면 엔티티 조회 시 해당 연관 관계 매핑 객체는 프록시 객체로 초기화 됩니다.   
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;


참조

https://www.inflearn.com/course/ORM-JPA-Basic/lecture/21709?tab=curriculum
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 (강추!)