hwasowl.log

둘 이상의 컬렉션 조인 시 생길 수 있는 문제점 - MultipleBagFetchException 본문

기술면접

둘 이상의 컬렉션 조인 시 생길 수 있는 문제점 - MultipleBagFetchException

화 솔 2024. 12. 2. 15:42

전 글에서는 fetch join 시 생길 수 있는 페이지네이션 문제에 대해서 다뤄보았다.

짧게 상기해보면 fetch join은 배치 사이즈에서 얘기한대로 하나의 collection fetch join에 대해서 인메모리에서 모든 값을 가져오기 때문에 페이지네이션이 의도한대로 작동하지 않았다.

 

fetch join을 할 때 ToMany의 경우 한번에 fetch join을 가져오기 때문에 collection join이 2개 이상이 될 경우 너무 많은 값이 메모리에 들어와 exception이 추가로 걸린다. 그 exception이 MultipleBagFetchException인데, 아래 사진에서 볼 수 있듯이 2개 이상의 bags, 즉 collection join이 두개 이상일 때 exception이 발생한다.

 

MultipleBagFetchException

 

코드만 봐서는 이해가 바로 가지 않을 수 있으니 예를 들어서 바로 exception이 발생할 수 있는 Entity를 가져와보겠다.

 

@Entity
@Table(name = "users")
public class User {

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

    @Column(length = 10, nullable = false)
    private String name;
    
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Article> articles = new ArrayList<>();
    
    @OneToMany(mappedBy = "question", fetch = FetchType.LAZY)
    private List<Question> questions = new ArrayList<>();
 }

 

당연히 유저만 조회한다면 지연로딩으로 인해 아무런 문제가 발생하지 않겠지만, articles과 questions를 가져와야하는 상황이라면 N+1이 발생할 것이다. 그럼 지금까지 했던대로 fetch join을 해보자

 

@EntityGraph(attributePaths = {"articles", "questions"}, type = EntityGraphType.FETCH)
@Query("select distinct u from User u left join u.articles")
List<User> findAllEntityGraph2();
@Test
@DisplayName("collection join 2개일 때 fetch join")
void collectionFetchJoinTest() {
    System.out.println("== start ==");
    List<User> users = userRepository.findAllEntityGraph2();
    System.out.println("== find all ==");
}

 

이렇게 진행하면 Users는 ~ToMany가 두 개, 즉 컬렉션 fetch join이 두 개 이상 걸리기 때문에 바로 이야기했던 MultipleBagFetchException 이 발생한다.

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.example.jpa.domain.User.articles, com.example.jpa.domain.User.questions]; nested exception is java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.example.jpa.domain.User.articles, com.example.jpa.domain.User.questions]

 

~ToOne은 얼마만큼 fetch join을 해도 괜찮지만 ~ToMany는 하나일 때는 인메모리에서 처리하고 두 개 이상은 Exception으로 제한한다... 뭐 할 수 있는게 없어 보인다. 이걸 어떻게 해결할 수 있을까?

 

1. MultipleBagFetchException 해결방안 - 자료형을 Set으로 변경하기

자료형을 Set으로 변경을 하면 해결되는 아마도 MultipleBag가 List로 되어있을 때 중복 자체를 허용하지 않는다면 복잡한 여러개의 collection fetch 관계를 해결할 수 있음이 아닐까 생각한다.

 

@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private Set<Article> articles = emptySet();

@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private Set<Question> questions = emptySet(); 

 

Set 변경 후 결과

 

MultipleBagFetchException이라는 둘이상의 collection fetch join을 막는 exception없이 정상적으로 모든 데이터를 가져옴을 알 수 있다. Set을 사용하게 된다면 HashSet으로는 순서가 중요한 데이터에 순서를 보장할 수 없기에 LinkedHashSet을 사용해야 한다.

 

다만.. Set을 사용한다고 해서 페이지네이션은 마찬가지로 해결이 불가능하다.

페이지네이션은 근본적으로 몇개의 컬렉션 조인이 있던 간에 인메모리에서 가져오기 때문에 OOM을 발생시킬 수 있는 원인이 되어 해당 방법으로는 해결이 불가능하다.

 

설령 컬렉션 조인이 한 개인 상황에서 Set자료구조를 사용한다고 해도 인메모리에서 가져와서 똑같아진다.

 

 

2. MultipleBagFetchException 해결방안 - 다시 BatchSize

앞서 페이지네이션의 해결책 중 하나로 나온 배치 사이즈이다.
(안보고 오셨다면 전 글을 보고 오시는걸 추천합니다.)

 

물론 이 방법이 페이지네이션의 해결책 중 하나로 나온 방법이긴 하지만 컬렉션 조인이 두 개 이상일 때 MultipleBagFetchException을 해결할 수 있는 방법이기도 하다.

 

리스트 자료구조를 사용해야 하는 상황이거나, Set을 사용한다고 해도 페이지네이션에서 인메모리 로딩을 막을 수 없기 때문에 2개 이상의 컬렉션 조인을 사용하는데 페이지네이션을 사용해야할 경우도 Batch Size를 통해 인메모리를 사용하지 않고 사용할 수 있다.

 

음 정리하자면 이렇게 간추릴 수 있을 것 같다.

- List 자료구조를 사용해야 될 경우

- 2개 이상의 컬렉션 조인을 사용하는데 페이지네이션을 사용해야 하는 경우 (OOM 방지)

 

[참고] Set, 혹은 List위에 @BatchSize를 걸게 되면 동일하게 인메모리에 가져오는 것이 아닌 호출하는 당시에 한번에 모든 데이터를 가져오는 동작구조를 가진다.

 

@BatchSize(size = 100)
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private Set<Article> articles = emptySet();

@BatchSize(size = 100)
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private Set<Question> questions = emptySet();
@BatchSize(size = 100)
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Article> articles = new ArrayList<>();

@BatchSize(size = 100)
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Question> questions = new ArrayList<>();
@Query("select distinct u from User u left join u.articles left join u.questions")
Page<User> findAllPage2(Pageable pageable);

 

 

주의해야할 점은 batch size에 fetch join을 걸면 안된다.

fetch join이 우선시되어 적용되기 때문에 batch size가 무시되고 fetch join을 인메모리에서 먼저 진행하여 List가 MultipleBagFetchException가 발생하거나, Set을 사용한 경우에는 Pagination의 인메모리 로딩을 진행하기 때문

 

결과

일단 fetch join이 아닌 지연 로딩으로 유저만 가져온 모습이며 필요한 아티클 값을 출력하고자 할 때 아티클만 따로 배치 쿼리를 날려 받음을 볼 수 있다.

 

정리

지연로딩은 기본으로 깔고 들어가되, 페이지네이션 상황이 아니라면 Set 자료구조를 채택해서 MultipleBagFetchException을 예방하고 페이지네이션을 사용해야 한다면 BatchSize를 사용할 것 같다.

 

'기술면접' 카테고리의 다른 글

fetch join 사용 시 생길 수 있는 문제점 - Pagination  (2) 2024.12.01
JPA N+1  (0) 2024.11.30
트랜잭션 격리수준  (0) 2024.11.26