[JPA] 일 대 다 Fetch Join + Pagination

김형진·2023년 5월 1일
0

참조객체의 데이터를 필요로 할 때 조회쿼리를 날리는 지연로딩의 N+1문제로 인한 성능저하를 극복하기 위해 사용한 fetch join에도 문제점이 있는데 바로 데이터베이스의 pagination 기능을 사용하기 어렵다는 것이다.

fetch join의 대상과 일 대 일 관계에서는 data의 row가 늘어나지 않기 때문에 상관없지만, 일 대 다 join관계에서는 join 대상 테이블의 data수가 많아짐에 따라 주요 엔티티의 data row 수도 같이 늘어나기 때문에 Database의 limit과 offset을 이용한 쿼리를 통해 pagination하는 것이 불가능하다.

fetch join을 하면서 페이징 기능을 사용하려고 하는 경우 위와 같은 문제를 해결하기 위해 Hibernate에서는 자체적으로 모든 데이터를 불러와 주요 엔티티의 중복 row를 없앤 후 offset과 limit을 적용하여 어플리케이션으로 보내준다.
하지만 이는 모두 메모리에서 처리되는 작업이기 때문에 데이터가 많은 경우 메모리가 뻑날 수 있다는 위험이 있기 때문에 애초에 일대 다 fetch join에서는 아무 장치 없이 page기능을 사용하고자 해서는 안된다.

그러면 일 대 다 관계에서 paging기능을 적용할 방법은 없을까?
문제를 해결하는 과정은 다음과 같다.

  1. 먼저 toOne관계의 참조객체만 fetch join하여 pagination한다. (~ 대 일 관계의 참조객체는 주요 엔티티의 데이터가 뻥튀기 될 일 없으니 fetch join해도 상관없음.)
  2. 페이지네이션은 정상처리 되었지만 toMany 관계의 참조객체는 fetch join하지 않았기 때문에, 애플리케이션에서 참조객체의 데이터를 사용하려는 시점에 데이터 조회 쿼리가 날라가며 n+1 문제가 발생하게 된다.
  3. application 글로벌 설정에서 hibernate의 default_batch_fetch_size 옵션을 준다
  4. 해당 옵션이 적용되면 주요 엔티티의 수만큼 참조객체를 불러오는 쿼리를 날리는 대신 hibernate가 1번 과정에서 불러온 주요 엔티티와 관련된 모든 toMany 참조객체를 in쿼리를 사용하여 한 번에 불러오게 된다

n개의 Team과 각 팀에 m명의 Member가 속해 있다고 할 때, Team과 Member를 처음부터 join해서 조회하면 team은 중복된 data가 발생하게 된다.
그래서 처음부터 member를 join해서 가져오는 대신 지연로딩을 유지하여 member 데이터를 조회하는 시점에 조회쿼리를 날리도록 하되, Team개수가 n개 이기 때문에 n번만큼 Member를 조회하도록 하는 대신 hibernate의 default_batch_fetch_size옵션을 사용하여
n개의 모든 Team과 관련된 모든 Member를 한 번에 조회하여 맵핑해주는 것이다.
(예를 들어 Team의 id가 1,2,3,...,n인 경우 select member where teamId = ? 을 n번 날리는게 아니라, select member where teamId in (1,2,3,...,n)으로 조회하여 한 번에 가져오는 방식)

개꿀기능이 아닐 수 없다

+추가

엔티티가 아닌 Dto를 꼭 사용해야 하는 경우라면 당연하게도 위의 기능을 직접적으로 사용할 수 없다.
이럴 때에는 위 기능의 동작 원리를 그대로 코드로 구현하면 된다.
먼저 Team객체들을 모두 조회해온 뒤 각 Team의 Id를 가지고 있는 Member를 조회하되, Team의 수만큼 순회하며 Member를 조회하지 말고 in쿼리를 이용하여 Team들과 관련된 Member들을 전부 불러온 뒤 어플리케이션에서 Team과 Member를 맵핑해주면 된다.

profile
히히

0개의 댓글