Blog

프로젝트 성능 개선 : 대용량 데이터 처리를 위한 페이징에서 슬라이스로의 전환

tag
기능 개선
날짜
2023/10/26
생성 일시
2023/10/28 06:39
작성자

문제 상황

Paging

public Page<BookResponseDto> getAllBooksByCategoryOrKeyword3(String bookCategoryName, String keyword, int page) { QBook qBook = QBook.book; BooleanBuilder builder = new BooleanBuilder(); List<BookCategory> bookCategories = null; if (bookCategoryName != null) { BookCategory bookCategory = bookCategoryRepository.findByBookCategoryName(bookCategoryName); bookCategories = saveAllCategories(bookCategory); } if(keyword != null) builder.and(qBook.bookName.contains(keyword)); if(bookCategories != null) builder.and(qBook.bookCategory.in(bookCategories)); Sort sort = Sort.by(Sort.Direction.ASC, "bookId"); Pageable pageable = PageRequest.of(page, 20, sort); Page<BookResponseDto> bookList = bookRepository.findAll(builder, pageable).map(BookResponseDto::new); System.out.println(bookList.getTotalElements()); return bookList; }
Java
복사
기존에는 JPA의 페이징 기능을 사용하여 데이터를 조회하고 있었다. 하지만 데이터의 양이 많아지면서 페이지를 계산하기 위한 카운트 쿼리의 실행 시간이 길어지기 시작했고 이로 인해 사용자가 데이터를 조회하는데 거의 4 ~ 5 초 가까이 걸리는 상황이 발생했다.

해결 방안

이 문제를 해결하기 위해 슬라이스 기능을 도입하게 되었다. 슬라이스는 전체 페이지 수나 총 데이터 개수를 알 필요 없이 현재 페이지의 데이터만을 가져올 수 있는 방식으로 메모리 사용량을 줄이고 성능을 향상시킬 수 있는 장점이 있다.

구현 과정

1. CustomBookRepository

새로운 슬라이스 기능 적용을 위한 Repository를 만들고 Slice<T>를 반환하는 메소드를 만들었다.
@Repository public interface CustomBookRepository { Slice<Book> findAllSliceBooks(BooleanBuilder builder, Pageable pageable); }
Java
복사

2. CustomBookRepositoryImpl

생성한 메소드를 Override해서 함수를 최종적으로 구현한다.
@Repository public class CustomBookRepositoryImpl implements CustomBookRepository{ @Autowired private JPAQueryFactory queryFactory; @Override public Slice<Book> findAllSliceBooks(BooleanBuilder builder, Pageable pageable) { QBook book = QBook.book; List<Book> results = queryFactory.selectFrom(book) .where(builder) .limit(pageable.getPageSize()+1) .orderBy(book.bookId.asc()) .offset(pageable.getOffset()) .fetch(); boolean hasNext = false; if(results.size() > pageable.getPageSize()){ results.remove(results.size()-1); hasNext = true; } return new SliceImpl<>(results,pageable,hasNext); } }
Java
복사

3. Service 수정

Slice를 반환받아 로직을 처리하도록 수정하였고 클라이언트에 필요한 데이터만을 추려서 전달하도록 했다.
public Slice<BookResponseDto> getAllBooksByCategoryOrKeywordV4(String bookCategoryName, String keyword, int page) { QBook qBook = QBook.book; BooleanBuilder builder = new BooleanBuilder(); List<BookCategory> bookCategories = null; if (bookCategoryName != null) { BookCategory bookCategory = bookCategoryRepository.findByBookCategoryName(bookCategoryName); bookCategories = saveAllCategories(bookCategory); } if (keyword != null) builder.and(qBook.bookName.contains(keyword)); if (bookCategories != null) builder.and(qBook.bookCategory.in(bookCategories)); Sort sort = Sort.by(Sort.Direction.ASC, "bookId"); Pageable pageable = PageRequest.of(page, 20, sort); // Slice로 변경 Slice<BookResponseDto> bookList = customBookRepository.findAllSliceBooks(builder, pageable).map(BookResponseDto::new); System.out.println(bookList.hasNext()); return bookList; }
Java
복사

4. 컨트롤러 수정

클라이언트에게 Slice의 내용물과 함께 다음 페이지 존재 여부도 함께 전달하도록 수정했다.
@GetMapping("/search/v4") public String mySearchView4(@RequestParam(value = "bookCategoryName", required = false) String bookCategoryName, @RequestParam(value = "keyword", required = false) String keyword, @RequestParam(value = "page", defaultValue = "0", required = false) Integer page, Model model) { // Slice로 변경 Slice<BookResponseDto> bookResponseDtoSlice = searchService.getAllBooksByCategoryOrKeywordV4(bookCategoryName, keyword, page); long startTime = System.currentTimeMillis();//실행시간 측정 model.addAttribute("categories", adminCategoriesService.getAllCategories()); model.addAttribute("currentPage", page); model.addAttribute("books", bookResponseDtoSlice.getContent()); model.addAttribute("hasNext", bookResponseDtoSlice.hasNext()); long endTime = System.currentTimeMillis(); long durationTimeSec = endTime - startTime; System.out.println(durationTimeSec + "m/s"); // 실행시간 측정 return "/users/searchV2"; }
Java
복사

5. 프론트엔드 수정

필요한 경우 프론트엔드에서도 SlicehasNext 값을 사용하여 추가적인 데이터 로딩 여부를 결정할 수 있도록 수정했다.
<div id="paging"> <button th:if="${currentPage!=0}" onclick="goToPrePage()">Pre</button> <button th:if="${hasNext}" onclick="goToNextPage()">Next</button> </div> <script th:inline="javascript"> /*<![CDATA[*/ let currentPage = [[${currentPage}]]; /*]]>*/ </script>
HTML
복사

페이징과의 성능 비교

카운터 하는 쿼리가 나가지 않기에 성능이 4257 -> 13으로 300배 정도 성능이 향상되었다.

정리

슬라이스를 도입한 후 데이터 조회 시간이 현저히 줄어들었다. 사용자는 더 빠르게 데이터를 조회할 수 있게 되었고 서버와 데이터베이스에 가해지는 부하도 감소했다.