즉시 로딩
회원
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
private List<Order> orders = new ArrayList<>();
...
}
주문정보
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne
private Member member;
...
}
- 예를 들어 회원과 주문정보가 1:N 양방향 연관관계이고, 회원이 참조하는 주문정보인 Member.orders를 즉시 로딩하는 경우 특정 회원 하나를 em.find() 메소드로 조회하면 즉시 로딩으로 설정한 주문정보도 함께 조회한다.
- 실제로 실행되는 SQL은 아래와 같으며, 조인을 사용해서 한 번의 SQL로 회원과 주문정보를 함께 조회한다.
SELECT M.*, O.*
FROM MEMBER M
OUTER JOIN ORDERS O ON M.ID = O.MEMBER_ID
N+1 문제
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
- 위와 같은 JPQL을 실행하면 JPA는 JPQL만 사용해서 SQL을 생성하므로, 즉시 로딩을 신경쓰지 않고 아래와 같은 SQL이 실행된다.
SELECT * FROM MEMBER
- SQL의 실행 결과로 회원 엔티티를 애플리케이션에 로딩하고, 회원 엔티티와 연관된 주문 컬렉션이 즉시 로딩으로 설정되어 있으므로, JPA는 주문 컬렉션을 즉시 로딩하기 위해 다음 SQL을 추가로 실행한다.
SELECT * FROM ORDERS WHERE MEMBER_ID = ?
- 만약 조회된 회원이 10명이면 조회한 각각의 회원 엔티티와 연관된 주문 컬렉션을 즉시 조회하기 위해 10번의 SQL이 추가로 실행된다.
- 이처럼 처음 실행한 SQL의 결과 수만큼 추가로 SQL을 실행하는 것을 N+1 문제라고 한다.
- 즉시 로딩은 JPQL을 실행할 때 N+1 문제가 발생할 수 있으며, N+1 문제는 JPA로 애플리케이션을 개발할 때 성능상 가장 주의해야 한다.
지연 로딩
회원
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();
...
}
- 위의 예체에서 회원과 주문을 지연 로딩으로 설정하면, JPQL에서는 N+1 문제가 발생하지 않는다.
- 지연 로딩이므로 데이터베이스에서 회원만 조회되고, 연관된 주문 컬렉션은 지연 로딩된다.
N+1 문제
- 하지만 이후 비즈니스 로직에서 주문 컬렉션을 실제 사용할 때 지연 로딩이 발생한다.
for (Member member : members) {
// 지연 로딩 초기화
System.out.println(member.getOrders().size());
}
- 위처럼 모든 회원에 대해 연관된 주문 컬렉션을 사용하는 경우, 주문 컬렉션을 초기화하는 수만큼 아래의 SQL이 실행된다.
SELECT * FROM ORDERS WHERE MEMBER_ID = ?
- 따라서 N+1 문제는 즉시 로딩과 지연 로딩일 때 모두 발생할 수 있다.
N+1 문제 해결 방법
1. 페치 조인 사용
- N+1 문제를 해결하는 가장 일반적인 방법은 페치 조인을 사용하는 것이다.
- 페치 조인은 SQL 조인을 사용해서 연관된 엔티티를 함께 조회하므로 N+1 문제가 발생하지 않는다.
select m from Member m join fetch m.orders
- 위의 JPQL을 실행하면 아래와 같은 SQL이 실행된다.
SELECT M.*, O.*
FROM MEMBER M
INNER JOIN ORDERS O ON M.ID = O.MEMBER_ID
2. 하이버네이트 @BatchSize
- 하이버네이트가 제공하는 @BatchSize 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정된 size만큼 SQL의 IN 절을 사용해서 조회한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@BatchSize(size = 5)
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
private List<Order> orders = new ArrayList<>();
...
}
- 예를 들어 조회한 회원이 10명인데 size를 5로 지정한 경우
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
- 즉시 로딩으로 설정하면 조회 시점에 10건의 데이터를 모두 조회해야 하므로 아래 SQL이 2번 추가로 실행된다.
- 지연 로딩으로 설정하면 지연 로딩된 엔티티를 최초 사용하는 시점에 SQL을 실행해서 5건의 데이터를 미리 로딩해둔고 6번째 데이터를 사용할 때 SQL을 추가로 실행한다.
SELECT * FROM ORDERS
WHERE MEMBER_ID IN (?, ?, ?, ?, ?)
3. 하이버네이트 @Fetch(FetchMode.SUBSELECT)
- 하이버네이트가 제공하는 @Fetch 어노테이션에 FetchMode를 SUBSELECT로 설정하면, 연관된 데이터를 조회할 때 서브쿼리를 사용해서 N+1 문제를 해결한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@BatchSize(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
private List<Order> orders = new ArrayList<>();
...
}
- 즉시 로딩으로 설정하면 SQL이 조회 시점에, 지연 로딩으로 설정하면 지연 로딩된 엔티티를 사용하는 시점에 실행된다.
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
// 실행되는 서브쿼리
SELECT O
FROM ORDERS O
WHERE O.MEMBER_ID IN (SELECT M.ID
FROM MEMBER M)
'Spring > JPA' 카테고리의 다른 글
스프링에서의 영속성 관리 (0) | 2022.03.05 |
---|---|
영속성 컨텍스트와 연속성 관리 (0) | 2022.03.05 |
JPA와 ORM (0) | 2022.02.23 |
JPQL과 페치 조인 (0) | 2022.02.23 |
QueryDSL (0) | 2022.02.04 |