- 참조: 자바 ORM 표준 JPA 프로그래밍
- 지연 로딩
사내 주문 관리 시스템을 개발한다고 가정해보자. 요구사항은 아래와 같다.
- 회원은 팀 하나에만 속할 수 있다.
- 회원은 여러 주문내역을 가진다.
- 주문내역은 상품정보를 가진다.
이를 UML 다이어그램에서 다중성을 표시하면 위와 같다. 연관 엔티티를 로딩할 때 즉시냐 지연이냐를 결정하는것은 비즈니스 로직을 고려해야 한다. 비즈니스 로직이 아래와 같은 특성을 갖는다고 가정해보자.
- Member 와 Team 은 자주 함께 사용되었다.
- Member 과 연관된 Order 는 가끔 사용되었다.
- Order 와 연관된 Product 는 자주 함께 사용되었다.
@Entity
public class Member {
@Id
@Column(name = "USER_ID")
private String id;
private String userName;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID", referencedColumnName = "TEAM_ID")
private Team team;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Order> orders;
..................
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
private String orderName;
@ManyToOne
@JoinColumn(name = "USER_ID", referencedColumnName = "USER_ID")
private Member member;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "PRODUCT_ID", referencedColumnName = "PRODUCT_ID")
private Product product;
비즈니스 로직에서 자주 함께 사용되거나 상대적으로 덜 사용되는 요구사항에 따라 fetch 속성을 설정하였다. 먼저 회원은 팀과 함께 자주 사용되어 EAGER 로 설정했으며, 주문은 상대적으로 덜 사용되어 LAZY 로 설정하였다. 또한 주문에서 상품은 자주 함께 사용되어 EAGER 로 설정하였다.
테스트 코드의 순서는 다음과 같다. 회원 MEMBER1 을 팀에 종속시킨다. 회원은 어떤 상품을 주문한 주문건과 연관을 맺는다. 그리고 회원을 엔티티매니저에서 조회 한다.
public static void save(EntityManager em) {
EntityTransaction tx = em.getTransaction();
tx.begin();
Team team = new Team();
team.setName("Team#1");
em.persist(team);
Member member = new Member();
member.setId("MEMBER_1");
member.setUserName("UesrName#1");
member.setTeam(team);
em.persist(member);
Product product = new Product();
product.setProductName("ProductName#1");
em.persist(product);
Order order = new Order();
order.setOrderName("OrderName#1");
order.setProduct(product);
em.persist(order);
order.setMember(member);
tx.commit();
em.close();
}
..............
public static void find(EntityManager em) {
EntityTransaction tx = em.getTransaction();
tx.begin();
Member member = em.find(Member.class, "MEMBER_1");
System.out.println("member name: " + member.getUserName());
tx.commit();
em.close();
}
회원을 찾으면 아래와 같은 SQL 이 실행된다. Team 은 즉시 로딩이기 때문에 join 을 이용하여 한번에 조회하지만 Order 는 지연 로딩이기 때문에 검색하지 않는다.
Hibernate:
select
member0_.user_id as user_id1_3_0_,
member0_.team_id as team_id3_3_0_,
member0_.user_name as user_nam2_3_0_,
team1_.team_id as team_id1_8_1_,
team1_.name as name2_8_1_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.user_id=?
member name: UesrName#1
- 프록시와 컬렉션 래퍼
find 메소드를 아래와 같이 수정하자.
public static void find(EntityManager em) {
EntityTransaction tx = em.getTransaction();
tx.begin();
Member member = em.find(Member.class, "MEMBER_1");
List<Order> orders = member.getOrders();
System.out.println("class name: " + orders.getClass().getName());
System.out.println("first order's name: " + orders.get(0).getOrderName());
tx.commit();
em.close();
}
Order 를 실제로 사용하므로 주문을 조회하는 SQL 도 수행될것이다.
class name: org.hibernate.collection.internal.PersistentBag
Hibernate:
select
orders0_.user_id as user_id3_4_0_,
orders0_.order_id as order_id1_4_0_,
orders0_.order_id as order_id1_4_1_,
orders0_.user_id as user_id3_4_1_,
orders0_.order_name as order_na2_4_1_,
orders0_.product_id as product_4_4_1_,
product1_.product_id as product_1_7_2_,
product1_.product_name as product_2_7_2_
from
orders orders0_
left outer join
product product1_
on orders0_.product_id=product1_.product_id
where
orders0_.user_id=?
first order's name: OrderName#1
주문을 직접 사용하기 전에 출력한 class name 을 보면 PersistentBag 이 반환되었다. 앞에서 엔티티를 지연 로딩할 땐 프록시가 반환되었는데, 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리한다.
주문의 이름을 직접 출력해보는 메소드를 실행할 때 주문 SQL 이 수행되었다. 이때 주문에서 참조하는 상품 엔티티는 자주 함게 사용되므로 fetch 유형을 EAGER 로 설정하였으므로 join 을 이용하여 한꺼번에 조회한다.
- JPA 의 기본 fetch 전략
JPA 의 fetch 의 기본속성은 다중성 어노테이션에 따라 다르다.
- @ManyToOne, @OneToOne: 즉시 로딩 (EAGER)
- @OneToMany, @ManyToMany: 지연 로딩 (LAZY)
다중성에 따라 즉시 로딩 혹은 지연 로딩 기본값이 다른것은 상대방의 다중성 수에 따라 다르다. 상대방이 1 이면 즉시 로딩을 해도 성능에 이슈가 없으므로 기본적으로 즉시 로딩이며, 상대방이 N 이면 성능에 영향을 줄 수 있으므로 기본값이 지연 로딩이다.
책에서는 지연 로딩을 기본값으로 설정해놓고 비즈니스 로직에 따라 즉시 로딩을 사용해야할 경우에만 FetchType 을 설정해주는것을 추천하는데 일리가 있는 말이다.
- 즉시 로딩의 주의점
컬렉션을 2 개 이상 즉시 로딩하는것은 위험하다. 1 : N 관계에서 join 을 한다는것은 N 만큼 데이터가 늘어난다는것을 의미한다. 그런데 연관된 엔티티가 2 개 이상이 컬렉션 형태일 때 이를 즉시 로딩 한다면 2 개의 엔티티만큼의 수가 곱해져서 늘어난다. 따라서 2 개 이상의 컬렉션을 즉시 로딩할때에는 다시 한번 생각해보자.
optional 속성을 이용할 때, 그에 따라 실행될 outer 및 inner join SQL 사용에 주의한다. 회원과 팀의 관계에서 회원은 1 개의 팀에 무조건 종속되어야 한다면 회원에서 팀을 참조할 때에는 inner join 을 사용해도 된다. 하지만 팀에서 회원을 참조할 때에는(컬렉션을 참조할 때) 팀에 회원이 속하는 경우가 없을 수도 있으므로 outer join 을 해야 속한 회원이 없더라도 팀이 조회된다.
'Framework and Tool > JPA' 카테고리의 다른 글
JPA - 고급맵핑 - 요구사항 분석과 맵핑4 (0) | 2021.07.18 |
---|---|
JPA - 영속성 전이와 고아 객체 (0) | 2021.07.17 |
JPA - 즉시 로딩과 지연 로딩 (0) | 2021.07.15 |
JPA - 프록시 (0) | 2021.07.14 |
JPA - 고급맵핑 - 요구사항 분석과 맵핑3 (0) | 2021.07.13 |
댓글