0+ 스프링/0 + 스프링 ORM(JPA)

[JPA] 하이버네이트 프록시와 지연로딩(Lazy), 즉시로딩(Eager)

힘들면힘을내는쿼카 2023. 2. 22. 10:34
728x90
반응형

[JPA] 프록시와 지연로딩(Lazy), 즉시로딩(Eager)

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

회원(Member)과 주문(Order)이 일대다(1:N) 관계로 설계 되었다고 합시다.
연관관계가 있으면 Order를 조회할때 Member항상 함께 조회해야 할까요? 🤔

주문과 회원을 함께 출력하는 코드

이때는 주문도 함께 조회하는 것이 좋아 보입니다. 👍

Order findOrder = em.find(Order.class, 1L);
Member findMember = findOrder.getMember();

System.out.println("주문 상태 = " + findOrder.getStatus());
System.out.println("회원 이름 = " + findMember.getName());

주문만 출력하는 코드

음… 만약 이때 주문도 함께 조회한다면 서버의 자원을 낭비하는 꼴이 되네요…. 👎

Order findOrder = em.find(Order.class, 1L);

System.out.println("주문 상태 = " + findOrder.getStatus());

결국에는 어느 경우에는 주문과 회원을 함께 조회해야 하고,
어느 경우에는 주문만 조회해야 합니다.
좋은 방법이 있을까요?

 

프록시

먼저! 프록시에 대해서 말씀 드리겠습니다.
프록시를 알아야 다음에 나오는 지연로딩, 즉시로딩을 이해할 수 있습니다.
JPA에서는 em.find() 말고 em.getReference() 라는 메소드가 있습니다.
em.getReference()DB 조회를 미루는 프록시(가짜) 엔티티 객체를 조회 합니다.

tx.begin();

Member member = new Member();
member.setName("밍밍이");

em.persist(member);

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

// 프록시 조회
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember="+findMember.getClass());

tx.commit();

SELECT 쿼리가 발생하지 않았습니다.!!
findMemberclass com.study.purejpa.entity.Member$HibernateProxy$3Alj9zrh 프록시 객체인것을 확인 할수 있습니다.
(em.flush(), em.clear()를 통해 영속성 컨텍스트를 초기화 했기 때문에 1차 캐시에 member는 없습니다.)

 

Hibernate: 
    /* insert com.study.purejpa.entity.Member
        */ insert 
        into
            Member
            (createDate, lastModifiedDate, city, name, street, zipcode, id) 
        values
            (?, ?, ?, ?, ?, ?, ?)
findMember=class com.study.purejpa.entity.Member$HibernateProxy$3Alj9zrh

 

그런데 여기에서 회원의 이름을 출력 해볼까요?

tx.begin();

Member member = new Member();
member.setName("밍밍이");

em.persist(member);

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

// 프록시 조회
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember="+findMember.getClass());
System.out.println("username="+findMember.getName());

tx.commit();

 

SELECT SQL이 나가면서, 밍밍이(회원 이름)가 조회 되었습니다…! 😯

findMember=class com.study.purejpa.entity.Member$HibernateProxy$kZq6Iob1
Hibernate: 
    select
        member0_.id as id1_6_0_,
        member0_.createDate as createda2_6_0_,
        member0_.lastModifiedDate as lastmodi3_6_0_,
        member0_.city as city4_6_0_,
        member0_.name as name5_6_0_,
        member0_.street as street6_6_0_,
        member0_.zipcode as zipcode7_6_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
username=밍밍이

em.getReference();만 사용한다면, 커밋 이후에도 DBSQL을 날리지 않습니다.
하지만, em.getReference()을 통해서 얻은 값을 실제로 사용한다면 이야기가 달라집니다.

 

프록시의 특징

프록시를 이용해서 JPA는 연관된 객체들을 모두 처음부터 DB에서 조회하는 것이 아니라,
실제 사용하는 시점에 DB를 조회할 수 있습니다.

  • 실제 클래스를 상속 받아서 생성됩니다.
    • 동일하지 않은 트랜잭션에서 타입 체크시 == 비교 대신, instance of 를 사용합니다.
      • (findMember1 instanceof Member)
    • 프록시 객체는 실제 클래스의 자식입니다.
  • 클라이언트는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됩니다.
  • 프록시 객체는 실제 객체의 참조(target)를 보관합니다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출합니다.
  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아닙니다.
    • 초기화가 되면 프록시 객체를 통해 실제 엔티티에 접근이 가능합니다.
  • 만약 영속성 컨텍스트(persistence context)에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티가 반환 됩니다.
    • 이때는 == 비교가 가능합니다.
  • 만약 em.getReference()를 호출하고, em.find()를 호출해도 프록시 객체가 반환 됩니다.
    • 이때는 == 비교가 가능합니다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생 합니다.

 

