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

song yuheon·2023년 10월 26일
0

Trouble Shooting

목록 보기
42/57
post-thumbnail
post-custom-banner

문제 상황



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;
    }

기존에는 JPA의 페이징 기능을 사용하여 데이터를 조회하고 있었다.
하지만 데이터의 양이 많아지면서 페이지를 계산하기 위한 카운트 쿼리의 실행 시간이 길어지기 시작했고 이로 인해 사용자가 데이터를 조회하는데 거의 4 ~ 5 초 가까이 걸리는 상황이 발생했다.


해결 방안


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


구현 과정

1. CustomBookRepository

새로운 슬라이스 기능 적용을 위한 Repository를 만들고
Slice<T>를 반환하는 메소드를 만들었다.


@Repository
public interface CustomBookRepository {
    Slice<Book> findAllSliceBooks(BooleanBuilder builder, Pageable pageable);
}

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);
    }
}

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;
    }

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";
    }

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>

페이징과의 성능 비교

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

정리


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


profile
backend_Devloper
post-custom-banner

0개의 댓글