1. FetchJoin과 페이징
- 모든 Team의 이름과, 각 Team의 Member들의 이름까지 알아야 하는 경우 Pagination을 이용하여 조회를 한다고 가정하자.

- N+1문제를 방지하기 위해 Fetch Join을 사용하였고, Pagealbe을 이용해 Page을 반환하게 하는 JPQL을 작성하였다.
- Pageable객체를 Spring Data JPA Repository에 인자로 전달하면, 사용자가 페이징 쿼리(JPQL)을 만들지 않아도 Spring Data JPA가 파라미터로 받은 Pageble 객체로 페이징, 정렬 쿼리를 만들어 준다. → 그렇다면 Paging 처리를 위해 mysql에서 limit, offset 쿼리가 포함될 것을 예상할 수 있다.

- [WARN] :
firstResult/maxResults specified with collection fetch; applying in memory

- 하지만 실제로는 페이징할 때 사용하는 limit 이 SQL에서 포함이 되어 있지 않는다. 또한 쿼리 결과를 전부 메모리에 적재한 뒤 어플리케이션 단에서 Pagination 작업을 수행한다는 경고 로그가 발생한다.
- Team 엔티티가 3개가 있고 각각의 Team 엔티티와 연관된 Member가 7명 씩 존재한다고 가정을 해보면 1:N관계를 Join하면 총 21개의 DB Row가 조회된다. 조회되는 데이터의 수가 변경되기 때문에 단순하게 LIMIT 구문을 사용하는 쿼리로 페이지네이션을 적용하기 어렵다.
- 쿼리 결과를 전부 메모리에 적재한 뒤 Pagination 작업을 어플리케이션 레벨에서 하기 때문에 위험하다는 로그이다. → 만약 Team이 10만 이고 각 Team당 연관된 Member도 10만이면 100만이 메모리에 적재된 뒤 Pagination 작업을 또 해야한다!

- MemberRepository에도 똑같은 상황으로 조회를 해보자


- 이번에는 경고 문자도 없고, limit 도 sql 문에 포함이 되어있다.
- Member와 Team는 N:1 관계이기 때문에 Join해도 조회되는 DB ROW의 수가 변경되지 않기 때문이다.
정리

- 김영한 님이 말쓸하신바에 정리를 해보자면
ToOne 애너테이션을 통해 형성된 관계인 경우 테이블 조인에 따라 데이터 수가 변경되지 않으므로 페이징 처리가 잘된다.
ToMany 애너테이션을 통해 형성된 관계인 경우 테이블 조인에 따라 데이터가 변경되어 페이징 처리와 페치 조인이 동시에 불가능하다. → 되더라도 모든 데이트를 메모리에 불럿와서 페이징을 적용하기 때문에 OutOfMemoryError가 발생할 수 있어 매우 위험하다!
- 이전 글인 N+1 게시글에서 N+1문제를 해결하는 방법은 FetchJoin말고도 BatchSIze가 있다.
- application.yml에 default_batch_fetch_size=10 으로 설정을 하고, Team의 개수가 100이라고 가정하자. → 원래 아무런 설정이 없었을 때는 1 + 100번의 조회 쿼리가 발생하지만 batchSize의 설정으로 인해 1 + 10으로 줄어든다.

- application.yml에서 batch_size를 설정한 것은 전역적으로 한 것이고 일부반 적용하고 싶다면 @BatchSize를 사용하면 된다. team 엔티티에서 연관된 데이터를 가져올 List에 FetchType도 EAGER이어야 한다.


- 다시 테스트를 돌려보면 먼저 teamId를 조회하고 그 팀 Id가 포함되어있는지 조회를 한다.
2. 연관관계에서 ‘1’인 엔티티에 FetchJoin을 2번 사용하는 경우

- 팀 엔티티와 1대N 관계를 하나 더 만들기 위해 Sponsor 엔티티를 하나 더 만들어 보았다. 팀 엔티티에는 OneToMany 어노테이션을 이용해 양방향으로 맺어 주었다.

- Sponsor 테이블에는 다음과 같이 데이터를 삽입해주었다.
Team을 조회하는데 팀의 Member와 Sponsor를 출력해야한다. 라고 가정을 해보면 Team 엔티티를 조회할 때 Fetch Join을 Member와 Sponsor 각각 하여 조회를 하면 될 것이라고 생각을 했다.

- 그러나 다음 테스트를 실행해보면
MultipleBagFetchException 이 발생한다다.
- ToMany에서 FetchJoin을 2번 사용해서 그렇습니다.
- ToOne은 몇개든 사용 가능합니다
- ToMany는 1개만 가능합니다.
그럼 ‘1’인 엔티티에서 ‘N’인 엔티티의 데이터를 어떻게 가져와야 할까요?

- 저는 처음에 TeamsDto 레코드를 생성하고 Left Join을 두 번 사용하면 문제가 해결이 될 줄 알았다.

- 그러나 테스트를 실행시켜 보면 다음과 같이 예외가 발생한다.

- 쿼리는 한방으로 나가는데 문제는 Member의 개수와 Sponsor의 개수가 곱해져서 출력이 되는 것이다.
- left join을 두 번 사용한다면, 첫 번째 left join 결과에 대해 두 번째 left join을 수행하게 됩니다. 이때 각각의 결과 행은 첫 번째 left join 결과 행과 두 번째 테이블 간의 카테시안 곱이 발생하게 되어서 전체 결과 행의 수가 다음과 같이 발생하게 되었다.
QueryDSL을 이용한 group_concat

- 이번에는 QueryDSL을 이용하여 mysql의 group_concat을 사용하였다.
- group_concat은 조건을 만족하는 결과값으로 합쳐져 있는 문자열로 받을 수가 있다.

- 다음과 같이 쿼리가 발생하고 결과를 얻을 수 있다.
- 이 방식 말고 BatchSize를 이용하여 쿼리를 나눠서 문제를 해결할 수 도 있다.
정리
- 연관관계에서 ‘1’인 엔티티에 FetchJoin을 2번 사용하는 경우 예외가 발생하기에 QueryDSL과 Mysql의 group_concat 함수를 이용하여 하나의 쿼리로 문제를 해결 할 수 있다. 잘못된 정보는 알려주시면 감사하겠습니다^^
참고
JPA Pagination, 그리고 N + 1 문제
JPA에서 Fetch Join과 Pagination을 함께 사용할때 주의하자
JPA Fetch 조인(join)과 페이징(paging) 처리
fetch join + Paging (limit) 처리 시 발생하는 문제 및 해결
MultipleBagFetchException 발생시 해결 방법