[JPA] 프록시와 지연로딩(Lazy), 즉시로딩(Eager)
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.
회원(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
쿼리가 발생하지 않았습니다.!!findMember
가 class 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();
만 사용한다면, 커밋 이후에도 DB
에 SQL
을 날리지 않습니다.
하지만, 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 o
가 SQL
로 번역되어서 Order
를 조회합니다.
그런데 어라?!! Member
가 EAGER
로 되어있네요?
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
지연로딩을 사용했는데도 한방에 조회되는 것을 볼수 있습니다.!👍
패치 조인은 내용이 많아 나중에 더 상세하게 포스팅 하겠습니다.^^
'0+ 스프링 > 0 + 스프링 ORM(JPA)' 카테고리의 다른 글
[JPA] CASCADE(영속성 전이)와 고아객체 (0) | 2023.02.24 |
---|---|
[JPA] JPA가 Entity를 판별하는 방법과 save()의 비밀(entityInformation.isNew(entity)) (0) | 2023.02.22 |
[JPA] @MappedSuperclass(공통 매핑 정보 해결 + 스프링 적용) (0) | 2023.02.20 |
[JPA] 상속관계 매핑 (0) | 2023.02.20 |
[JPA] 연관관계 매핑(@ManyToOne, @OneToMany, @OneToOne, @ManyToMany) (0) | 2023.02.20 |