프로젝트에서 places
테이블은 place_images
와 place_blogs
라는 두 개의 테이블과 연관 관계를 맺고 있다. JPA에서는 Place
엔티티와 PlaceImage
, PlaceBlog
엔티티 간에 양방향 연관 관계(@OneToMany
와 @ManyToOne
)를 설정해 두었다. 사용자가 특정 장소의 상세 정보를 조회할 때, 장소의 기본 정보(예: 이름, 주소)뿐만 아니라 여러 개의 이미지(place_images
)와 블로그 후기(place_blogs
)를 함께 보여줘야 했다.
이를 위해 JPA를 사용해 Place와 두 컬렉션(place_images
, place_blogs
)을 조인하려 했다. 처음에는 FETCH JOIN
을 활용해 한 번의 쿼리로 데이터를 가져오려고 했지만, MultipleBagFetchException
이라는 예외가 발생했다 확인해보니, 이 예외는 2개 이상의 컬렉션을 FETCH JOIN
으로 조회하려 할 때 발생하는 문제라는 걸 알게 되었다. 그래서 왜 이런 제약이 있는지부터 찾아 보았다.
컬렉션을 FETCH JOIN
으로 조회하면 결과 데이터에 카테시안 곱이 발생한다.
예를 들어, 한 장소(Place
)에 5개의 이미지(place_images
)와 3개의 블로그 후기(place_blogs
)가 있다고 가정하면:
Place
1개당 5 × 3 = 15
개의 행이 생성된다.3 × 5 × 3 = 45
개의 행이 결과로 나온다.JPA는 이 데이터를 엔티티로 매핑해야 하는데, 중복된 행을 어떻게 처리할지, 객체 그래프를 어떻게 구성할지 판단하기 어렵다. 이로 인해 데이터가 기하급수적으로 늘어나면서 성능 저하와 메모리 낭비가 발생할 수 있다.
JPA는 쿼리 결과를 엔티티 객체로 매핑할 때 객체의 상태를 일관성 있게 유지하려 한다. 하지만 두 개 이상의 컬렉션을 FETCH JOIN
하면 결과 행마다 컬렉션 데이터가 얽히면서 혼란이 생긴다.
예를 들어, 위의 45개 행을 보고 JPA가 "이건 하나의 Place
에 대한 데이터다"라고 판단하려면:
place_images
와 place_blogs
를 각각 어떤 리스트에 나눠 담을지 결정해야 하는데, 그 로직이 애매해진다.Hibernate는 이런 혼란을 방지하기 위해 애초에 두 개 이상의 컬렉션에 대한 FETCH JOIN
을 금지하고 있다.
JPA 스펙은 FETCH JOIN
을 연관된 엔티티나 컬렉션을 한 번에 로드하는 용도로 정의하였다. 하지만 **"복잡한 다중 컬렉션 조회"**는 의도하지 않았다. Hibernate 같은 구현체는 이를 명확히 제한하기 위해 MultipleBagFetchException을 발생시킨다.
여기서 "Bag"은 중복을 허용하고 순서가 보장되지 않는 컬렉션(MultiSet)을 뜻한다. 자바에는 Bag이라는 컬렉션이 따로 없지만, Hibernate는 List
를 Bag으로 간주한다. 특히 @OrderBy
로 순서를 정의한 컬렉션이라도, 다중 Bag을 동시에 조회하면 순서가 꼬일 수 있어서 문제가 된다.
Place
), 이미지(place_images
), 블로그(place_blogs
)를 각각 별도의 쿼리로 조회한 뒤, 서비스 계층에서 데이터를 합쳐 DTO로 변환한다.