N+1 쿼리
서론
ORM을 통해 쿼리를 조회할때 객체안의 객체를 죄회하기 위해 또 다른 쿼리가 발생하는 경우가 있는데 그러면 처음 조회된 n개의 결과만큼 새로운 쿼리가 발생한다.
이걸 N+1 쿼리라고 하는데 오늘은 이 N+1 쿼리에 대해서 알아보자.
N+1쿼리
N+1 쿼리는 서버에 큰 부하를 줄 수 있다.
데이터를 조회할 때 1번 쿼리를 조회할때, 또 다른 10만, 100만 종류의 데이터를 조회한다면, 그리고 이걸 100명, 1000명이 동시에 조회한다면 서버에 큰 문제가 발생할 수 있다.
N+1 쿼리는 이런 경우에 발생할 수 있다.
예를 들어 집이 있고, 집에 사는 사람이 있다고 하자.
그럴 경우 집이라는 데이터 하나를 조회했을때, 집에 사는 사람들의 데이터까지 같이 조회가 되어버린다.
원인
즉시로딩의 경우 예를 들어 User 엔티티를 findAll()을 이용해 모든 사용자를 조회하려고할때, 사용자에 관한 내용만 조회되어야하는데 User의 연관관계 엔티티도 같이 조회된다.
따라서, 연관관계까지 데이터를 모두 조회하는 즉시로딩일 경우 N+1 문제가 발생하는 것이다.
지연로딩의 경우 필요할때만 연관데이터를 가져오기때문에 단순이 User엔티티를 조회할때 N+1문제가 발생하지 않지만, 하위 엔티티를 조회하는 경우 하위 엔티티는 프록시 객체이기때문에 상위 엔티티를 조회하고나서 하위 엔티티를 조회하는 쿼리가 나가기 때문에 N+1문제가 발생한다.
해결법
JPA에서는 두가지의 해결법이 존재한다.
Fetch Join
@Query("select u from User u join fetch u.order")
List<User> findAll();
DB에서 데이터를 가져올때 처음부터 연관된 엔티티나 컬렉션을 한번에 같이 조회하는 방법이다.
즉, 조회할때 관계에 있는 테이블을 같이 조회해서 하위 테이블을 N번 조회하는 쿼리가 나가지 않도록 하는 것이다.
이 방법은 단 한번의 쿼리만 발생하도록 설계할 수 있고, 특정 엔티티의 하위 엔티티의 하위 엔티티까지 가져올 수 있다.
단점으로는 번거롭게 쿼리문을 작성해야하고, JPA가 제공하는 페이징 단위로 데이터 가져오기 불가능하다.
EntityGraph 어노테이션
EntityGraph 상에 있는 Entity들의 연관관계 속에서 필요한 엔티티와 컬레션을 함께 조회할때 사용한다.
@EntityGraph(attributePaths = {"order"})
List<User> findAllEntityGraph();
매번 쿼리를 작성하지 않아도 되지만, outerJoin을 사용하기때문에 중복 데이터가 발생한다.
그래서 중복을 처리해주는 추가적인 로직을 써줘야한다는 단점이 있다.