우선 n+1문제가 발생하는 상황부터 보면 이해가 빠르다.
이렇게 되면 이제 하위 엔티티를 조회하기 위해 N번의 추가 쿼리가 발생되버린다.
예시를 보자
1. Select * from parent
-> 부모 엔티티 10개를 가져온다
2. 각각의 부모에 대해 연관된 자식 엔티티를 조회
select * from child where parent_id = 1;
select * from child where parent_id = 2;
...
select * from child where parent_id = 10;
위의 예시처럼 부모의 자식들을 조회할 때 마다 우리는 위의 쿼리가 발생하기 때문에 성능저하 및 DB에 큰 부하를 줄 수 있다.
음,,,, 근데 지연로딩을 하더라도 하위 엔티티를 조회하는 순간 n+1 문제가 발생하지 않나요???
정확하다! 지연문제 만으로는 N+1을 완벽하게 방지할 수는 없다. 지연로딩은 기존 쿼리를 통해 N+1을 막을 뿐 해당 쿼리로 하위 엔티티를 조회하는 순간 다시 쿼리가 나가기 때문에 N+1을 방지하기 위해서는 아래와 같은 방법 또한 고려해야한다.
@Query("SELECT p FROM Parent p JOIN FETCH p.children")
List<Parent> findAllWithChildren();
다만 위의 방법을 과도하게 이용할 경우 DB 성능에 부하가 생길 수 있기 때문에 쿼리를 신중하게 짜야하고 그에 따른 인덱스 생성 또한 필수이다.
@EntityGraph(attributePaths = {"children"})
@Query("SELECT p FROM Parent p")
List<Parent> findAllWithEntityGraph();
@BatchSize(size = 10)
@OneToMany(mappedBy = "parent")
private List<Child> children;
@Query("SELECT p FROM Parent p LEFT JOIN FETCH p.children")
List<Parent> findParentsWithChildren();
위의 여러가지 방식이 있지만 현재 우리 회사에서는 2번인 fetch join과 5번인 쿼리 최적화를 자주 사용한다. n+1문제는 코드 짤때는 인지하기가 어렵고 막상 배포를 했을 때 DB I/O의 성능을 보고 확인하기 때문에 처음 쿼리를 짤 때 주의를 기울여 짜는 것이 중요하다. 그럼 오늘의 포스팅 마무리~