인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

안채연님의 프로필 이미지
안채연

작성한 질문수

실전! 스프링 데이터 JPA

컬렉션 조회 관련

작성

·

29

·

수정됨

0

배운거를 적용하면서 쿼리 개선을 하고 있었는데, 혼자 해결해보려고 했으나 컬렉션 조회가 같이 섞이면서 이해가 잘 안 되는 것 같아 질문을 남기게 되었습니다.

게시글 목록을 불러올때 태그이름 리스트를 같이 필요하기 때문에 관련 memePostTags와 Tag까지 같이 조회를 해야하는 상황입니다.

테이블 관계가 MemePost 1 : N MemePostTag 1 : 1 Tag 이렇게 되어있을때

getMemePost5 메서드와 getMemePost6메서드 둘 중에 어느 것이 더 나은 방법인지, 아니면 더 나은 다른 방법이 있는지 궁금합니다.

getMemePost5의 경우는 MemePostTagRepository에서 Tag까지 fetch join으로 불러오고, Map 객체에 postId를 key로 저장해서 태그 이름을 조회합니다.

getMemePost6의 경우는 페이징으로 조회한 후 postIds를 in절로 postTag와 tag를 fetch join합니다.

 

컬렉션 조회의 경우 페이징을 안할 경우 fetch join으로, 페이징을 할 경우 batch size로 하라는 것까지는 이해가 되었는데, 이 경우에는 postTags를 batch size로 가져오고, 그에 다한 tag도 batchsize로 가져와 추가로 두번의 쿼리가 더 나가게 되어서 다른 방법을 고안해내다가 점점 미궁으로 빠지는 것 같아서 질문을 남기게 되었습니다 😂

public Slice<MemePostSummaryResponse> getMemePosts5(int page, int size, MemePostSort postSort, Long userId) {
        Pageable pageable = PageRequest.of(page, size, postSort.toSort());
        Slice<MemePost> memePostSlice = memePostRepository.findSliceAll(pageable);
        List<Long> postIds = getPostIds(memePostSlice.getContent());

        // MemePostTag와 Tag를 한번에 fetch join 하나의 쿼리로 하되, MemePostTagRepository에서 entitygraph를 통해 fetch join
        Map<Long, List<String>> postTagNames = memePostTagRepository.findTagsByMemePostIdIn(postIds)
                .stream()
                .collect(groupingBy(tag -> tag.getMemePost().getId(),
                        mapping(tag -> tag.getTag().getName(), toList())));
//좋아요도 하나의 쿼리로
        Set<Long> likedPostIds = new HashSet<>(memePostRepository.findLikedPostIds(postIds, user));

        List<MemePostSummaryResponse> responses = memePostSlice.getContent().stream()
                .map(mp -> new MemePostSummaryResponse(
                        mp,
                        likedPostIds.contains(mp.getId()),
                        postTagNames.getOrDefault(mp.getId(), Collections.emptyList())
                ))
                .toList();

        return new SliceImpl<>(responses, pageable, memePostSlice.hasNext());
    }

public Slice<MemePostSummaryResponse> getMemePosts6(int page, int size, MemePostSort postSort, Long userId) {
        Pageable pageable = PageRequest.of(page, size, postSort.toSort());
        Slice<MemePost> memePostSlice = memePostRepository.findSliceAll(pageable);
        List<Long> postIds = getPostIds(memePostSlice.getContent());

  // MemePostTag와 Tag를 한번에 fetch join 
        memePostRepository.findAllWithTagsInPostIds(postIds);
        

//좋아요도 하나의 쿼리로
        Set<Long> likedPostIds = new HashSet<>(memePostRepository.findLikedPostIds(postIds, user));
        List<MemePostSummaryResponse> responses = memePostSlice.getContent().stream()
                .map(mp -> new MemePostSummaryResponse(
                        mp,
                        likedPostIds.contains(mp.getId()),
                        mp.getTagNames()
                ))
                .toList();

        return new SliceImpl<>(responses, pageable, memePostSlice.hasNext());
    }

public interface MemePostRepository extends JpaRepository<MemePost, Long>, MemePostRepositoryCustom {

    @Query("SELECT mp FROM MemePost mp where mp.deletedAt IS NULL")
    Slice<MemePost> findSliceAll(Pageable pageable);

