웹개발/Spring Boot

JPA N+1문제 @BatchSize로 해결하기

BEOTIZA♥ 2024. 1. 30. 20:15

N+1?

N+1은 쿼리문이 1회 실행되어야 하는데,  N개 의 쿼리를 추가로 조회해서 총 N+1만큼 추가적인 쿼리가 발생한다. 'N+1'로 실행되는 쿼리는 데이터베이스를 엄청나게 많이 사용(메모리 사용 多)하기 때문에 문제가 된다.

N+1 문제를 해결해보자.


 

1. 테스트 데이터 추가 

  public class BoardRepositoryTests {
  
   @Test
    public void testInsertAll(){
        for (int i = 1; i<= 100; i++){
            Board board = Board.builder() //board테이블 100개
                    .title("Title.."+i)
                    .content("Content.."+i)
                    .writer("writer.."+i)
                    .build();
                    
            for (int j =0; j <3; j++){
                if(i % 5 == 0){ // board_image테이블 5배수 
                    continue;
                }
                board.addImage(UUID.randomUUID().toString(),i+"file"+j+".jpg"); 
            }

            boardRepository.save(board);
        }
      }
    }

 

2. 목록 데이터를 처리하기 위해  searchWithAll메소드 추가

//BoardSearch 인터페이스

public interface BoardSearch {

Page<BoardListAllDTO> searchWithAll(String[] types,
                                        String keyword,
                                        Pageable pageable);
}

 

3. searchWithAll() 메소드 구현

searchWithAll() 내용은 Board와 Reply를 left join처리하고 쿼리를 실행해서 내용 확인

  • Queryds 사용
//BoardSearchImpl클래스 

  public class BoardSearchImpl extends QuerydslRepositorySupport implements BoardSearch {
  
  @Override
    public Page<BoardListAllDTO> searchWithAll(String[] types, String keyword, Pageable pageable) {

        QBoard board = QBoard.board;
        QReply reply = QReply.reply;

        JPQLQuery<Board> boardJPQLQuery = from(board);
        boardJPQLQuery.leftJoin(reply).on(reply.board.eq(board)); 

        getQuerydsl().applyPagination(pageable, boardJPQLQuery); //paging

        List<Board> boardList = boardJPQLQuery.fetch(); 

     	boardList.forEach(board1 -> {
        System.out.println(board1.getBno());
        System.out.println(board1.getImageSet());
        System.out.println("-------------------");
        });
        
        return null;
     }
    
    }

 

4. 테스트 확인

페이징 처리

  • PageRequest.of(0,10, : 페이지 0~10
  • Sort.by("bno").descending()); : Sort.by정렬,  descending 내림차순
//BoardRepositoryTests 

public class BoardRepositoryTests {

    @Transactional
    @Test
    public void testSearchImageReplyCount(){
        Pageable pageable = PageRequest.of(0,10,Sort.by("bno").descending()); //페이징 처리
        boardRepository.searchAll(null, null,pageable);
     }
    
 }

4-1. 테스트 결과

1) Board에 대한 페이징 처리 실행되면서 limit?로 처리

2) BoardSearchImpl클래스 System.out.println()을 통해 Board bno 출력

3) Board객체 imageSet 가져오기 위해 board_image테이블 조회하는 쿼리 실행

4) 2,3 과정 반복 실행

 

 

5. 'N+1' 문제 BATCH SIZE로 해결하기

@BatchSize에는 size라는 속성을 지정하는데 'N번'에 해당하는 쿼리를 모아서 한번에 실행할수 있다.

Size를 설정해두면 JPA에서 지연로딩을 할 때, 한번에 쿼리를 모아서Batch Size만큼의 엔티티를

where절에 'in'으로 가져온다. 

 

  • 엔티티 관계

@OneToMany는 기본적으로 지연 로딩(Lazy)이다.

일대다 관계 정의 : board는 여러 <BoardImage>를 가질 수 있음

//Board클래스에 @BatchSize 적용

public class Board extends BaseEntity{

    @OneToMany(mappedBy = "board",
    cascade = {CascadeType.ALL},
    fetch = FetchType.LAZY, //LAZY
    orphanRemoval = true) //BoardImage의 board변수
    @Builder.Default
    @BatchSize(size = 20)
    private Set<BoardImage> imageSet = new HashSet<>();
 }

 

@BatchSize 의 size속성값은 지정된 수만큼 BoardImage를 조회할 때 한번 에 in조건으로 사용

1) 목록 처리하는 쿼리 2) Board객체 bno출력 3) Board imageSet출력할 때 board_image 테이블 조회

 

6. 정리

데이터베이스를 많이 사용하기 때문에 작업시 N+1의 문제를 생각해두고
간단한 보완책으로 Batch Size를 이용해 N+1 문제 최소화하고 페이징 기능까지 해결할 수 있어야 된다.

수시로 로그를 통해서 쿼리를 모아서 실행할 수 있도록 확인해야 한다.