어떤 엔티티를 조회할때, 그 엔티티의 하위 엔티티들을 가져오기 위해 별도의 쿼리가 실행되는 것을 N+1 문제라고 한다. N+1 문제는 DB 조회 성능에 악영향을 끼치기 때문에 JPA 사용시 꼭 주의해야 한다.
예를 들어 아래와 같은 두 엔티티가 있다고 해보자.
@Entity
public class User {
@Id
@Column(name = "user_id")
private Long id;
@OneToMany(mappedBy = "user")
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id
@Column(name = "order_id")
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
User 테이블에 총 100개의 컬럼이 있을때, User를 전체조회하면 총 몇개의 쿼리가 생길까?
101개의 쿼리가 생긴다. 왜냐하면 User를 조회하는 쿼리 하나가 실행된 후에, (select * from User
) 각 User와 연관된 Order를 조화하는 쿼리(select * from Order where user_id = ?
)가 User의 수만큼 실행되기 때문이다.
public class Order {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
그렇다면, 아래처럼 FetchType을 LAZY로 설정했을 때는 어떻게 될까?
이렇게 하면 User를 조회했을때 Order가 함께 조회되지 않기 때문에 하나의 쿼리만 실행된다.
하지만 N+1 문제가 해결된 것은 아니다. User만 조회할 때는 쿼리가 하나만 실행되지만, Order의 정보를 조회하기 위해선 별도의 쿼리를 날려야만 하기 때문이다. 그렇기 때문에 N+1 문제를 해결하려면 User조회 쿼리와 Order조회 쿼리를 하나로 만들기 위한 다른 방법을 사용해야한다.
1. 페치 조인(Fetch Join)
가장 일반적인 방법이다. 페치 조인은 연관된 엔티티나 컬렉션을 한 번에 같이 조회하도록 하는 JPQL의 문법이다. Order 테이블을 조인하여 하나의 쿼리로 정보를 가져오기 떄문에 N+1 문제가 해결된다.
하이버네이트가 제공하는 org.hibernate.annotations.BatchSize 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정된 size 만큼 SQL의 IN절을 사용해서 조회된다.
SELECT * FROM
order WHERE user_id IN(
?, ?, ?, ?, ?
)
IN절에 들어갈 수 있는 최대 인자 갯수를 따로 설정할 수도 있다. 최대 인자 수를 넘으면 여러개의 쿼리로 나뉘어서 실행된다.
@BatchSize(size=10)
연관된 데이터를 조회할 때 서브쿼리를 사용해서 조회된다.
SELECT * FROM user;
SELECT * FROM order
WHERE user IN(
SELECT user_id
FROM user
)
페치조인과 다르게 두개의 쿼리가 실행된다.
통상적으로는 지연 로딩을 설정하고 페치조인을 사용하는 것이 권장된다.