[개발일지] 취미 커뮤니티 Trouble Shooting - MultipleBagFetchException

zwon·2023년 10월 17일
0

개발일지

목록 보기
17/23

해시 태그 구현 중에 다음과 같이 에러가 발생했다.

Invocation of init method failed; nested exception isjavax.persistence.PersistenceException:[PersistenceUnit: default]
Unable to build Hibernate SessionFactory;nested exception is org.hibernate.loader.MultipleBagFetchException:
cannot simultaneously fetch multiple bags: [com.jiwon.blog.domain.board.Board.tags, com.jiwon.blog.domain.board.Board.replies]

Board

@Entity @Getter
@NoArgsConstructor @AllArgsConstructor
@Builder
public class Board extends BaseTimeEntity {

  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;
  @Lob
  private String content;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "user_id")
  private User user;

  @OneToMany(mappedBy = "board", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE) // 글이 삭제되면 댓글 모두 삭제
  @OrderBy("createdDate")
  private List<Reply> replies = new ArrayList<>();

  @OneToMany(mappedBy = "board", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
  private List<Tag> tags = new ArrayList<>();

  public void setUser(User user){ 
    this.user = user;
  }

  public void update(BoardUpdateRequestDto request) {
    this.title = request.getTitle();
    this.content = request.getContent();
  }
}

Hibernate MultipleBagFetchException

찾아보니 OneToMany, ManyToMany인 Bag 두 개 이상을 EAGER로 fetch할 때 발생하는 에러라고 한다.
위 코드에선 OneToMany인 replies와 tags를 EAGER로 fetch해서 발생한 것이었다.

Bag

Bag 2개 이상.. Bag란 무엇인가
Bag(Multiset)은 Set과 같이 순서가 없고, List와 같이 중복을 허용하는 자료구조라고 한다.
하지만 자바 컬렉션 프레임워크에서는 Bag가 없기 때문에 하이버네이트에서는 List를 Bag으로써 사용하고 있는 것이다고 한다.

해시 태그는 보통 중복이 발생하지 않으니 List로 된 형식을 Set으로 바꿔서 해결할 수 있는데 프로젝트에선 tagify 라이브러리를 사용해서 해시 태그를 구현했다.
사용해보니 화면에서 값이 넘어올 때 중복 해시 태그가 제거된 상태로 넘어와서 해시 태그도 Set타입이 아닌 List타입으로 사용했었다.

그렇다면 왜 OneToMany, ManyToMany인 Bag 두 개 이상을 EAGER로 fetch할 때 발생하는것인가?

이유

해시 태그를 set자료형으로 바꾸고 실행해보면 MultipleBagFetchException이 발생하지 않는다.

  • 하이버네이트에서는 list를 Bag타입으로 취급한다고 했다.
  • Bag는 순서도 없고 중복을 허용하는 자료구조이다.
  • OneToMany, ManyToMany인 Bag(list) 두 개 이상을 가져오면 발생한다고 했다.
  • 순서도 보장해주지 않고 중복도 허용하는 자료구조에서 어떤 기준으로 join을 할 지에 대한 기준도 명확하지 않기 때문에 발생하는 에러인거 같다.

해결

그래서 해결법에 대한 것을 찾아보다가 다음과 같이 정리해주신 분의 블로그를 읽으면서 해결했다. MultipleBagFetchException 발생시 해결 방법

  1. fetch 타입을 LAZY로 변경
  2. @Entitygraph 사용
  3. bachsize 지정
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Board extends BaseTimeEntity {

  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;
  @Lob
  private String content;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name = "user_id")
  private User user;

  @OneToMany(mappedBy = "board", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) // 글이 삭제되면 댓글 모두 삭제
  @OrderBy("createdDate")
  private List<Reply> replies = new ArrayList<>();

  @OneToMany(mappedBy = "board", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
  private Set<Tag> tags = new HashSet<>();

  // 연관관계 편의 메서드 ...(?) 맞나? 잘 모르겠다..
  public void addTags(Tag tag){
    this.tags.add(tag);
  }

  public void setUser(User user){ // 연관관계 편의 메서드
    this.user = user;
  }

  public void update(BoardUpdateRequestDto request) {
    this.title = request.getTitle();
    this.content = request.getContent();
  }
}
@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {

  @Override
  @EntityGraph(attributePaths = {"replies","tags"}) //{}안에 있는 것들을 fetch join해서 가져오겠다.
  List<Board> findAll();

  Page<Board> findAll(Pageable pageable);
}
...
  jpa:
    hibernate:
      ddl-auto: create # ???? ??? drop? ???
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        default_batch_fetch_size: 1000 # multibagfetchException 해결
...

reference
(Troubleshooting) Hibernate MultipleBagFetchException 정복하기

MultipleBagFetchException 발생시 해결 방법

profile
Backend 관련 지식을 정리하는 Back과사전

0개의 댓글