페이징 쿼리 최적화

bbbooo·2023년 11월 14일

프로젝트 진행 중, 페이징 쿼리가 연속적으로 발생하는 것을 발견하였다. 쿼리가 한번만 날라갔으면 어떨까.. 싶어서 리팩토링을 하게 되었다.

페이징을 하기 전, 프로젝트의 흐름은 다음과 같다.

  1. 사용자 동영상 업로드
  2. 동영상을 Flask 서버에 전달
  3. Flask 서버는 동영상을 분석 후, 해당 동영상이 어떤 모션(걷기, 뛰기, 잡기.. 등)을 취하는지 문자열 리스트를 반환한다.
  4. Spring 에서는 그것을 userHistory 라는 컬럼에 저장한다.

페이징은 위 과정을 거쳤다 가정하고 다음 부분에서 일어난다.

userHistory를 꺼내와서 Motion 이라는 테이블에 담겨있는 Url(해당 테이블은 미리 생성되어있고, 조회만 가능하다)에서 userHistory가 포함된 Url을 가져와 클라이언트에 반환한다.

해당 코드

public Page<ViewResponse> createViewResponse(Long userId, Pageable pageable) {
        User user = getUserById(userId);
        List<String> userHistories = getUserUserHistory(user);

        List<String> allGifs = new ArrayList<>();
        List<String> allFbxs = new ArrayList<>();

        for (String userHistory : userHistories) {
            Page<String> gifPage = motionRepository.findGifByMotionContaining(userHistory, pageable);
            Page<String> fbxPage = motionRepository.findFbxByMotionContaining(userHistory, pageable);

            allGifs.addAll(gifPage.getContent());
            allFbxs.addAll(fbxPage.getContent());
            });
        }

        ViewResponse viewResponse = ViewResponse.fromList(allGifs, allFbxs);

        return new PageImpl<>(Collections.singletonList(viewResponse), pageable, allGifs.size());
    }
    
// MotionRepository.java

@Query("select m.motionGif from Motion m where m.motionGif like %:message%")
    Page<String> findGifByMotionContaining(@Param("message") String message, Pageable pageable);

    @Query("select m.motionFbx from Motion m where m.motionFbx like %:message%")
    Page<String> findFbxByMotionContaining(@Param("message") String message, Pageable pageable);

이때, 페이징을 할때 쿼리가 3개가 날라간다.

Hibernate: 
    select
        user0_.id as id1_4_0_,
        user0_.created_date as created_2_4_0_,
        user0_.modified_date as modified3_4_0_,
        user0_.email as email4_4_0_,
        user0_.email_verified as email_ve5_4_0_,
        user0_.image_url as image_ur6_4_0_,
        user0_.name as name7_4_0_,
        user0_.password as password8_4_0_,
        user0_.provider as provider9_4_0_,
        user0_.provider_id as provide10_4_0_,
        user0_.role as role11_4_0_,
        user0_.user_group as user_gr12_4_0_,
        user0_.user_history as user_hi13_4_0_ 
    from
        user user0_ 
    where
        user0_.id=?
Hibernate: 
    select
        motion0_.motion_gif as col_0_0_ 
    from
        motion motion0_ 
    where
        motion0_.motion_gif like concat('%', ?, '%') limit ?
Hibernate: 
    select
        motion0_.motion_fbx as col_0_0_ 
    from
        motion motion0_ 
    where
        motion0_.motion_fbx like concat('%', ?, '%') limit ?
  1. 유저를 검색하는 쿼리
  2. gif를 검색하는 쿼리
  3. fbx를 검색하는 쿼리

gif와 fbx 두개를 동시에 검색하게 하고싶었다. 겸사겸사 user를 조회하면서 동시에 gif와 fbx 검색을 가능하게 하고싶었다. 우선, gif와 fbx를 하나의 쿼리에서 동작하게끔 바꾸어보았다.

public Page<ViewResponse> createViewResponse(Long userId, Pageable pageable) {
        User user = getUserById(userId);
        List<String> userHistories = getUserUserHistory(user);

        List<String> allGifs = new ArrayList<>();
        List<String> allFbxs = new ArrayList<>();

        for (String userHistory : userHistories) {
            Page<Motion> motionPage = motionRepository.findByMotionContaining(userHistory, pageable);

            motionPage.getContent().stream().forEach(motion -> {
                allGifs.add(motion.getMotionGif());
                allFbxs.add(motion.getMotionFbx());
            });
        }

        ViewResponse viewResponse = ViewResponse.fromList(allGifs, allFbxs);

        return new PageImpl<>(Collections.singletonList(viewResponse), pageable, allGifs.size());
    }
    
// MotionRepository.java

@Query("select m from Motion m where m.motionGif like %:userHistory% or m.motionFbx like %:userHistory%")
    Page<Motion> findByMotionContaining(@Param("userHistory") String userHistory, Pageable pageable);

기존에는 String으로 따로 가져왔었는데 Motion 객체에 gif, fbx의 정보를 함께 담아둔다. 그리고 페이징된 Motion 객체의 gif와 fbx 정보를 각각의 리스트에 저장한다.

이 작업을 수행한 결과 발생한 쿼리가 2개로 줄었다!

Hibernate: 
    select
        user0_.id as id1_4_0_,
        user0_.created_date as created_2_4_0_,
        user0_.modified_date as modified3_4_0_,
        user0_.email as email4_4_0_,
        user0_.email_verified as email_ve5_4_0_,
        user0_.image_url as image_ur6_4_0_,
        user0_.name as name7_4_0_,
        user0_.password as password8_4_0_,
        user0_.provider as provider9_4_0_,
        user0_.provider_id as provide10_4_0_,
        user0_.role as role11_4_0_,
        user0_.user_group as user_gr12_4_0_,
        user0_.user_history as user_hi13_4_0_ 
    from
        user user0_ 
    where
        user0_.id=?
Hibernate: 
    select
        motion0_.id as id1_2_,
        motion0_.motion_fbx as motion_f2_2_,
        motion0_.motion_gif as motion_g3_2_ 
    from
        motion motion0_ 
    where
        motion0_.motion_gif like concat('%', ?, '%') 
        or motion0_.motion_fbx like concat('%', ?, '%') limit ?

이후 한번의 쿼리로 전부를 가져오는 작업을 진행했는데 막상 완성해보니 성능은 좋아질 순 있어도 코드의 가독성이 너무 안좋아지는 것 같았다.

둘의 정보를 가진 DTO를 만들거나 쿼리를 복잡하게 짜거나..하면 가져올 수는 있었으나, 혼자 작업해도 이 코드는 도저히 못쓸 것같아서 기각했다.

서로 직접적인 연관관계도 없다보니 일단은 2개로 멈추기로 했다. 물론, 내가 생각한 방법이 최선은 아닐 것이고 분명 어떤 방법이 있을 것 같다는 생각도 든다.

0개의 댓글