최근 프로젝트를 진행하다가 오류가 발생하여 찾아보니 카테시안 곱 때문에 생긴 에러였다.
카테시안 곱에 대해서 알아보고, 오류를 어떻게 해결했는지 작성해보려 한다.
카테시안 곱이란?
카테시안 곱이란 쉽게 말해 두 개의 테이블의 모든 행이 서로 짝지어지는 것을 말한다. JPA에서는 데이터베이스 쿼리 사용 시 테이블간의 조인이 잘못되어 발생하며, 예상보다 훨씬 많은 수의 결과 레코드를 생성한다.
문제 파악
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:
[com.example.adhd_backend.entity.PlanItem.planItemContents, com.example.adhd_backend.entity.
Plan.planItems]
프로젝트를 하며 마주한 문제가 이거였다.. MultipleBagFetchException, 카테시안 곱 문제가 발생할 수 있다고 판단되어 Hibernate 가 막는 예외이다.
@OneToMany로 매핑된 컬럼들을 join fetch로 동시에 가져오려 해서 발생하였다.
@EntityGraph(attributePaths = {"planItems", "planItems.planItemContents"})
@Query("SELECT p FROM Plan p WHERE p.id = :id")
Optional<Plan> findByIdWithItems(@Param("id") Long id);
repository에 이런 메서드가 있었다. 이 메서드가 의도하는 바는 Plan을 조회할 때 planItems도 같이 fetch join 하고, planItems 안에 있는 planItemContents도 같이 한 번에 join fetch 하는 것이다.
SELECT p.*, pi.*, pic.*
FROM plan p
LEFT JOIN plan_item pi ON p.id = pi.plan_id
LEFT JOIN plan_item_content pic ON pi.id = pic.plan_item_id
WHERE p.id = ?
내부적으로는 이런 형식의 sql 쿼리 문이 생성된다.
이게 왜 MultipleBagFetchException을 발생시킬까?
plan, plan_item, plan_item_content가 각각 1:N 관계라면, 결과적으로 모든 경우의 수를 생각하기 때문에 N:M 형태의 카테시안 곱이 형성된다. 따라서 Hibernate는 이걸 감지하고 MultipleBagFetchException로 막아버리는 것이다.
카테시안 곱이 발생하는 또 다른 이유에서는 다중 조인 시 별칭 혼동, 동적 쿼리에서 조건 문자열이 빠지는 경우 등이 있다.
해결 과정 Link -> Set
우선 첫번째 대안으로 중복을 사전에 걸러서 내보내주는 Set을 사용하였다.
Set 은 중복을 허용하는 List와는 달리 같은 Entity가 두 번 들어올 경우 하나로 합쳐진다. 따라서 Collection에는 중복이 남지 않는다.
@OneToMany(mappedBy = "plan", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("date ASC")
@Builder.Default
private Set<PlanItem> planItems = new LinkedHashSet<>();
하지만 이 마저도 모든 문제를 해결하진 못한다..
equals() 또는 hashcode()로 객체를 비교하기 때문에 카테시안 곱으로 인한 중복 결과를 어느정도 막을 수 있는 것은 사실이지만, Set은 순서를 보장하지 않는다. (LinkedHashSet으로 순서를 보장할 순 있지만 인덱스 접근이 어려움)
즉, 일부 중복 제거는 가능하나, 쿼리 자체가 여전히 Join으로 여러 행을 만들어내기 때문에 카테시안 곱의 부하가 남을 수 있다. (근본적으로 카테시안 곱을 해결하는 방안이 아님!)
다음 코드 리팩토링 시에는 Distinct + DTO Projection 조합으로 해결해보겠다.

감사합니다 ( ゚д゚)つ Bye
'Development > DB' 카테고리의 다른 글
| [MongoDB] Mongoose Transaction 사용하기 (0) | 2025.05.16 |
|---|---|
| [Express+MongoDB] 검색 기능 구현 (MongoDB Atlas Search Index) (0) | 2025.04.08 |