SpringDataJpa를 사용했을 때 아래와 같은 예외 메시지를 만났다.
에러 메시지
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
내가 실제로 만난 메시지 상세
org.springframework.dao.InvalidDataAccessApiUsageException:
org.hibernate.loader.MultipleBagFetchException:
cannot simultaneously fetch multiple bags:
[com.enjoyservice.domain.place.entity.Place.images, com.enjoyservice.domain.course.entity.Course.coursePlaceSequences]
하나의 entity
에서 OneToMany이거나 ManyToMany인 entity 두 개 이상을 fetch할 때
(fetch를 FetchType.EAGER로 했을 때도) 발생하는 문제였다.
Place라는 Entity에 CoursePlaceSequence entity와 PlaceImage entity가 모두 OneToMany 관계로 엮여있었는데
아래와 같이 모두 FetchType.LAZY로 엮여있는 엔터티들에 fetch join을 사용해서 발생했다.
(자세한 연관 관계는 아래에 해결 방법
부분에서 확인할 수 있다.)
@Query("select c " +
"from Course c " +
"left join fetch c.coursePlaceSequences cps " +
"inner join fetch cps.place p " +
"left join fetch p.images " +
"where c.region = :region")
List<Course> findAllByRegionFetchPlace(@Param("region") Region region, Pageable pageable);
문제를 해결하기 위해 위의 에러 메시지에 적혀있는 bags
에 대해 먼저 알아보았다.
A generalization of the notion of a set is that of a
multiset
orbag
, which is similar to a set but allows repeated ("equal") values (duplicates).
번역)
집합 개념의 일반화는다중 집합
또는가방
개념으로, 집합과 유사하지만 반복되는("동일") 값(중복)을 허용합니다.
Java
offers theSet interface
to support sets (with the HashSet class implementing it using a hash table), and the SortedSet sub-interface to support sorted sets (with the TreeSet class implementing it using a binary search tree).
번역)
Java
는 집합을 지원하는Set 인터페이스 HashSet
( 해시 테이블을 사용하여 이를 구현하는 클래스 포함)와 SortedSet정렬된 집합을 지원하는 하위 인터페이스( TreeSet이진 검색 트리를 사용하여 이를 구현하는 클래스 포함)를 제공합니다 .
즉, Bag
은 MultiSet
과 같은 의미이고 순서가 없고 중복은 허용되는 자료구조이다.
그런데 Java의 Collection에는 Bag이라는 자료구조가 없어서 Hibernate에서 List를 Bag 대신에 사용하고 있었다.
결론부터 말하자면 OneToMany 관계에서 양방향 매핑을 List로 구현한 부분을 Set으로 바꿔주면
된다.
(하나만 List로 남도록 해도되고 모두 Set으로 바꿔줘도 된다.)
아래는 위의 문제를 만난 프로젝트의 예시이다.
이 프로젝트는 Course와 Place가 M:N
으로 연관관계가 엮여있고 Place와 Place_img는 1:N
의 관계로 역겨있다.
이런 상황에서 아래와 모두 FetchType.LAZY로 엮여있는 엔터티들에 fetch join을 사용했는데 쿼리문의 마지막 fetch join인 left join fetch p.images
이 부분을 수정해 문제를 해결했다.
@Query("select c " +
"from Course c " +
"left join fetch c.coursePlaceSequences cps " +
"inner join fetch cps.place p " +
"left join fetch p.images " +
"where c.region = :region")
List<Course> findAllByRegionFetchPlace(@Param("region") Region region, Pageable pageable);
Place 엔터티에서 아래와 같이 List로 양방향 매핑된 곳을
@OneToMany(mappedBy = "place", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<PlaceImage> images = new ArrayList<>();
이렇게 Set
으로 바꿔주면 된다.
여기선 PlaceImage와 연관된 부분을 Set바꿔줬다.
@OneToMany(mappedBy = "place", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true)
private Set<PlaceImage> images = new HashSet<>();
문제 상황
부분에서 말했듯이 Place라는 Entity에 CoursePlaceSequence entity와 PlaceImage entity가 모두 OneToMany 관계로 엮여있는 상황을 아래에서 확인할 수 있다.package com.enjoyservice.domain.place.entity;
import com.enjoyservice.domain.common.BaseTimeEntity;
import com.enjoyservice.domain.courseplacesequence.entity.CoursePlaceSequence;
import com.enjoyservice.domain.place.entity.type.*;
import com.enjoyservice.domain.placeimage.entity.PlaceImage;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Place extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
@OneToMany(mappedBy = "place", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true)
// private List<PlaceImage> images = new ArrayList<>(); // 이 부분을
private Set<PlaceImage> images = new HashSet<>(); // 이렇게 수정하면 됨
@OneToMany(mappedBy = "place", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<CoursePlaceSequence> sequences = new ArrayList<>();
}
이렇게 하면 문제가 해결이 된다.
위에서 Bag은 순서가 없고 중복은 허용되는 자료구조라고 했다.
이 때 한 entity에서 OneToMany나 ManyToMany인 관계를 가진 다른 entity를 2개 이상 fetch 한다면 연관된 entity들이 중복과 순서 모두 보장되지 않는 상황에서 어떤 기준으로 Row를 매핑할 지 확실하게 정하지 못할 수 있다.
2개가 아니라 더 많은 수의 entity들이 연관된 상황이라면 문제가 더 클 것이다.
이런 문제를 미리 방지하기 위해 Set을 써서 중복이라도 제거하는 것이다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@OrderColumn(name = "address")
private List<Address> addresses = new ArrayList<>();
}