문제 해결 기록

[JPA x QueryDSL] MultipleBagFetchException 해결 방법

타태 2022. 7. 10. 21:34


2022.07.09 - [공부 기록] - [Rest Assured] Rest Assured로 Mock과 함께 Controller Unit Test 하기

 

[Rest Assured] Rest Assured로 Mock과 함께 Controller Unit Test 하기

2022.06.11 - [Back-End/Java] - [JAVA] Socket with JDK [JAVA] Socket with JDK 2022.04.16 - [실전 공부] - [JAVA x Apache POI] 전략 패턴과 리플렉션을 활용하여 컬럼 자동 생성 엑셀 다운로드 구현하기 [JA..

ktae23.tistory.com



이미 *toMany 대상을 fetchJoin하는 쿼리에 또 다른 *toMany 관계를 fetchJoin하려 할때 MultipleBagFetchException 가 발생한다.

테스트하기 위해 MemberTeam, TeamInfo 엔티티를 만들고 Team에는 MemberTeamInfooneToMay로 연결 되어 있다고 상황을 설정했다..

@Entity  
@Getter  
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {

    @Id  
    @GeneratedValue  
    private Long id;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "team")
    private List<TeamInfo> teamInfos = new ArrayList<>();
}

이 상황에서 쿼리를 아래와 같이 작성 할 때 예외가 발생한다.

    private List<Team> fetchAll() {
        QTeam team = QTeam.team;
        return queryFactory
                .selectFrom(team )
                .leftJoin(team.members).fetchJoin()
                .leftJoin(team.teamInfos).fetchJoin()
                .fetch();
     }

검색해보면 이를 해결하기 위한 여러 방법들이 있다.

  1. 데이터가 많은 쪽을 fetch join 하고 다른 쪽을 lazy loading한 뒤 default_batch_fetch_size 설정으로 성능 개선 [참고]
  2. 데이터 타입을 Set으로 변경 [참고]
  3. fetch join 쿼리를 2회로 나누어 실행 [참고]
  4. mainQuerysubQuery를 별도 서비스에서 조회한 뒤 key를 기준으로 조립하기

기존 로직을 최대한 건들고 싶지 않아서 4번 방식을 이용해 Facade에서 조립하려 했지만 N * M 번의 순회를 매번 해야 한다는 것이 아쉬웠다.

2번을 제외하고 1번과 3번 중 고민하였고, 1번은 단순하기 때문에 3번에 대해 테스트 해보겠다.

레퍼런스에 나온 대로 쿼리를 나눠 사용하려면 아래와 같은 쿼리를 작성할 수 있다.

중복되는 쿼리를 mainQuery라는 변수로 뽑아서 각 조건을 추가해봤더니 동일하게MultipleBagFetchException이 발생하여 각각 작성 했다.

private List<Team> fetchAll() {
        QTeam team = QTeam.team;  

        final JPAQuery<Team> teamWithFetchMembers = queryFactory.selectFrom(team)  
          .leftJoin(team.members).fetchJoin();  
        teamWithFetchMembers.fetch();  

        final JPAQuery<Team> teamWithFetchTeamInfos = queryFactory.selectFrom(team)  
          .leftJoin(team.teamInfos).fetchJoin();  

        return teamWithFetchTeamInfos.fetch();
}

 


발생한 쿼리는 다음과 같다.

1번 쿼리

 select
        team0_.id as id1_2_0_,
        members1_.member_id as member_i1_1_1_,
        team0_.name as name2_2_0_,
        members1_.age as age2_1_1_,
        members1_.team_id as team_id4_1_1_,
        members1_.username as username3_1_1_,
        members1_.team_id as team_id4_1_0__,
        members1_.member_id as member_i1_1_0__ 
    from
        team team0_ 
    left outer join
        member members1_ 
            on team0_.id=members1_.team_id

2번 쿼리

    select
        team0_.id as id1_2_0_,
        teaminfos1_.id as id1_3_1_,
        team0_.name as name2_2_0_,
        teaminfos1_.name as name2_3_1_,
        teaminfos1_.team_id as team_id3_3_1_,
        teaminfos1_.team_id as team_id3_3_0__,
        teaminfos1_.id as id1_3_0__ 
    from
        team team0_ 
    left outer join
        team_info teaminfos1_ 
            on team0_.id=teaminfos1_.team_id


이렇게 2번의 fetch 쿼리가 실행되며 이에 대한 조회 결과는 아래의 초기화 테스트를 통과한다.

List<Team> result = repository.fetchAll();  

Assertions.assertEquals(2, result.size());  
for (Team team : result) {  
  Assertions.assertTrue(Hibernate.isInitialized(team.getMembers()));  
  Assertions.assertTrue(Hibernate.isInitialized(team.getTeamInfos()));  
}


fetch Join을 고민한다는 것은 조회 할 때마다 매번 함께 조회해야 하는 데이터일 확률이 높다.

때문에 따로 조회한 뒤 조립하기보다는 쿼리를 한번 더 실행하더라도 한번에 받아오는 것이 더 좋다고 결정했다.
또, lazy loading을 하고 default_batch_fetch_size 를 설정해도 좋겠지만, 이러한 설정의 도움 없이 발생하는 쿼리를 더 주의 깊게 보며 조심히 다루는 것도 좋은 연습이 된다고 생각한다.

반응형