-
Resolved [org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:이커머스 devops/국비교육 2025. 11. 22. 13:21

상황 :
회원ID로 단건의 회원을 조회해야 하며 회원ID에 맵핑된 주문 건, 주문상세내용을 조회해야 한다
회원 entity 내부에 orders 엔티티가 1:N 관계로 있고
orders 엔티티 안에 orderProducts 엔티티가 1:N 관계로 있다
방법 1. user - orders - orderProducts fetchJoin
@Repository @RequiredArgsConstructor public class UserQueryRepository { private final JPAQueryFactory queryFactory; public User getUserById(Long id) { return queryFactory .selectDistinct(user) .from(user) .leftJoin(user.orders, order).fetchJoin() .leftJoin(order.orderProducts, orderProduct).fetchJoin() .where(user.id.eq(id)) .fetchOne(); } }ERROR : Resolved [org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:
@OneToMany List <Order> orders (User → Order)
@OneToMany List <OrderProduct> orderProducts (Order → OrderProduct)
둘 다 List = bag으로 매핑되어 있고, 지금 쿼리에서 둘 다 fetch join으로 한 번에 끌고 오고 있어서 터졌다
- User 1명이 여러 Order
- 각 Order 가 여러 OrderProduct
이걸 한 번에 fetch join 하면,
DB 쿼리는 User × Order × OrderProduct 곱으로 늘어나고
Hibernate 가 List(순서 없는 bag) 두 개를 중첩해서 정리할 수가 없음방법 2. User+Orders만 fetch join, orderProducts는 LAZY + batch fetch
@Repository @RequiredArgsConstructor public class UserQueryRepository { private final JPAQueryFactory queryFactory; public User getUserById(Long id) { return queryFactory .selectDistinct(user) .from(user) .leftJoin(user.orders, order).fetchJoin() .where(user.id.eq(id)) .fetchOne(); } }# application.yml spring: jpa: properties: hibernate.default_batch_fetch_size: 20// Orders Entity @Builder.Default @BatchSize(size = 20) @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) private List<OrderProduct> orderProducts = new ArrayList<>();// UserService // 회원 ID 단건 조회 public UserResponse findById(Long id) { User findUser1 = userQueryRepository.getUserById(id); findUser1.getOrders().forEach(order -> { order.getOrderProducts().size(); // 강제 초기화 }); return userMapper.toResponse(findUser1); }일단 User, oreders만 먼저 fetch join 쿼리 한 번으로 결과를 가져왔다
그리고 orderProducts가 지연로딩이니까 service에서 강제 초기화를 통해 값을 가져왔다
Hibernate: select distinct u1_0.id,u1_0.address,u1_0.created_at,u1_0.email,o1_0.user_id,o1_0.id,o1_0.order_date,o1_0.status,u1_0.password_hash,u1_0.updated_at,u1_0.username from users u1_0 left join orders o1_0 on u1_0.id=o1_0.user_id where u1_0.id=?
Hibernate: select op1_0.order_id,op1_0.id,op1_0.count,op1_0.order_price,op1_0.product_id from order_product op1_0 where op1_0.order_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)쿼리를 보면 총 2번 실행되면서 값을 제대로 가져오는 것을 알 수 있다

그럼에도 불구하고 orders + orderProducts의 값이 null로 나왔다
그렇다면 문제는 mapping이 안된다는 말...
@Mapper(componentModel = "spring") public interface UserMapper { User toEntity(CreateUserRequest req); UserResponse toResponse(User user); List<UserResponse> toResponseList(List<User> users); @Mapping(target="orderDate", source="orderDate") @Mapping(target = "status", source = "status") UserOrderResponse toResponseUserOrder(Order order); @Mapping(target="orderDate", source="orderDate") @Mapping(target = "status", source = "status") List<UserOrderResponse> toResponseUserOrders(List<Order> order); @Mapping(target = "productId", source = "id") @Mapping(target = "productName", source = "product.name") @Mapping(target = "quantity", source = "count") @Mapping(target = "orderPrice", source = "orderPrice") OrderProductResponse toResponseOrderProduct(OrderProduct orderProduct); @Mapping(target = "productId", source = "id") @Mapping(target = "productName", source = "product.name") @Mapping(target = "quantity", source = "count") @Mapping(target = "orderPrice", source = "orderPrice") List<OrderProductResponse> toResponseOrderProducts(List<OrderProduct> orderProducts); }나름 꼼꼼하게 맵핑을 해놨었는데 유일하게 toResponse에는 @Mapping이 없었다
@Mapper(componentModel = "spring") public interface UserMapper { User toEntity(CreateUserRequest req); @Mapping(target = "userOrders", source = "orders") UserResponse toResponse(User user); ... }// 회원 ID 단건 조회 public UserResponse findById(Long id) { User findUser1 = userQueryRepository.getUserById(id); return userMapper.toResponse(findUser1); }맵핑을 추가해 주고 서비스단에 지연로딩 강제 초기화 코드를 삭제했다

Hibernate:
select distinct u1_0.id,u1_0.address,u1_0.created_at,u1_0.email,o1_0.user_id,o1_0.id,o1_0.order_date,o1_0.status,u1_0.password_hash,u1_0.updated_at,u1_0.username from users u1_0 left join orders o1_0 on u1_0.id=o1_0.user_id where u1_0.id=?
-- 1,Seoul,2025-11-20 13:59:20,jy@test.com,1,1,2025-11-01 10:00:00,COMPLETED,1234,2025-11-20 13:59:20,jiyeon
-- 1,Seoul,2025-11-20 13:59:20,jy@test.com,1,2,2025-11-10 12:30:00,PENDING,1234,2025-11-20 13:59:20,jiyeon
Hibernate: select op1_0.order_id,op1_0.id,op1_0.count,op1_0.order_price,op1_0.product_id from order_product op1_0 where op1_0.order_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
-- 1,1,2,12900.00,100
-- 1,2,1,34900.00,108
-- 2,3,1,39900.00,104
-- 2,4,1,15900.00,204
Hibernate: select p1_0.id,p1_0.created_at,p1_0.description,p1_0.name,p1_0.price,p1_0.stock,p1_0.stock_status,p1_0.updated_at from product p1_0 where p1_0.id=?
Hibernate: select p1_0.id,p1_0.created_at,p1_0.description,p1_0.name,p1_0.price,p1_0.stock,p1_0.stock_status,p1_0.updated_at from product p1_0 where p1_0.id=?
Hibernate: select p1_0.id,p1_0.created_at,p1_0.description,p1_0.name,p1_0.price,p1_0.stock,p1_0.stock_status,p1_0.updated_at from product p1_0 where p1_0.id=?
Hibernate: select p1_0.id,p1_0.created_at,p1_0.description,p1_0.name,p1_0.price,p1_0.stock,p1_0.stock_status,p1_0.updated_at from product p1_0 where p1_0.id=?실제로 내가 원하는 응답을 받았고 쿼리도 순서대로 나갔다
문제는 총 4번의 쿼리 실행을 기대했는데 6번의 쿼리가 실행되었다
1번 쿼리에서 userId를 통해 2건의 결과를 얻었고 order_id 또한 1, 2 총 두 건의 결과가 나왔다
2번 쿼리에서 in 절에서 1번 쿼리의 결과 2건으로 실행했고 결과가 예상치 못한 4건이 나왔다> 잘못 생각했던 부분인데 order_id를 통해 주문 2건, 주문 상품 4건의 결과가 나오는게 맞다
따라서 3번 쿼리에서 총 4건의 select 절이 실행되면서 6번의 쿼리가 실행되었다
어딘가에 batchSize가 제대로 실행되지 않아 보였다
3. product - orderProduct 연결 + batchSize()
(지금)
User 1 ── N Order 1 ── N OrderProduct ??? Product
(회원) (주문) (주문상품) (상품)
(수정 후)
User 1 ── N Order 1 ── N OrderProduct N ── 1 Product
(회원) (주문) (주문상품) (상품)보니까 OrderProduct와 Product가 연결되지 않았다
주문과 상품이 N:N 관계에서 중간 테이블로 1:N 관계로 되는 게 맞았다
public class Product { ... @BatchSize(size = 20) @OneToMany(mappedBy = "product") private List<OrderProduct> orderProducts; ... }product 엔티티에 해당 부분을 수정해 줬다

Hibernate:
select distinct u1_0.id,u1_0.address,u1_0.created_at,u1_0.email,o1_0.user_id,o1_0.id,o1_0.order_date,o1_0.status,u1_0.password_hash,u1_0.updated_at,u1_0.username from users u1_0 left join orders o1_0 on u1_0.id=o1_0.user_id where u1_0.id=?
Hibernate: select op1_0.order_id,op1_0.id,op1_0.count,op1_0.order_price,op1_0.product_id from order_product op1_0 where op1_0.order_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Hibernate: select p1_0.id,p1_0.created_at,p1_0.description,p1_0.name,p1_0.price,p1_0.stock,p1_0.stock_status,p1_0.updated_at from product p1_0 where p1_0.id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)batchSize 적용이 잘 되었고 원하는 대로 쿼리 3번 실행되었으며 응답도 제대로 나왔다
cf. 애초에 DTO 프로젝션으로 별도 쿼리
dto 프로젝션을 이용하는 방법도 있었지만 귀찮아서 pass...
728x90'이커머스 devops > 국비교육' 카테고리의 다른 글
Redis - 카테고리 조회 적용 (1) 2025.12.06 P6Spy 적용하기 (0) 2025.11.22 실무 활용 - 스프링 데이터 JPA와 Querydsl (0) 2025.11.19 동적 쿼리와 성능 최적화 조회 (0) 2025.11.19 QueryDSL 중급 문법 (0) 2025.11.19