spring boot/기술 적용

MultipleBagFetchException 발생시 - fetchJoin 여러개 썼을 때

ballde 2022. 6. 24. 09:48

JPA에서 N + 1 문제를 해결하려고 fetchJoin 을 사용하게 됩니다. 이 때 하나의 entity에서 1:N이 여러개 일때 fetchJoin을 여러번 사용했는데 MultipleBagFetchException이 발생하게 됩니다.

  • 즉, 2개 이상의 OneToMany 자식 테이블에 Fetch Join을 선언했을때 발생
  • OneToOne, ManyToOne과 같이 단일 관계의 자식 테이블에는 Fetch Join을 써도 됩니다
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Record extends BaseEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "record_seq")
    private Long seq;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_seq", foreignKey = @ForeignKey(name="record_member_fk"))
    private Member member;

    @OneToMany(mappedBy = "record", cascade = CascadeType.ALL, orphanRemoval = true)
    private final List<File> fileList = new ArrayList<>();

    @OneToMany(mappedBy = "record", cascade = CascadeType.ALL, orphanRemoval = true)
    private final List<RecordHashTag> recordHashTagList = new ArrayList<>();
    ...
  }

이러한 상황이 있을 떄 record 에서 file, hashtag들을 가져오려고 할 때 fetchJoin을 쓰지 않게 되면 N + 1이 발생하기 떄문에 fetchJoin 사용

@Override
    public List<Record> retrieveRecord(Long memberId, String code, String search, LocalDate date, List<IsPrivate> isPrivateList) {
        BooleanBuilder booleanBuilder = searchBuilder(code, search);
        return jpaQueryFactory.selectFrom(record).distinct()
                .leftJoin(record.fileList, file).fetchJoin()
                .leftJoin(record.recordHashTagList, recordHashTag).fetchJoin()
                .where(
                        record.member.seq.eq(memberId).and(booleanBuilder),
                        eqDate(date),
                        eqIsPrivateList(isPrivateList)
                )
                .orderBy(record.seq.desc())
                .fetch();
    }

이러한 문제 발생

해결책

  • default_batch_fetch_size

일단 N + 1이 발생하는 이유

  • record 가 하나가 있을 때 그 record에 해당하는 file을 찾으려고 합니다.
  • 만약 record가 10 개라면? select * from file where record_id = ? 를 10번 합니다.
  • 즉, 부모 엔티티의 Key 하나하나를 자식 엔티티 조회로 사용하기 때문인데요

만약 1개씩 사용되는 조건문을 in 절로 묶는다면?

  • select * from file where record_id in (1, 2, 3…)
  • select * from hashTag where record_id in (1, 2, 3…)

이렇게 묶는 다면 조회수를 엄청 줄일 수 있습니다.

⇒ 이 옵션이 hibernate.default_batch_fetch_size

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 1000
  • n절 파라미터로 1,000 개 이상을 주었을때 너무 많은 in절 파라미터로 인해 문제가 발생할수도 있기 때문에 옵션값을 1000이상 주지는 않는다고 합니다.

결과

결론

  • N+1 문제를 최대한 in 쿼리로 기본적인 성능을 보장하게 한다.
  • OneToOne, ManyToOne 같은 곳에는 fetchJoin을 적용한다.
  • OneToMany 같은 관계에서는 데이터가 많은 쪽에 fetchJoin을 사용한다.
    • fetchJoin이 없는 자식 엔티티에 관해서는 in 쿼리로 조회하게 된다.