diff --git a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/PostPersistenceAdapter.java b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/PostPersistenceAdapter.java index 232033e..c54fdd3 100644 --- a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/PostPersistenceAdapter.java +++ b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/PostPersistenceAdapter.java @@ -20,9 +20,9 @@ import com.spoony.spoony_server.global.message.business.PostErrorMessage; import com.spoony.spoony_server.global.message.business.UserErrorMessage; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Adapter @@ -48,6 +48,11 @@ public List findUserByUserId(Long userId) { .map(PostMapper::toDomain) .collect(Collectors.toList()); } + public Post findPostWithPhotosAndCategoriesByPostId(Long postId) { + return postRepository.findPostWithPhotosAndCategories(postId) + .map(PostMapper::toDomain) + .orElseThrow(() -> new BusinessException(PostErrorMessage.POST_NOT_FOUND)); + } public Post findPostById(Long postId) { return postRepository.findById(postId) @@ -60,6 +65,18 @@ public PostCategory findPostCategoryByPostId(Long postId) { .map(PostCategoryMapper::toDomain) .orElseThrow(() -> new BusinessException(CategoryErrorMessage.CATEGORY_NOT_FOUND)); } + @Override + public Map findPostCategoriesByPostIds(List postIds) { + List postCategoryEntities = postCategoryRepository.findPostCategoriesByPostIds(postIds); + + return postCategoryEntities.stream() + .map(PostCategoryMapper::toDomain) // PostCategoryEntity -> PostCategory 변환 + .collect(Collectors.toMap( + postCategory -> postCategory.getPost().getPostId(), + postCategory -> postCategory, + (existing, replacement) -> existing // 중복 방지 + )); + } public Category findCategoryById(Long categoryId) { return categoryRepository.findById(categoryId) diff --git a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/CategoryEntity.java b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/CategoryEntity.java index b6d6316..6bbc2f7 100644 --- a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/CategoryEntity.java +++ b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/CategoryEntity.java @@ -11,6 +11,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "category") + public class CategoryEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PhotoEntity.java b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PhotoEntity.java index b7ea022..0258b65 100644 --- a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PhotoEntity.java +++ b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PhotoEntity.java @@ -5,11 +5,13 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "photo") +@BatchSize(size = 10) public class PhotoEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PhotoRepository.java b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PhotoRepository.java index c27160c..b88dd97 100644 --- a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PhotoRepository.java +++ b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PhotoRepository.java @@ -1,10 +1,17 @@ package com.spoony.spoony_server.adapter.out.persistence.post.db; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; public interface PhotoRepository extends JpaRepository { Optional> findByPost_PostId(Long postId); + + @Query("SELECT p FROM PhotoEntity p WHERE p.post.postId IN :postIds GROUP BY p.post.postId") + List findFirstPhotosByPostIds(@Param("postIds") List postIds); + } diff --git a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostCategoryEntity.java b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostCategoryEntity.java index d4c761e..7e39f0b 100644 --- a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostCategoryEntity.java +++ b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostCategoryEntity.java @@ -5,11 +5,13 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "post_category") +@BatchSize(size = 10) public class PostCategoryEntity { @Id @@ -31,4 +33,5 @@ public PostCategoryEntity(Long postCategoryId, PostEntity post, CategoryEntity c this.post = post; this.category = category; } + } diff --git a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostCategoryRepository.java b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostCategoryRepository.java index fed9853..1290ef9 100644 --- a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostCategoryRepository.java +++ b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostCategoryRepository.java @@ -1,12 +1,18 @@ package com.spoony.spoony_server.adapter.out.persistence.post.db; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface PostCategoryRepository extends JpaRepository { Optional findByPost_PostId(Long postID); + + @Query("SELECT pc FROM PostCategoryEntity pc WHERE pc.post.postId IN :postIds") + List findPostCategoriesByPostIds(@Param("postIds") List postIds); } diff --git a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostEntity.java b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostEntity.java index f74e7a4..f469559 100644 --- a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostEntity.java +++ b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostEntity.java @@ -7,11 +7,13 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; +import java.util.List; @Entity @Getter @@ -41,6 +43,8 @@ public class PostEntity { @LastModifiedDate private LocalDateTime updatedAt; + + @Builder public PostEntity(Long postId, UserEntity user, PlaceEntity place, String title, String description) { this.postId = postId; diff --git a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostRepository.java b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostRepository.java index 6f084cd..233450d 100644 --- a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostRepository.java +++ b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/post/db/PostRepository.java @@ -1,11 +1,21 @@ package com.spoony.spoony_server.adapter.out.persistence.post.db; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface PostRepository extends JpaRepository { + List findByUser_UserId(Long userId); + + @EntityGraph(attributePaths = {"photos", "postCategories", "postCategories.category"}) + @Query("SELECT p FROM PostEntity p WHERE p.postId = :postId") + Optional findPostWithPhotosAndCategories(@Param("postId") Long postId); + } diff --git a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/ZzimPersistenceAdapter.java b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/ZzimPersistenceAdapter.java index 768e704..c9432dd 100644 --- a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/ZzimPersistenceAdapter.java +++ b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/ZzimPersistenceAdapter.java @@ -21,6 +21,8 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Repository @RequiredArgsConstructor @@ -39,7 +41,20 @@ public Long countZzimByPostId(Long postId) { public boolean existsByUserIdAndPostId(Long userId, Long postId) { return zzimPostRepository.existsByUser_UserIdAndPost_PostId(userId, postId); } + @Override + public Map findFirstPhotosByPostIds(List postIds) { + List photos = photoRepository.findFirstPhotosByPostIds(postIds) + .stream() + .map(PhotoMapper::toDomain) + .toList(); + return photos.stream() + .collect(Collectors.toMap( + photo -> photo.getPost().getPostId(), + photo -> photo, + (existing, replacement) -> existing // 중복 방지 + )); + } @Override public List findUserByUserId(Long userId) { return zzimPostRepository.findByUser_UserId(userId) @@ -63,6 +78,7 @@ public void saveZzimPost(User user, Post post) { zzimPostRepository.save(zzimPostEntity); } + // postId별로 개별적인 조회가 발생하여 N+1 문제 발생! @Override public Photo findFistPhotoById(Long postId) { return photoRepository.findByPost_PostId(postId) @@ -70,6 +86,7 @@ public Photo findFistPhotoById(Long postId) { .orElseThrow(() -> new BusinessException(PostErrorMessage.PHOTO_NOT_FOUND)); } + @Override public List findPhotoListById(Long postId) { return photoRepository.findByPost_PostId(postId) diff --git a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/db/ZzimPostEntity.java b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/db/ZzimPostEntity.java index a28a37e..08cb1b3 100644 --- a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/db/ZzimPostEntity.java +++ b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/db/ZzimPostEntity.java @@ -7,6 +7,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; @Entity @Getter diff --git a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/db/ZzimPostRepository.java b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/db/ZzimPostRepository.java index fa1aeea..9943a71 100644 --- a/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/db/ZzimPostRepository.java +++ b/src/main/java/com/spoony/spoony_server/adapter/out/persistence/zzim/db/ZzimPostRepository.java @@ -13,6 +13,8 @@ public interface ZzimPostRepository extends JpaRepository { Long countByPost_PostId(Long postId); boolean existsByUser_UserIdAndPost_PostId(Long userId, Long postId); + + List findByUser_UserId(Long userId); void deleteByUser_UserIdAndPost_PostId(Long userId, Long postId); } diff --git a/src/main/java/com/spoony/spoony_server/application/port/out/post/PostPort.java b/src/main/java/com/spoony/spoony_server/application/port/out/post/PostPort.java index 05bc87a..7203ae1 100644 --- a/src/main/java/com/spoony/spoony_server/application/port/out/post/PostPort.java +++ b/src/main/java/com/spoony/spoony_server/application/port/out/post/PostPort.java @@ -7,9 +7,11 @@ import com.spoony.spoony_server.domain.user.User; import java.util.List; +import java.util.Map; public interface PostPort { List findUserByUserId(Long userId); + Post findPostWithPhotosAndCategoriesByPostId(Long postId); boolean existsByUserIdAndPostId(Long userId, Long postId); Post findPostById(Long postId); List findPhotoById(Long postId); @@ -19,4 +21,5 @@ public interface PostPort { void saveMenu(Menu menu); void savePhoto(Photo photo); void saveScoopPost(User user, Post post); + Map findPostCategoriesByPostIds(List postIds); } diff --git a/src/main/java/com/spoony/spoony_server/application/port/out/zzim/ZzimPostPort.java b/src/main/java/com/spoony/spoony_server/application/port/out/zzim/ZzimPostPort.java index 7560704..016fca5 100644 --- a/src/main/java/com/spoony/spoony_server/application/port/out/zzim/ZzimPostPort.java +++ b/src/main/java/com/spoony/spoony_server/application/port/out/zzim/ZzimPostPort.java @@ -6,6 +6,7 @@ import com.spoony.spoony_server.domain.zzim.ZzimPost; import java.util.List; +import java.util.Map; public interface ZzimPostPort { Long countZzimByPostId(Long postId); @@ -15,4 +16,5 @@ public interface ZzimPostPort { List findUserByUserId(Long userId); void saveZzimPost(User user, Post post); void deleteByUserAndPost(User user, Post post); + Map findFirstPhotosByPostIds(List postIds); // 🔥 추가된 메서드 } diff --git a/src/main/java/com/spoony/spoony_server/application/service/post/PostService.java b/src/main/java/com/spoony/spoony_server/application/service/post/PostService.java index 2980519..c2584f7 100644 --- a/src/main/java/com/spoony/spoony_server/application/service/post/PostService.java +++ b/src/main/java/com/spoony/spoony_server/application/service/post/PostService.java @@ -62,7 +62,9 @@ public class PostService implements @Transactional public PostResponseDTO getPostById(PostGetCommand command) { - Post post = postPort.findPostById(command.getPostId()); + //Post post = postPort.findPostById(command.getPostId()); + Post post = postPort.findPostWithPhotosAndCategoriesByPostId(command.getPostId()); + User user = userPort.findUserById(command.getUserId()); PostCategory postCategory = postCategoryPort.findPostCategoryByPostId(post.getPostId()); diff --git a/src/main/java/com/spoony/spoony_server/application/service/zzim/ZzimPostService.java b/src/main/java/com/spoony/spoony_server/application/service/zzim/ZzimPostService.java index 9279ec4..ec99870 100644 --- a/src/main/java/com/spoony/spoony_server/application/service/zzim/ZzimPostService.java +++ b/src/main/java/com/spoony/spoony_server/application/service/zzim/ZzimPostService.java @@ -52,7 +52,6 @@ public void addZzimPost(ZzimAddCommand command) { zzimPostPort.saveZzimPost(user,post); } - //사용자 지도 리스트 조회 public ZzimCardListResponseDTO getZzimCardList(ZzimGetCardCommand command) { List zzimPostList = zzimPostPort.findUserByUserId(command.getUserId()); @@ -70,12 +69,20 @@ public ZzimCardListResponseDTO getZzimCardList(ZzimGetCardCommand command) { } } + // 🔥 N+1 문제 해결: 한 번에 모든 postId의 첫 번째 사진을 조회 + List postIds = uniquePlacePostMap.values().stream() + .map(zzimPost -> zzimPost.getPost().getPostId()) + .toList(); + + Map firstPhotos = zzimPostPort.findFirstPhotosByPostIds(postIds); + Map postCategories = postPort.findPostCategoriesByPostIds(postIds); List 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()); + Photo photo = firstPhotos.get(post.getPostId()); // 🔥 Batch 조회된 결과 사용 + PostCategory postCategory = postCategories.get(post.getPostId()); + //PostCategory postCategory = postCategoryPort.findPostCategoryByPostId(post.getPostId()); CategoryColorResponseDTO categoryColorResponse = new CategoryColorResponseDTO( postCategory.getCategory().getCategoryId(), @@ -84,13 +91,12 @@ public ZzimCardListResponseDTO getZzimCardList(ZzimGetCardCommand command) { postCategory.getCategory().getTextColor(), postCategory.getCategory().getBackgroundColor()); - return new ZzimCardResponseDTO( - place.getPlaceId(), // placeId 추가 + place.getPlaceId(), place.getPlaceName(), place.getPlaceAddress(), post.getTitle(), - photo.getPhotoUrl(), + photo != null ? photo.getPhotoUrl() : null, // 사진이 없을 경우 null 처리 place.getLatitude(), place.getLongitude(), categoryColorResponse @@ -101,6 +107,56 @@ public ZzimCardListResponseDTO getZzimCardList(ZzimGetCardCommand command) { return new ZzimCardListResponseDTO(zzimCardResponses.size(), zzimCardResponses); } + +// //사용자 지도 리스트 조회 +// public ZzimCardListResponseDTO getZzimCardList(ZzimGetCardCommand command) { +// List zzimPostList = zzimPostPort.findUserByUserId(command.getUserId()); +// +// Map 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 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); +// } + public ZzimFocusListResponseDTO getZzimFocusList(ZzimGetFocusCommand command) { User user = userPort.findUserById(command.getUserId()); List zzimPostList = zzimPostPort.findUserByUserId(user.getUserId());