작성
·
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로 조회하는 것도 더 최적화 할 수 있는 방안으로 보입니다.
감사합니다.
0
안녕하세요, 인프런 AI 인턴입니다.
안채연님께서 문의하신 내용은 JPA에서 컬렉션을 조회하는 방법에 대한 고민이신 것 같습니다. 두 메서드 방식 모두 각각의 장단점이 있습니다.
getMemePosts5 방식:
getMemePosts6 방식:
두 방법 모두 장단점이 있으므로 상황에 맞춰 선택하시는 것이 좋습니다. 저희가 추천하는 방법은 데이터 양이 많지 않고 성능에 큰 문제가 없다면 getMemePosts6 방식이 더 좋을 것 같습니다. 이는 페이징 처리와 같이 사용할 때 더 올바른 데이터를 가져오는 것에 유리합니다.
그러나, 성능이 중요한 경우에는 Batch Size를 적절히 설정하여 두 메서드의 이점을 적절히 결합하는 방법도 고려해볼 만 합니다.
아래는 추가적인 참고 자료입니다:
- Spring Data JPA - Defining Query Methods - Paging, Iterating Large Results, Sorting
도움이 되셨길 바랍니다. 추가 질문이 있으시면 언제든지 말씀해 주세요. 감사합니다!
getMemePost6에서 memePostRepository.findAllWithTagsInPostIds(postIds);는 memePostTags와 tag를 memePost와 함께 미리 로딩하고 난 후, memePost.getTagNames()에서 이미 로딩된 엔티티에서 필요한 내용을 가져와서 쓰려고 했습니다. 이런식으로는 보통 사용하지는 않나요?
DTO로 최적화하는 방안도 있군요! 한번 더 고쳐보겠습니다!
답변 너무 감사합니다!!