❗QueryDSL에서 N+1 문제 BatchSize 로 해결하는 법
List<Todo> findTodoList = queryFactory
.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.leftJoin(todo.managers, managers).fetchJoin()
.leftJoin(todo.comments, comments).fetchJoin()
.where(where)
.fetch();
Todo 엔티티를 기준으로 User, Manager, Comment를 조인하여 페이지 단위로 조회하고자 했다.user는 @ManyToOne 구조,managers, comments는 @OneToMany List 구조이다.List 연관 컬렉션에 대해 동시에 fetchJoin()을 시도하면 다음과 같은 예외가 발생한다:Caused by: java.lang.IllegalArgumentException:
org.hibernate.loader.MultipleBagFetchException:
cannot simultaneously fetch multiple bags:
[org.example.Todo.comments, org.example.Todo.managers]List 타입 컬렉션을 fetch join으로 동시에 로딩하게 되면 데이터 중복 및 부정확성 문제를 막기 위해 제한하기 때문이다.fetchJoin()은 user만 유지.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin() // 단건 관계만 fetchJoin
.leftJoin(todo.managers, managers) // fetchJoin 제거
.leftJoin(todo.comments, comments) // fetchJoin 제거
.where(where)
.fetch()
@ManyToOne 관계인 user만 fetchJoin() 사용managers, comments)은 fetchJoin 제거하여 중복 및 예외 방지@BatchSize로 N+1 문제 해결@OneToMany(mappedBy = "todo", cascade = CascadeType.REMOVE)
@BatchSize(size = 100)
private List<Comment> comments = new ArrayList<>();
@OneToMany(mappedBy = "todo", cascade = CascadeType.ALL)
@BatchSize(size = 100)
private List<Manager> managers = new ArrayList<>();
//쿼리문에서는 join 제거
.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(where)
.fetch();
comments와 managers에 @BatchSize(size = 100) 추가todo.getComments().size() 호출 시,todo.id in (...) 쿼리로 일괄 로딩(batch loading)MultipleBagFetchException 완전 제거User는 한 번의 fetch join 쿼리로 즉시 로딩comments, managers는 2회 batch 쿼리로 N+1 없이 로딩@BatchSize는 Lazy 로딩 시, 연관 엔티티를 한 번에 in-query로 일괄 조회하도록 돕는다.fetch join 없이도 효율적인 데이터 접근이 가능하다.fetch join 대신 @BatchSize를 각 필드에 적용하면 안정적이다.@BatchSize(size = 100)
private List<Manager> managers = new ArrayList<>();
@ManyToOne fetchJoin + @OneToMany @BatchSize 조합이 가장 안전하고 성능 좋은 패턴