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