Skip to content

Conversation

@dltnals317
Copy link
Collaborator

@dltnals317 dltnals317 commented Mar 10, 2025

📝 Work Description

Zzim 리스트 조회 시 발생했던 N+1 문제를 해결하기 위해, User, Place, Photo, PostCategory 조회 방식을 최적화했습니다.
기존에는 개별적으로 쿼리를 실행하여 데이터를 가져왔으나, 전역 Batch Size 설정 + IN 절 조회 방식으로 쿼리를 최적화하여 성능을 개선했습니다.
페이징을 고려하여 Fetch Join 대신 BatchSize 적용을 활용했습니다.

1. User, Place N+1 문제

  • post:user 관계와 post:place 관계는 n:1 관계이므로, 전역 BatchSize 설정을 통해 N+1 문제를 해결했습니다.
  • Hibernate의 hibernate.default_batch_fetch_size를 설정하여, 한 번의 쿼리로 여러 엔티티를 가져오도록 변경했습니다.
  • 개별 엔티티에 @BatchSize를 추가하여 기본 BatchSize를 사용하지 않는 환경에서도 동일한 효과를 기대할 수 있도록 했습니다.

2. Photo, PostCategory N+1 문제

  • post:photo와 post:postCategory는 1:n 관계입니다.( 이 경우 전역적으로 batch설정이 안됨) 기존 방식으로는 Post별로 각 Photo나 PostCategory를 개별 쿼리로 조회하여 N+1 문제가 발생했습니다.
  • 이를 해결하기 위해, photoRepository와 postCategoryRepository에 새로운 메서드를 추가해, IN 절을 활용한 쿼리로 여러 Post의 Photo 및PostCategory 데이터를 한 번에 가져오도록 수정했습니다.

⚙️ 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 인터페이스 수정:

  • Zzim 관련 Photo, PostCategory 데이터를 일괄 조회하는 메서드를 추가.
  • 서비스 레이어는 포트를 통해 새로운 IN 절 기반 메서드만 사용.

4. Repository 수정:

  • IN 절 기반 쿼리를 추가하여 한 번의 호출로 필요한 데이터를 모두 가져오도록 변경.

@dltnals317 dltnals317 requested a review from airoca March 10, 2025 15:11
@dltnals317 dltnals317 self-assigned this Mar 10, 2025
@dltnals317 dltnals317 added 🐇수민🥕 개쩌는 개발자 이수민 🔨REFACTOR labels Mar 10, 2025
Comment on lines +68 to +79
@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 // 중복 방지
));
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: 말했다시피 2차 스프린트 때 하나의 게시물이 여러 카테고리를 갖도록 수정될 것 같습니다! 고려해서 다시 한번 생각해주셔도 좋을 것 같아요.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "photo")
@BatchSize(size = 10)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Post와 Photo가 1:N 관계라서 @BatchSize가 적용되지 않을 것 같습니다!

Comment on lines +14 to +16
@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);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: 해당 쿼리는 게시물의 "첫 번째" 사진만을 가져와야 하는데, 현재 쿼리는 해당 로직이 없는 것 같습니다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "post_category")
@BatchSize(size = 10)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: 마찬가지로 @BatchSize가 적용되지 않아 불필요한 코드로 보입니다.

}
@Override
public Map<Long, Photo> findFirstPhotosByPostIds(List<Long> postIds) {
List<Photo> photos = photoRepository.findFirstPhotosByPostIds(postIds)
Copy link
Collaborator

@airoca airoca Mar 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: 이렇게 되면 실질적으로 (photoRepository의) findFirstPhotosByPostIds는 첫 번째 사진을 가져오는 메서드가 아니게 되는 것 같습니다. 메서드명에 혼돈이 생기고, 불필요한 조회도 발생할 것 같아요.

.collect(Collectors.toMap(
photo -> photo.getPost().getPostId(),
photo -> photo,
(existing, replacement) -> existing // 중복 방지
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: 해당 부분에서 게시물 당 사진 1개만 남기고 있긴 하지만, 조회 메서드 자체를 수정하는게 나아보입니다.

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.BatchSize;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: 이렇게 사용되지 않는 import는 최종 커밋 전에 깔끔하게 청소해주면 더 좋을 것 같아요!

Comment on lines +17 to +19
@EntityGraph(attributePaths = {"photos", "postCategories", "postCategories.category"})
@Query("SELECT p FROM PostEntity p WHERE p.postId = :postId")
Optional<PostEntity> findPostWithPhotosAndCategories(@Param("postId") Long postId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: 현재 Entity 분리를 감안했을 때 해당 부분이 의도대로 동작할지 의문입니다.

Comment on lines +15 to +16
@Query("SELECT pc FROM PostCategoryEntity pc WHERE pc.post.postId IN :postIds")
List<PostCategoryEntity> findPostCategoriesByPostIds(@Param("postIds") List<Long> postIds);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분 구현이 가장 예상했던 형태에 가까운 것 같습니다.

void saveMenu(Menu menu);
void savePhoto(Photo photo);
void saveScoopPost(User user, Post post);
Map<Long, PostCategory> findPostCategoriesByPostIds(List<Long> postIds);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: 개별 게시물이 여러 카테고리를 갖도록 변경된다는 점이 반영되면 좋을 것 같습니다.

.map(zzimPost -> zzimPost.getPost().getPostId())
.toList();

Map<Long, Photo> firstPhotos = zzimPostPort.findFirstPhotosByPostIds(postIds);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: 결과적으로는 의도대로 동작할 것 같지만, 위의 피드백처럼 postRepositoryfindFirstPhotosByPostIds 쿼리 자체에서 첫 번째 사진만 가져오도록 하는 수정 고려해주세요!

.toList();

Map<Long, Photo> firstPhotos = zzimPostPort.findFirstPhotosByPostIds(postIds);
Map<Long, PostCategory> postCategories = postPort.findPostCategoriesByPostIds(postIds);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: 같은 코멘트를 여러 번 남기고 있지만, 카테고리 복수 선택 가능! 꼭 기억해주세요.

Place place = post.getPlace();
Photo photo = zzimPostPort.findFistPhotoById(post.getPostId());
PostCategory postCategory = postCategoryPort.findPostCategoryByPostId(post.getPostId());
Photo photo = firstPhotos.get(post.getPostId()); // 🔥 Batch 조회된 결과 사용
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

왜 주석이 "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());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: 이 부분도 카테고리 복수 선택 가능 고려해야 할 겁니다.

Comment on lines +110 to +159

// //사용자 지도 리스트 조회
// 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);
// }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 주석들도 최종 커밋 시 꼭 정리해주세요! (혼동 방지)

Copy link
Collaborator

@airoca airoca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코멘트 남긴 부분들 체크해주시고, 꼭!!!! 수정된 API 전부 테스트 해주셔야 합니다.

@heogeonho heogeonho force-pushed the main branch 9 times, most recently from f3757a2 to 982f9d7 Compare September 24, 2025 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐇수민🥕 개쩌는 개발자 이수민 🔨REFACTOR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants