프로젝트

[Spring/QueryDSL] Fetch Join이랑 limit은 함께 못쓴다고요 ?

SY 키키 2024. 6. 13. 00:14

이전에 이어 페이지네이션 관련 로직을 최적화해보도록 하겠습니다.

https://soyeonnnb.tistory.com/124

 

[Spring] 오프셋 기반 vs 커서 기반 페이지네이션

현재 진행중인 Restagram 프로젝트는 프론트엔드에서 무한스크롤을 통해 사용자에게 피드 리스트를 보여줍니다.이러한 리스트는 한번에 모든 데이터들을 가져오지 않고 페이지를 분리해서 가져

soyeonnnb.tistory.com

 

 

먼저, 쿼리 DSL을 쓰지 않은 결과부터 보도록 하겠습니다.

쿼리DSL 안쓴거

다음과 같이 단순히 jpa와 pageable 객체를 이용해 데이터를 가져와주었습니다.

   List<Feed> feedList = feedRepository.findByIdLessThanAndWriterInOrderByIdDesc(cursorId,
             searchUserList, PageRequest.of(0, 20));

 

그러자, 시간이 매우 많이 걸리는 것을 확인할 수 있습니다.

 

쿼리 DSL쓴거

그래서, 이번에는 쿼리DSL를 써보도록 하겠습니다.

        List<Feed> feedList = feedRepository.findByIdLessThanAndWriterInOrderByIdDescQuerydsl(cursorId,
                searchUserList);

 

다음과 같이 필요한 부분에 대해 join을 걸고, limit을 주었습니다.

 @Override
    public List<Feed> findByIdLessThanAndWriterInOrderByIdDescQuerydsl(Long cursorId, List<User> userList) {
        List<Feed> results = queryFactory
                .selectFrom(feed)
                .leftJoin(feed.feedImageList).fetchJoin()
                .leftJoin(feed.store, store).fetchJoin()
                .leftJoin(store.emdAddress, emdAddress).fetchJoin()
                .leftJoin(emdAddress.siggAddress, siggAddress).fetchJoin()
                .leftJoin(siggAddress.sidoAddress, sidoAddress).fetchJoin()
                .where(feed.id.lt(cursorId)
                        .and(feed.writer.in(userList)))
                .orderBy(feed.id.desc())
                .limit(20)
                .fetch();

        return results;
    }

 

하지만, 속도가 전혀 개선되지 않았음을 확인할 수 있었습니다.

 

개선하기

이게 원인이 아닐 거라 생각이 들어 쿼리를 확인해보았습니다.

그러자 limit이 걸리지 않았음을 확인할 수 있었습니다. !!! 그래서 모든 데이터를(더미데이터 기준 10만개) 다 가져와야 했기 때문이라고 유추할 수 있었습니다.

Hibernate: 
    select
        f1_0.id,
        f1_0.content,
        f1_0.created_at,
        fil1_0.feed_id,
        fil1_0.id,
        fil1_0.created_at,
        fil1_0.number,
        fil1_0.updated_at,
        fil1_0.url,
        f1_0.store_id,
        s1_0.id,
        s1_1.description,
        s1_1.joined_at,
        s1_1.name,
        s1_1.nickname,
        s1_1.phone,
        s1_1.profile_image,
        s1_1.updated_at,
        s1_0.address,
        s1_0.detail_address,
        s1_0.email,
        s1_0.emd_id,
        s1_0.latitude,
        s1_0.longitude,
        s1_0.password,
        s1_0.store_name,
        s1_0.store_phone,
        f1_0.updated_at,
        f1_0.user_id 
    from
        feed f1_0 
    left join
        feed_image fil1_0 
            on f1_0.id=fil1_0.feed_id 
    left join
        (store s1_0 
    join
        user s1_1 
            on s1_0.id=s1_1.id) 
        on s1_0.id=f1_0.store_id 
    where
        f1_0.id<? 
        and f1_0.user_id in (?, ?, ?, ?) 
    order by
        f1_0.id desc

 

 

알아보니, QueryDsl에서 fetch join과 limit 이 함께 사용이 안된다고 합니다....! 

 

그 이유는 간단히 말해 limit절의 경우(즉, Pagination의 작업은) 쿼리에서 진행되는 것이 아닌, 메모리/애플리케이션 단에서 수행이 되기 때문입니다.

 

저의 경우는

1️⃣ limit이 걸린 데이터를 가져옵니다.

2️⃣ 해당 데이터에 대해서 fetch join하는 데이터를 가져옵니다.

이렇게 총 2개의 쿼리를 날려 해결하였습니다.

@RequiredArgsConstructor
public class FeedQuerydslRepositoryImpl implements FeedQuerydslRepository {

    private final JPAQueryFactory queryFactory;
    private final FeedImageRepository feedImageRepository;
    private final FeedLikeRepository feedLikeRepository;

    @Override
    public List<FeedResponse> findByIdLessThanAndWriterInOrderByIdDescQuerydsl(Long cursorId, List<User> userList, User loginUser) {
        List<Feed> feeds = queryFactory
                .selectFrom(feed)
                .where(feed.id.lt(cursorId)
                        .and(feed.writer.in(userList)))
                .orderBy(feed.id.desc())
                .limit(20)
                .fetch();

        // 가져온 feed ID 목록 추출
        List<Long> feedIds = feeds.stream()
                .map(Feed::getId)
                .collect(Collectors.toList());

        // 두 번째 쿼리: feed ID 목록을 이용해 연관 데이터 가져오기 (fetch join 사용 가능)
        List<Feed> results = queryFactory
                .selectFrom(feed)
                .leftJoin(feed.feedImageList).fetchJoin()
                .leftJoin(feed.store, store).fetchJoin()
                .where(feed.id.in(feedIds))
                .fetch();

        return results.stream()
                .map(f -> {
                    List<FeedImage> images = feedImageRepository.findAllByFeed(f);
                    boolean isLike = feedLikeRepository.existsByFeedAndUser(f, loginUser);
                    return FeedResponse.of(f, images, isLike);
                })
                .collect(Collectors.toList());
    }
}

 

속도가 드디어 개선되었습니다 !!!

 

 

쿼리 또한, 2 + 20(좋아요 여부)개가 날라가는 것을 확인할 수 있었습니다.

 

관련 코드는 다음 깃허브에서 확인해주세요.

쿼리 DSL 설정부터 해당 로직까지의 커밋입니다 

https://github.com/soyeonnnb/restagram-api/commit/f68fb5569c9446e8cb526675f0ae370e273d0f78

 

Refactor: 피드목록 가져오기 JPA -> QueryDSL 로 속도 개선 · soyeonnnb/restagram-api@f68fb55

soyeonnnb committed Jun 12, 2024

github.com