트러블 슈팅 - QueryDSL, BatchSize 사용

Zyoon·2025년 6월 25일

트러블슈팅

목록 보기
9/11
post-thumbnail

❗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]
  • 이는 Hibernate가 두 개 이상의 List 타입 컬렉션을 fetch join으로 동시에 로딩하게 되면 데이터 중복 및 부정확성 문제를 막기 위해 제한하기 때문이다.

해결 방법

1. fetchJoin()user만 유지

.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin() // 단건 관계만 fetchJoin
.leftJoin(todo.managers, managers)     // fetchJoin 제거
.leftJoin(todo.comments, comments)     // fetchJoin 제거
.where(where)
.fetch()
  • @ManyToOne 관계인 userfetchJoin() 사용
  • 나머지 컬렉션(managers, comments)은 fetchJoin 제거하여 중복 및 예외 방지

2. @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();
  • commentsmanagers@BatchSize(size = 100) 추가
  • QueryDSL 조회 후 todo.getComments().size() 호출 시,
  • Hibernate가 자동으로 todo.id in (...) 쿼리로 일괄 로딩(batch loading)

결과

  • MultipleBagFetchException 완전 제거
  • User는 한 번의 fetch join 쿼리로 즉시 로딩
  • comments, managers2회 batch 쿼리로 N+1 없이 로딩
  • 총 3회 이하의 쿼리로 성능과 안정성 모두 확보

BatchSize란?

  • @BatchSize는 Lazy 로딩 시, 연관 엔티티를 한 번에 in-query로 일괄 조회하도록 돕는다.
  • N+1 문제를 해결하면서도 fetch join 없이도 효율적인 데이터 접근이 가능하다.
  • 컬렉션이 여러 개인 경우 fetch join 대신 @BatchSize를 각 필드에 적용하면 안정적이다.
  • 페이지 크기보다 약간 크게 설정하면 성능과 네트워크 효율을 균형 있게 맞출 수 있다.
  • 일반적으로 size 값은 10~100 사이. → 너무 많으면 오히려 성능 저하
@BatchSize(size = 100)
private List<Manager> managers = new ArrayList<>();

결론

  • List 두 개 이상은 fetchJoin 금지, batch 로딩으로 대체
  • 실전에서는 @ManyToOne fetchJoin + @OneToMany @BatchSize 조합이 가장 안전하고 성능 좋은 패턴
profile
기어 올라가는 백엔드 개발

0개의 댓글