JPA에서 LAZY와 EAGER의 차이
이전 작성한 Lazy Loading 문제에서 해결 방법 중 한가지의 내용을 다룹니다.
JPA(Java Persistence API)는 엔티티의 데이터 로딩 방식을 제어하기 위해 LAZY(지연 로딩)와 EAGER(즉시 로딩)라는 두 가지 방법이 있습니다. 이들의 장단점과 차이점에 대해 알아보겠습니다.
1. LAZY와 EAGER란?
1) LAZY (지연 로딩)
LAZY는 연관된 엔티티 데이터를 실제로 사용하는 시점
에 데이터베이스에서 조회합니다.
(1) 특징
- 초기에는 프록시 객체를 생성하여 연관 데이터를 로드하지 않습니다.
연관 데이터를 사용할 때 추가 쿼리를 실행하여 필요한 데이터를 로드
합니다.
(2) 장점
- 불필요한 데이터를 로드하지 않으므로 메모리 사용량을 줄이고 초기 로딩 속도를 높일 수 있습니다.
(3) 단점
- 연관 데이터를 사용할 때마다 추가 쿼리가 발생하므로, 잘못된 설계나 사용으로 인해 N+1 문제가 발생할 수 있습니다.
- 트랜잭션 범위 외부에서 데이터를 접근하려고 하면 LazyInitializationException이 발생할 수 있습니다. (실제 겪은 문제)
2) EAGER (즉시 로딩)
EAGER는 연관된 엔티티 데이터를 즉시 로드
하며, 엔티티 조회 시점
에 데이터베이스에서 모두
가져옵니다.
(1) 특징
- JPQL 또는 SQL 실행 시 JOIN 쿼리를 통해 연관된 데이터를 함께 조회합니다.
- 추가적인 데이터 로딩 없이 연관 데이터를 즉시 사용할 수 있습니다.
(2) 장점
- 추가적인 데이터베이스 쿼리를 방지하므로 데이터 일관성이 보장됩니다.
- 연관 데이터가 항상 필요한 경우 적합합니다.
(3) 단점
- 항상 모든 연관 데이터를 로드하므로 초기 로딩 속도가 느릴 수 있습니다.
- 사용하지 않는 데이터까지 로드하여 메모리 낭비가 발생할 수 있습니다.
2. Member와 Order 엔티티로 간단 예시
1) Entity
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY) // LAZY 지연 로딩
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
private String productName;
@ManyToOne(fetch = FetchType.EAGER) // EAGER 즉시 로딩
private Member member;
}
Member
와Order
는 @OneToMany와 @ManyToOne 관계로 연결되어 있습니다.Member
의orders
필드는 LAZY 로딩으로 설정되어, 데이터를 사용할 때만 로드합니다.Order
의member
필드는 EAGER 로딩으로 설정되어, 데이터 조회 시점에 즉시 로드됩니다.
2) 데이터 로딩 방식
public void example(EntityManager em) {
// Member를 조회 (LAZY 로딩)
Member member = em.find(Member.class, 1L); // member 객체를 조회
System.out.println("회원 이름: " + member.getName()); // 단순 필드는 항상 즉시 로드
// orders에 접근할 때 쿼리 실행 (지연 로딩)
for (Order order : member.getOrders()) { // orders 필드는 LAZY 지연 로딩이므로 처음 접근 시 쿼리 발생
System.out.println("주문 상품: " + order.getProductName());
}
// Order를 조회 (EAGER 로딩)
Order order = em.find(Order.class, 1L); // order 객체를 조회
System.out.println("주문한 회원: " + order.getMember().getName()); // member 필드는 EAGER이므로 즉시 로드
}
설명
member.getOrders()
를 호출하면 추가 쿼리가 실행되어 LAZY 로딩 동작이 발생합니다.order.getMember()
호출 시, 추가 쿼리 없이 즉시 로드된 데이터를 사용할 수 있습니다.
3. 주요 차이점 비교
특징 | LAZY (지연 로딩) | EAGER (즉시 로딩) |
---|---|---|
로딩 시점 | 연관 데이터 사용 시점 | 엔티티 조회와 동시에 로드 |
데이터베이스 쿼리 | 필요한 시점마다 실행 (N+1 문제 발생 가능) | 조회 시점에 JOIN 쿼리 실행 |
성능 | 초기 로딩 속도 빠름, 추가 쿼리로 인한 성능 저하 가능 | 초기 로딩 속도 느림, 추가 쿼리 없음 |
사용 사례 | 연관 데이터가 자주 사용되지 않거나 일부만 필요한 경우 | 연관 데이터가 항상 필요하거나 데이터의 크기가 작은 경우 |
4. 주의사항
1) N+1 문제
- LAZY 로딩은 연관 데이터가 반복적으로 호출될 경우 다수의 추가 쿼리를 발생시켜 성능 문제를 초래할 수 있습니다.
- 이를 해결하기 위해
fetch join
이나 @EntityGraph를 활용해 데이터를 한 번에 로드하는 방법을 고려해야 합니다.
// Fetch Join
String jpql = "SELECT m FROM Member m JOIN FETCH m.orders";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
2) 트랜잭션 범위
- LAZY 로딩은 트랜잭션 범위 외부에서 데이터를 로드하려고 하면 LazyInitializationException이 발생할 수 있습니다.
- 이를 방지하기 위해 필요한 데이터를 미리 로드하거나, 트랜잭션 범위를 명확히 관리해야 합니다.
3) 설계 원칙 (결론)
- 기본적으로 LAZY 로딩을 사용하고, 필요한 경우 특정 조회에서만 EAGER 로딩을 활용하는 것이 권장됩니다.
- EAGER 로딩은 연관 데이터가 항상 필요하고 데이터 크기가 작을 때 적합합니다.
5. 결론
1) LAZY 로딩
- 연관 데이터가 자주 사용되지 않거나, 데이터를 효율적으로 제어하고 싶은 경우 LAZY 로딩이 적합합니다.
- 예를 들어, 회원 정보를 조회할 때 회원의 기본 정보만 로드하고 주문 내역은 필요 시 로드하는 방식입니다.
주의
: LazyInitializationException, 구성이 복잡해질 경우 추가 쿼리 반복 호출 문제
2) EAGER 로딩
- 연관 데이터가 항상 필요하거나, 데이터 크기가 작아 성능에 큰 영향을 주지 않는 경우 EAGER 로딩을 사용할 수 있습니다.
- 예를 들어, 주문 정보를 조회할 때 주문한 회원의 정보가 항상 필요한 경우입니다.
주의
: 엔티티 조회와 동시에 로드되므로 성능 저하 발생 우려