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 로딩을 사용할 수 있습니다.
- 예를 들어, 주문 정보를 조회할 때 주문한 회원의 정보가 항상 필요한 경우입니다.
주의: 엔티티 조회와 동시에 로드되므로 성능 저하 발생 우려