[REFACTOR]Zzim리스트 조회 시, User,Place,Photo,PostCategory 관련 N+1 문제 해결#115
[REFACTOR]Zzim리스트 조회 시, User,Place,Photo,PostCategory 관련 N+1 문제 해결#115dltnals317 wants to merge 1 commit intomainfrom
Conversation
| @Override | ||
| public Map<Long, PostCategory> findPostCategoriesByPostIds(List<Long> postIds) { | ||
| List<PostCategoryEntity> postCategoryEntities = postCategoryRepository.findPostCategoriesByPostIds(postIds); | ||
|
|
||
| return postCategoryEntities.stream() | ||
| .map(PostCategoryMapper::toDomain) // PostCategoryEntity -> PostCategory 변환 | ||
| .collect(Collectors.toMap( | ||
| postCategory -> postCategory.getPost().getPostId(), | ||
| postCategory -> postCategory, | ||
| (existing, replacement) -> existing // 중복 방지 | ||
| )); | ||
| } |
There was a problem hiding this comment.
P2: 말했다시피 2차 스프린트 때 하나의 게시물이 여러 카테고리를 갖도록 수정될 것 같습니다! 고려해서 다시 한번 생각해주셔도 좋을 것 같아요.
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Table(name = "photo") | ||
| @BatchSize(size = 10) |
There was a problem hiding this comment.
P1: Post와 Photo가 1:N 관계라서 @BatchSize가 적용되지 않을 것 같습니다!
| @Query("SELECT p FROM PhotoEntity p WHERE p.post.postId IN :postIds GROUP BY p.post.postId") | ||
| List<PhotoEntity> findFirstPhotosByPostIds(@Param("postIds") List<Long> postIds); | ||
|
|
There was a problem hiding this comment.
P1: 해당 쿼리는 게시물의 "첫 번째" 사진만을 가져와야 하는데, 현재 쿼리는 해당 로직이 없는 것 같습니다.
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Table(name = "post_category") | ||
| @BatchSize(size = 10) |
There was a problem hiding this comment.
P1: 마찬가지로 @BatchSize가 적용되지 않아 불필요한 코드로 보입니다.
| } | ||
| @Override | ||
| public Map<Long, Photo> findFirstPhotosByPostIds(List<Long> postIds) { | ||
| List<Photo> photos = photoRepository.findFirstPhotosByPostIds(postIds) |
There was a problem hiding this comment.
P1: 이렇게 되면 실질적으로 (photoRepository의) findFirstPhotosByPostIds는 첫 번째 사진을 가져오는 메서드가 아니게 되는 것 같습니다. 메서드명에 혼돈이 생기고, 불필요한 조회도 발생할 것 같아요.
| .collect(Collectors.toMap( | ||
| photo -> photo.getPost().getPostId(), | ||
| photo -> photo, | ||
| (existing, replacement) -> existing // 중복 방지 |
There was a problem hiding this comment.
P1: 해당 부분에서 게시물 당 사진 1개만 남기고 있긴 하지만, 조회 메서드 자체를 수정하는게 나아보입니다.
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import org.hibernate.annotations.BatchSize; |
There was a problem hiding this comment.
P3: 이렇게 사용되지 않는 import는 최종 커밋 전에 깔끔하게 청소해주면 더 좋을 것 같아요!
| @EntityGraph(attributePaths = {"photos", "postCategories", "postCategories.category"}) | ||
| @Query("SELECT p FROM PostEntity p WHERE p.postId = :postId") | ||
| Optional<PostEntity> findPostWithPhotosAndCategories(@Param("postId") Long postId); |
There was a problem hiding this comment.
P1: 현재 Entity 분리를 감안했을 때 해당 부분이 의도대로 동작할지 의문입니다.
| @Query("SELECT pc FROM PostCategoryEntity pc WHERE pc.post.postId IN :postIds") | ||
| List<PostCategoryEntity> findPostCategoriesByPostIds(@Param("postIds") List<Long> postIds); |
There was a problem hiding this comment.
해당 부분 구현이 가장 예상했던 형태에 가까운 것 같습니다.
| void saveMenu(Menu menu); | ||
| void savePhoto(Photo photo); | ||
| void saveScoopPost(User user, Post post); | ||
| Map<Long, PostCategory> findPostCategoriesByPostIds(List<Long> postIds); |
There was a problem hiding this comment.
P1: 개별 게시물이 여러 카테고리를 갖도록 변경된다는 점이 반영되면 좋을 것 같습니다.
| .map(zzimPost -> zzimPost.getPost().getPostId()) | ||
| .toList(); | ||
|
|
||
| Map<Long, Photo> firstPhotos = zzimPostPort.findFirstPhotosByPostIds(postIds); |
There was a problem hiding this comment.
P1: 결과적으로는 의도대로 동작할 것 같지만, 위의 피드백처럼 postRepository의 findFirstPhotosByPostIds 쿼리 자체에서 첫 번째 사진만 가져오도록 하는 수정 고려해주세요!
| .toList(); | ||
|
|
||
| Map<Long, Photo> firstPhotos = zzimPostPort.findFirstPhotosByPostIds(postIds); | ||
| Map<Long, PostCategory> postCategories = postPort.findPostCategoriesByPostIds(postIds); |
There was a problem hiding this comment.
P1: 같은 코멘트를 여러 번 남기고 있지만, 카테고리 복수 선택 가능! 꼭 기억해주세요.
| Place place = post.getPlace(); | ||
| Photo photo = zzimPostPort.findFistPhotoById(post.getPostId()); | ||
| PostCategory postCategory = postCategoryPort.findPostCategoryByPostId(post.getPostId()); | ||
| Photo photo = firstPhotos.get(post.getPostId()); // 🔥 Batch 조회된 결과 사용 |
There was a problem hiding this comment.
왜 주석이 "Batch 조회된 결과 사용" 인가요?
| Photo photo = zzimPostPort.findFistPhotoById(post.getPostId()); | ||
| PostCategory postCategory = postCategoryPort.findPostCategoryByPostId(post.getPostId()); | ||
| Photo photo = firstPhotos.get(post.getPostId()); // 🔥 Batch 조회된 결과 사용 | ||
| PostCategory postCategory = postCategories.get(post.getPostId()); |
There was a problem hiding this comment.
P1: 이 부분도 카테고리 복수 선택 가능 고려해야 할 겁니다.
|
|
||
| // //사용자 지도 리스트 조회 | ||
| // public ZzimCardListResponseDTO getZzimCardList(ZzimGetCardCommand command) { | ||
| // List<ZzimPost> zzimPostList = zzimPostPort.findUserByUserId(command.getUserId()); | ||
| // | ||
| // Map<Long, ZzimPost> uniquePlacePostMap = new LinkedHashMap<>(); | ||
| // | ||
| // for (ZzimPost zzimPost : zzimPostList) { | ||
| // Place place = zzimPost.getPost().getPlace(); | ||
| // if (place == null) { | ||
| // throw new BusinessException(PlaceErrorMessage.PLACE_NOT_FOUND); | ||
| // } | ||
| // | ||
| // Long placeId = place.getPlaceId(); | ||
| // if (!uniquePlacePostMap.containsKey(placeId)) { | ||
| // uniquePlacePostMap.put(placeId, zzimPost); | ||
| // } | ||
| // } | ||
| // | ||
| // List<ZzimCardResponseDTO> zzimCardResponses = uniquePlacePostMap.values().stream() | ||
| // .map(zzimPost -> { | ||
| // Post post = zzimPost.getPost(); | ||
| // Place place = post.getPlace(); | ||
| // Photo photo = zzimPostPort.findFistPhotoById(post.getPostId()); | ||
| // PostCategory postCategory = postCategoryPort.findPostCategoryByPostId(post.getPostId()); | ||
| // | ||
| // CategoryColorResponseDTO categoryColorResponse = new CategoryColorResponseDTO( | ||
| // postCategory.getCategory().getCategoryId(), | ||
| // postCategory.getCategory().getCategoryName(), | ||
| // postCategory.getCategory().getIconUrlColor(), | ||
| // postCategory.getCategory().getTextColor(), | ||
| // postCategory.getCategory().getBackgroundColor()); | ||
| // | ||
| // | ||
| // return new ZzimCardResponseDTO( | ||
| // place.getPlaceId(), // placeId 추가 | ||
| // place.getPlaceName(), | ||
| // place.getPlaceAddress(), | ||
| // post.getTitle(), | ||
| // photo.getPhotoUrl(), | ||
| // place.getLatitude(), | ||
| // place.getLongitude(), | ||
| // categoryColorResponse | ||
| // ); | ||
| // }) | ||
| // .collect(Collectors.toList()); | ||
| // | ||
| // return new ZzimCardListResponseDTO(zzimCardResponses.size(), zzimCardResponses); | ||
| // } | ||
|
|
There was a problem hiding this comment.
이런 주석들도 최종 커밋 시 꼭 정리해주세요! (혼동 방지)
airoca
left a comment
There was a problem hiding this comment.
코멘트 남긴 부분들 체크해주시고, 꼭!!!! 수정된 API 전부 테스트 해주셔야 합니다.
f3757a2 to
982f9d7
Compare
📝 Work Description
Zzim 리스트 조회 시 발생했던 N+1 문제를 해결하기 위해, User, Place, Photo, PostCategory 조회 방식을 최적화했습니다.
기존에는 개별적으로 쿼리를 실행하여 데이터를 가져왔으나, 전역 Batch Size 설정 + IN 절 조회 방식으로 쿼리를 최적화하여 성능을 개선했습니다.
페이징을 고려하여 Fetch Join 대신 BatchSize 적용을 활용했습니다.
1. User, Place N+1 문제
@BatchSize를 추가하여 기본 BatchSize를 사용하지 않는 환경에서도 동일한 효과를 기대할 수 있도록 했습니다.2. Photo, PostCategory N+1 문제
⚙️ Issue
🔨 Changes
1. User, Place 조회 방식 변경:
전역 BatchSize 설정(hibernate.default_batch_fetch_size) 적용.
UserEntity와 PlaceEntity에
@BatchSize(size = 100)추가.2. Photo, PostCategory IN 절 조회 적용:
기존에는 개별 Post별로 Photo와 PostCategory를 각각의 쿼리로 조회.
새로운 쿼리 메서드를 추가하여 IN 절로 다수의 Post에 대한 Photo와 PostCategory 데이터를 한 번에 가져옴.
결과적으로 Photo와 PostCategory 조회에서 발생하던 N+1 문제 해결.
3. Adapter 및 Port 인터페이스 수정:
4. Repository 수정: