jpa를 사용하다 보면 N + 1 문제를 겪게 될 것이다.
N + 1이란 무엇인가 ?
연관 엔티티를 조회할 때 처음 조회한 엔티티 수(N)만큼 추가로 조회 쿼리가 발생하는 현상
예시를 보자
member와 order이라는 두 엔터티가 있다고 가정하겠다.
class Member
id, name, List<Order>
class Order
id, name, price, Member
member와 order 1:N
member와 item 1:N
관계라고 가정한다.
order를 조회할 때, member 역시 조회하게 되는데 이때 맴버의 개수만큼 order를 조회한다.
전체 orders 조회
order.id == 1 조회
-> order.id == 1 인 member 조회
order.id == 2 조회
-> order.id == 2 인 member 조회
order 개수 만큼 member 자꾸 조회될 것이다.
지금은 연관관계가 하나라서 다행이지만 여러개라면 어떨까 ?
member 뿐만아니라 brand, delivery, factory ... 등등 여러가지가 있다면 ?
전체 orders 조회
order.id == 1 조회
-> order.id == 1 인 member, brand, delivery, factory ... 조회
order.id == 2 조회
-> order.id == 2 인 member, brand, delivery, factory ... 조회
이러면 쿼리문이 많이 보내기 때문에, 당연히 성능은 떨어질 수 밖에 없다.
이러한 문제를 해결하기 위해 fetch join을 사용한다.
fetch join은 무엇이냐?
쿼리가 실행되는 시점에 연관된 엔티티를 함께 조회, 즉 단일 쿼리로 필요한 데이터를 한 번에 가져온다.
여러번의 쿼리문을 보내 조회하는 것 대신 한 번의 쿼리문을 보냄으로서 성능을 개선시킬 수 있다.
select *
from order o
join fetch o.member m
이렇게 하면 한 번의 쿼리문만 보내도 똑같은 결과를 얻을 수 있을 것이다.
그럼 문제점은 없냐 ?
ToOne 관계일 때는 문제가 발생하지 않는다. 하지만 ToMany 관계인 경우는 ?
SELECT m
FROM Member m
JOIN FETCH m.orders
LIMIT 10 OFFSET 0
이러는 경우
member1 - order1
member1 - order2
member2 - order3
더욱 많은 row가 조회할 수 있다.
예를들어 member가 평균적으로 3개의 order를 가진다면 10명의 member를 조회시 30개나 되는 row가 반환된다.
해결법
1. ToOne 관계시 패치조인
2. 컬랙션은 지연로딩
3. 지연 로딩 최적화를 위해 BatchSize 적용
BatchSize를 적용하는 이유는 1 + N이 아닌 1 + 1로 최적화 하기위해서이다.
예시로
@Entity
public class Member {
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
@BatchSize(size = 100) // 이 사이즈만큼 IN 쿼리로 한번에 조회
private List<Order> orders = new ArrayList<>();
}
이런식으로 batchsize를 설정한다면
-- 1. Member 조회 (페이징 적용, ToOne 관계인 team도 함께 조회)
SELECT m.*, t.*
FROM member m
JOIN team t ON m.team_id = t.id
LIMIT 10 OFFSET 0
-- 2. 조회된 Member의 Order들을 한번에 IN 쿼리로 조회
SELECT *
FROM orders
WHERE member_id IN (1,2,3,4,5,6,7,8,9,10)
in query가 나가서 1 + 1로 쿼리문이 최적화되서 실행된다.