프록시 확인 메소드

  • 프록시 인스턴스 초기화 여부 확인
    • EntityManagerFactory.PersistenceUnitUtil.isLoaded(entity);
      • true / false
  • 프록시 클래스 확인
    • entity.getClass().getName()
  • 프록시 강제 초기화
    • JPA 표준은 강제 초기화가 존재하지 않음
    • org.Hibernate.initialize(entity);

지연 로딩(FetchType.LAZY)

다시 돌아가서, 주문을 조회할때 우리는 회원도 같이 조회해야 할까요?
아닙니다.
굳이 필요 없는것을 조회하면 손해겠죠?

@Entity
@Getter @Setter
@Table(name = "orders")
public class Order extends BaseTimeEntity {
    @Id @GeneratedValue
    private Long id;

    //..//

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
}

fetch = FetchType.LAZY으로 설정하게 되면 Member를 프록시 객체로 조회합니다.

 

실험

tx.begin();

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

Order order = new Order();
order.setStatus(OrderStatus.ORDER);
order.setMember(member);

em.persist(order);

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

Order findOrder = em.find(Order.class, order.getId());
// 프록시 조회
System.out.println("member="+findOrder.getMember().getClass());
// 프록시 초기화
System.out.println("memberName="+findOrder.getMember().getName());

tx.commit();

 

결과

    select
        order0_.id as id1_9_0_,
        order0_.createDate as createda2_9_0_,
        order0_.lastModifiedDate as lastmodi3_9_0_,
        order0_.delivery_id as delivery6_9_0_,
        order0_.member_id as member_i7_9_0_,
        order0_.orderDate as orderdat4_9_0_,
        order0_.status as status5_9_0_ 
    from
        orders order0_ 
    where
        order0_.id=?
member=class com.study.purejpa.entity.Member$HibernateProxy$rQdbdFb4
Hibernate: 
    select
        member0_.id as id1_6_0_,
        member0_.createDate as createda2_6_0_,
        member0_.lastModifiedDate as lastmodi3_6_0_,
        member0_.city as city4_6_0_,
        member0_.name as name5_6_0_,
        member0_.street as street6_6_0_,
        member0_.zipcode as zipcode7_6_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
memberName=밍밍이

Member가 프록시(member=class com.study.purejpa.entity.Member$HibernateProxy$rQdbdFb4)로 조회되고,
프록시 초기화 시점에 SELECT Member SQL이 생성된 것을 확인할 수 있습니다.

즉시 로딩(FetchType.EAGER)

주문을 조회할때 회원도 함께 조회하고 싶다면?
FetchType.EAGER를 사용하면 됩니다.

@Entity
@Getter @Setter
@Table(name = "orders")
public class Order extends BaseTimeEntity {
    @Id @GeneratedValue
    private Long id;

    //..//

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "member_id")
    private Member member;
}

fetch = FetchType.EAGER으로 설정하게 되면 Member를 실제 엔티티로 조회 합니다.

 

실험

tx.begin();

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

Order order = new Order();
order.setStatus(OrderStatus.ORDER);
order.setMember(member);

em.persist(order);

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

Order findOrder = em.find(Order.class, order.getId());
System.out.println("member="+findOrder.getMember().getClass());

System.out.println("memberName="+findOrder.getMember().getName());

tx.commit();

 

결과

Hibernate: 
    select
        order0_.id as id1_9_0_,
        order0_.createDate as createda2_9_0_,
        order0_.lastModifiedDate as lastmodi3_9_0_,
        order0_.delivery_id as delivery6_9_0_,
        order0_.member_id as member_i7_9_0_,
        order0_.orderDate as orderdat4_9_0_,
        order0_.status as status5_9_0_,
        member1_.id as id1_6_1_,
        member1_.createDate as createda2_6_1_,
        member1_.lastModifiedDate as lastmodi3_6_1_,
        member1_.city as city4_6_1_,
        member1_.name as name5_6_1_,
        member1_.street as street6_6_1_,
        member1_.zipcode as zipcode7_6_1_ 
    from
        orders order0_ 
    left outer join
        Member member1_ 
            on order0_.member_id=member1_.id 
    where
        order0_.id=?
member=class com.study.purejpa.entity.Member
memberName=밍밍이

Member를 조인해서 실제 엔티티로 조회된 것을 확인할 수 있습니다.

오….
연관관계가 있는 엔티티를 조회할때 사용하면 편할것 같네요!
라고 생각하면 안됩니다!! 안됩니다!! 안됩니다!! 안됩니다!!!!!!!!!!!!

즉시로딩 제발 사용하지마세요.