    @Query("SELECT mp.id FROM MemePost mp " +
            "JOIN MemePostLike mpl ON mpl.memePost = mp " +
            "WHERE mp.id IN :postIds AND mpl.user = :user")
    List<Long> findLikedPostIds(@Param("postIds") List<Long> postIds, @Param("user") User user);

   @Query("SELECT mp FROM MemePost mp " +
            "LEFT JOIN FETCH mp.memePostTags mpt " +
            "LEFT JOIN FETCH mpt.tag " +
            "WHERE mp.id In :postIds")
    List<MemePost> findAllWithTagsInPostIds(@Param("postIds") List<Long> postIds);


}

public interface MemePostTagRepository extends JpaRepository<MemePostTag, Long> {

    @EntityGraph(attributePaths = {"tag"})
    List<MemePostTag> findAllByMemePostId(Long postId);

    @EntityGraph(attributePaths = {"tag"})
    List<MemePostTag> findTagsByMemePostIdIn(List<Long> memePostIds);
}


public class MemePost {

    public List<String> getTagNames() {
        return memePostTags.stream()
                .map(mpt -> mpt.getTag().getName())
                .toList();
    }

}

답변 2

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 안채연님

getMemePost6의 경우에는 memePostRepository.findAllWithTagsInPostIds(postIds);의 반환값이 없기 때문에 정확한 의도를 잘 모르겠습니다.

getMemePost5 메서드의 경우에는 findTagsByMemePostIdIn부분에서 엔티티를 조회하지 말고 필요한 데이터만 찾아서 DTO로 조회하는 것도 더 최적화 할 수 있는 방안으로 보입니다.

감사합니다.

안채연님의 프로필 이미지
안채연
질문자

getMemePost6에서 memePostRepository.findAllWithTagsInPostIds(postIds);는 memePostTags와 tag를 memePost와 함께 미리 로딩하고 난 후, memePost.getTagNames()에서 이미 로딩된 엔티티에서 필요한 내용을 가져와서 쓰려고 했습니다. 이런식으로는 보통 사용하지는 않나요?

 

DTO로 최적화하는 방안도 있군요! 한번 더 고쳐보겠습니다!

답변 너무 감사합니다!!

0

안녕하세요, 인프런 AI 인턴입니다.

안채연님께서 문의하신 내용은 JPA에서 컬렉션을 조회하는 방법에 대한 고민이신 것 같습니다. 두 메서드 방식 모두 각각의 장단점이 있습니다.

  1. getMemePosts5 방식:

    • 장점: 태그 정보를 MemePostTagRepository에서 페치 조인(Fetch Join)하여 한 번에 가져오기 때문에 성능상 이점이 있습니다.
    • 단점: 페이징과 함께 사용할 경우에는 문제가 발생할 수 있으며, 케이스에 따라서 메모리 오버플로우 문제가 발생할 수 있습니다.
  2. getMemePosts6 방식:

    • 장점: 페이징 처리가 이루어진 후에 In 절을 사용하여 필요한 데이터를 가져오기 때문에 페이징과의 호환성이 좋습니다.
    • 단점: 이 경우 두 번의 추가 쿼리가 발생할 수 있으며, In 절에 의해 성능이 저하될 수 있습니다.

두 방법 모두 장단점이 있으므로 상황에 맞춰 선택하시는 것이 좋습니다. 저희가 추천하는 방법은 데이터 양이 많지 않고 성능에 큰 문제가 없다면 getMemePosts6 방식이 더 좋을 것 같습니다. 이는 페이징 처리와 같이 사용할 때 더 올바른 데이터를 가져오는 것에 유리합니다.

그러나, 성능이 중요한 경우에는 Batch Size를 적절히 설정하여 두 메서드의 이점을 적절히 결합하는 방법도 고려해볼 만 합니다.

아래는 추가적인 참고 자료입니다:
- Spring Data JPA - Defining Query Methods - Paging, Iterating Large Results, Sorting

도움이 되셨길 바랍니다. 추가 질문이 있으시면 언제든지 말씀해 주세요. 감사합니다!

안채연님의 프로필 이미지
안채연

작성한 질문수

질문하기