본문 바로가기
Framework and Tool/JPA

JPA - 지연 로딩

by ocwokocw 2021. 7. 16.

- 참조: 자바 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 을 해야 속한 회원이 없더라도 팀이 조회된다.

댓글