즉시로딩을 사용하면 예상하지 못한 SQL이 발생 합니다.
즉시로딩을 사용하면 N + 1 문제가 발생하게 됩니다.
(N + 1 문제는 1개의 SQL로 인해 N개의 SQL이 발생한다는 의미 입니다.)

만약에 연관관계가 걸려있는 테이블이 30개라고 합시다.
그 30개가 모두 즉시로딩으로 설정되어 있다면…?
이거 이거 EAGER 이상한데요?!

 

N + 1 예제

tx.begin();

Member member1 = new Member();
member1.setName("밍밍이");
em.persist(member1);

Member member2 = new Member();
member2.setName("싱싱이");
em.persist(member2);

Order order1 = new Order();
order1.setStatus(OrderStatus.ORDER);
order1.setMember(member1);
em.persist(order1);

Order order2 = new Order();
order2.setStatus(OrderStatus.CANCEL);
order2.setMember(member2);
em.persist(order2);

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

// jpql을 사용하여 조회하면? N + 1 문제 발생
List<Order> orders = em.createQuery("select o from Order o", Order.class)
        .getResultList();

tx.commit();

 

결과

Hibernate: 
    /* select
        o 
    from

    Order o */ select
        order0_.id as id1_9_,
        order0_.createDate as createda2_9_,
        order0_.lastModifiedDate as lastmodi3_9_,
        order0_.delivery_id as delivery6_9_,
        order0_.member_id as member_i7_9_,
        order0_.orderDate as orderdat4_9_,
        order0_.status as status5_9_ from
            orders order0_
Hibernate: 
    select
        member0_.id as id1_6_0_,
        member0_.createDate as createda2_6_0_,
        member0_.lastModifiedDate as lastmodi3_6_0_,
        member0_.city as city4_6_0_,
        member0_.name as name5_6_0_,
        member0_.street as street6_6_0_,
        member0_.zipcode as zipcode7_6_0_ 
    from
        Member member0_ 
    where
        member0_.id=?
Hibernate: 
    select
        member0_.id as id1_6_0_,
        member0_.createDate as createda2_6_0_,
        member0_.lastModifiedDate as lastmodi3_6_0_,
        member0_.city as city4_6_0_,
        member0_.name as name5_6_0_,
        member0_.street as street6_6_0_,
        member0_.zipcode as zipcode7_6_0_ 
    from
        Member member0_ 
    where
        member0_.id=?

😲 오잉...?
em.find()로 조회할때는 Member를 조인해서 하나의 SQL로 조회 했는데…
이번에는 3개의 SQL이 발생했습니다…

JPQL은 먼저 SQL로 번역이 되어서 DB에 날아갑니다.
select o from Order oSQL로 번역되어서 Order를 조회합니다.

그런데 어라?!! MemberEAGER로 되어있네요?

JPA는 즉시로딩해야지!!!!!
그래서 Member를 조회하기 위해 SQL이 2번 더 나갑니다.

이러한 문제를 모두 예상하기는 어렵습니다.
따라서 반드시 모든 연관관계는 지연로딩(fetch = FetchType.LAZY) 로 설정해서 사용하시기 바랍니다. 🙏

패치 조인

지연로딩을 사용하면 한번에 조회가 불가능 하고
한번에 조회 하려면 즉시로딩을 사용해야하고…
그렇지만, 실무에서는 지연로딩을 사용하는것이 좋고… 한번에 조회하고 싶은데 어쩌지? 😵‍💫

이러한 문제를 해결하기 위해 JPA는 패치조인을 제공합니다.

 

실험

// 패치조인 사용
List<Order> orders = em.createQuery(
        "select o" +
                " from Order o" +
                " join fetch o.member m", Order.class)
        .getResultList();

 

결과

Hibernate: 
    /* select
        o 
    from

    Order o join
        fetch o.member m */ select
            order0_.id as id1_9_0_,
            member1_.id as id1_6_1_,
            order0_.createDate as createda2_9_0_,
            order0_.lastModifiedDate as lastmodi3_9_0_,
            order0_.delivery_id as delivery6_9_0_,
            order0_.member_id as member_i7_9_0_,
            order0_.orderDate as orderdat4_9_0_,
            order0_.status as status5_9_0_,
            member1_.createDate as createda2_6_1_,
            member1_.lastModifiedDate as lastmodi3_6_1_,
            member1_.city as city4_6_1_,
            member1_.name as name5_6_1_,
            member1_.street as street6_6_1_,
            member1_.zipcode as zipcode7_6_1_ 
        from
            orders order0_ 
        inner join
            Member member1_ 
                on order0_.member_id=member1_.id

지연로딩을 사용했는데도 한방에 조회되는 것을 볼수 있습니다.!👍

패치 조인은 내용이 많아 나중에 더 상세하게 포스팅 하겠습니다.^^

 

 

728x90
반응형