ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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
Designed by Tistory.