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조건으로 사용



6. 정리
데이터베이스를 많이 사용하기 때문에 작업시 N+1의 문제를 생각해두고
간단한 보완책으로 Batch Size를 이용해 N+1 문제 최소화하고 페이징 기능까지 해결할 수 있어야 된다.
수시로 로그를 통해서 쿼리를 모아서 실행할 수 있도록 확인해야 한다.
'웹개발 > Spring Boot' 카테고리의 다른 글
| [Spring Boot] JPA 게시판 게시글 등록, swagger-ui (0) | 2024.02.19 |
|---|---|
| [Spring Boot] RequestDisPatcher 와 sendRedirect (0) | 2024.02.08 |
| [Spring Boot] Model Mapper (0) | 2024.01.29 |
| [Spring Boot ] REST방식 Swagger UI (0) | 2024.01.26 |
| [Spring Boot] 인텔리제이 Log4j2 import 오류 (0) | 2024.01.25 |