JPA - 페이징 컬렉션, N+1 문제, 브릿지 뻥튀기 문제 해결 정리(+Batch Size)

devdo·2023년 7월 8일
0

JPA

목록 보기
6/9

JPA Pagination, 그리고 N + 1 문제

뻥튀기 되는 문제 + distinct 해결법 정리

결국엔 Batch Size로 귀결!


1. 서브 쿼리 + groupBy

    @Override
    public Page<Board> findBoardPage(BoardRequestDto pageRequestDto) {
        Pageable pageable = pageRequestDto.getPageable();
        OrderSpecifier[] orderSpecifiers = createOrderSpecifier(pageRequestDto.getOrderCondition());

        List<Board> boardList = queryFactory
                .selectFrom(board)
                .leftJoin(board.categoryBridges, categoryBridge)
                .where(getSearch(pageRequestDto)
//                        , board.id.in(
//                                JPAExpressions
//                                        .select(categoryBridge.board.id)
//                                        .from(categoryBridge)
//                                        .where(categoryBridge.category.id.in(pageRequestDto.getCategoryIdList()))
//                        )
                )
                .groupBy(board.id)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(orderSpecifiers)
                .fetch();

        JPAQuery<Long> countQuery = queryFactory
                .select(board.count())
                .from(board)
                .leftJoin(board.categoryBridges, categoryBridge)
                .where(getSearch(pageRequestDto)
//                        , board.id.in(
//                                JPAExpressions
//                                        .select(categoryBridge.board.id)
//                                        .from(categoryBridge)
//                                        .where(categoryBridge.category.id.in(pageRequestDto.getCategoryIdList()))
//                        )
                )
//                .groupBy(board.id)
                ;

        return PageableExecutionUtils.getPage(boardList, pageable, countQuery::fetchOne);
    }

2. 브릿지 -> 주테이블 IdList만 groupBy로 가져오고 + 인쿼리로 가져오기

    @Override
    public Page<Board> findBoardPage(BoardRequestDto pageRequestDto) {
        Pageable pageable = pageRequestDto.getPageable();
        OrderSpecifier[] orderSpecifiers = createOrderSpecifier(pageRequestDto.getOrderCondition());


        List<Long> boardIdList = queryFactory
                .select(board.id)
                .from(board)
                .leftJoin(board.tagBridges, tagBridge)
                .where(eqTagIdList(pageRequestDto))
                .groupBy(board.id)
                .fetch();

        log.info("boardIdList: {}", boardIdList);

        List<Board> boardList = queryFactory
                .selectFrom(board)
                .where(
                        board.id.in(boardIdList),
                        getSearch(pageRequestDto)
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(orderSpecifiers)
                .fetch();

        JPAQuery<Long> countQuery = queryFactory
                .select(board.count())
                .from(board)
                .where(
                        board.id.in(boardIdList),
                        getSearch(pageRequestDto)
                )
                ;

        return PageableExecutionUtils.getPage(boardList, pageable, countQuery::fetchOne);
    }

-> 안됨!

in 쿼리로 들어올 idList 들이 너무 많아지면 문제 생김!

다 안되는 얘기!

Batch_size

이걸로 해결!

  • 컬렉션은 지연 로딩으로 조회한다.
  • 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size ,
    @BatchSize를 적용한다.
  • hibernate.default_batch_fetch_size: 글로벌 설정
  • @BatchSize: 개별 최적화 => 이 방식을 씀
  • 이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다.

application.yml

  jpa:
    open-in-view: true
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        default_batch_fetch_size: 100  # 위치 꼭 이곳!
      dialect: org.hibernate.dialect.MySQL8Dialect

Entity

    @BatchSize(size = 100)
    @OneToMany(mappedBy = "board")
    private List<TagBridge> tagBridges = new ArrayList<>();

해결된 사진

주테이블: board
컬렉션: tagBridges

주테이블 board_id IN 쿼리로 담아서 조회한다. size는 100개만큼!


default_batch_fetch_size 의 크기는?

default_batch_fetch_size 의 크기는 적당한 사이즈를 골라야 하는데, 100~1000 사이를 선택하는 것을 권장한다.

이 전략을 SQL IN 절을 사용하는데, 데이터베이스에 따라 IN 절 파라미터를 1000으로 제한하기도 한다.

1000으로 잡으면 한번에 1000개를 DB에서 애플리케이션에 불러오므로 DB에 순간 부하가 증가할 수 있다. 하지만 애플리케이션은 100이든 1000이든 결국 전체 데이터를 로딩해야 하므로 메모리 사용량이 같다.


1000으로 설정하는 것이 성능상 가장 좋지만, 결국 DB든 애플리케이션이든
순간 부하를 어디까지 견딜 수 있는지로 결정하면 된다.

profile
배운 것을 기록합니다.

0개의 댓글