diff --git a/build.gradle b/build.gradle index 834f997..0b2ec76 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,9 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // AOP 사용을 위한 스타터 추가 + implementation 'org.springframework.boot:spring-boot-starter-aop' } tasks.named('test') { diff --git a/src/main/java/com/moongeul/backend/api/post/entity/Post.java b/src/main/java/com/moongeul/backend/api/post/entity/Post.java index 6d791f4..81f043a 100644 --- a/src/main/java/com/moongeul/backend/api/post/entity/Post.java +++ b/src/main/java/com/moongeul/backend/api/post/entity/Post.java @@ -23,6 +23,7 @@ public class Post extends BaseTimeEntity { private LocalDate readDate; // 읽은날짜 private Double rating; // 평점 private Integer page; // 페이지 수 + @Column(columnDefinition = "TEXT") private String content; // 감상평 @Enumerated(EnumType.STRING) diff --git a/src/main/java/com/moongeul/backend/api/post/repository/LikeRepository.java b/src/main/java/com/moongeul/backend/api/post/repository/LikeRepository.java index 9b0467f..4262888 100644 --- a/src/main/java/com/moongeul/backend/api/post/repository/LikeRepository.java +++ b/src/main/java/com/moongeul/backend/api/post/repository/LikeRepository.java @@ -11,6 +11,8 @@ public interface LikeRepository extends JpaRepository { List findByPostIdAndMemberId(Long postId, Long memberId); + List findAllByMemberEmailAndPostIdIn(String email, List postIds); + Optional findByPostIdAndMemberIdAndLikeType(Long postId, Long memberId, LikeType likeType); // 특정 게시글의 모든 공감 조회 diff --git a/src/main/java/com/moongeul/backend/api/post/repository/QuoteRepository.java b/src/main/java/com/moongeul/backend/api/post/repository/QuoteRepository.java index 27c07c1..14743d9 100644 --- a/src/main/java/com/moongeul/backend/api/post/repository/QuoteRepository.java +++ b/src/main/java/com/moongeul/backend/api/post/repository/QuoteRepository.java @@ -9,5 +9,7 @@ public interface QuoteRepository extends JpaRepository { List findByPostId(Long postId); + List findAllByPostIdIn(List postIds); + void deleteAllByPostId(Long postId); } diff --git a/src/main/java/com/moongeul/backend/api/post/service/PostService.java b/src/main/java/com/moongeul/backend/api/post/service/PostService.java index 2aade4d..84816b2 100644 --- a/src/main/java/com/moongeul/backend/api/post/service/PostService.java +++ b/src/main/java/com/moongeul/backend/api/post/service/PostService.java @@ -16,6 +16,7 @@ import com.moongeul.backend.api.post.repository.PostRepository; import com.moongeul.backend.api.post.repository.QuoteRepository; import com.moongeul.backend.api.post.util.WritingGuideGenerator; +import com.moongeul.backend.common.annotation.Timer; import com.moongeul.backend.common.exception.NotFoundException; import com.moongeul.backend.common.exception.UnauthorizedException; import com.moongeul.backend.common.response.ErrorStatus; @@ -31,10 +32,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.TemporalAdjusters; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; @Slf4j @@ -116,6 +114,7 @@ public String writingGuide(){ } /* 기록(게시글) 전체 조회 */ + @Timer @Transactional public PostAllResponseDTO getPostAll(PostAllRequestDTO postAllRequestDTO, String email){ @@ -137,16 +136,55 @@ public PostAllResponseDTO getPostAll(PostAllRequestDTO postAllRequestDTO, String postPage = postRepository.findAllByFollower(email, pageable); } - List postDTOList = new ArrayList<>(); - if (!postPage.isEmpty()) { - for(Post post : postPage.getContent()){ - postDTOList.add(getPostDetail(post.getId(), email)); - } + if (postPage.isEmpty()) { + return PostAllResponseDTO.builder().data(new ArrayList<>()).build(); + } + + // Batch 준비: 현재 페이지의 게시글 ID 리스트 추출 + List postIds = postPage.getContent().stream() + .map(Post::getId) + .toList(); + + // Batch 조회 & Map 매핑: 내가 누른 공감들 한꺼번에 가져오기 + Map> myLikesMap; + if (!isAnonymous) { + List allMyLikes = likeRepository.findAllByMemberEmailAndPostIdIn(email, postIds); + myLikesMap = allMyLikes.stream().collect(Collectors.groupingBy( + like -> like.getPost().getId(), + Collectors.mapping(Likes::getLikeType, Collectors.toSet()) + )); + } else { + myLikesMap = new HashMap<>(); } + // Batch 조회 & Map 매핑: 인상 깊은 구절들 한꺼번에 가져오기 + List allQuotes = quoteRepository.findAllByPostIdIn(postIds); + Map> quotesMap = allQuotes.stream() + .collect(Collectors.groupingBy(quote -> quote.getPost().getId())); + + // 메모리 매핑: 루프를 돌며 Map에서 데이터를 꺼내 DTO 조립 (getPostDetail 호출 안 함 X) + List postDTOList = postPage.getContent().stream().map(post -> { + + // 해당 게시글의 공감/구절 데이터를 Map에서 즉시 꺼냄 (DB 조회 0번) + Set myTypes = myLikesMap.getOrDefault(post.getId(), Collections.emptySet()); + List postQuotes = quotesMap.getOrDefault(post.getId(), Collections.emptyList()); + + // MyLikesStatus 객체 조립 + PostDTO.MyLikesStatus myLikesStatus = PostDTO.MyLikesStatus.builder() + .relatableCount(myTypes.contains(LikeType.RELATABLE)) + .sameTasteCount(myTypes.contains(LikeType.SAME_TASTE)) + .impressiveExpressionCount(myTypes.contains(LikeType.IMPRESSIVE_EXPRESSION)) + .wantToReadCount(myTypes.contains(LikeType.WANT_TO_READ)) + .helpfulCount(myTypes.contains(LikeType.HELPFUL)) + .build(); + + // 최종 PostDTO 조립 (기존 getPostDetail에 있던 변환 로직만 활용) + return convertToPostDTO(post, myLikesStatus, postQuotes); + }).toList(); + return PostAllResponseDTO.builder() .total(postPage.getTotalElements()) - .page(postPage.getNumber() + 1) // 페이지 1부터 시작(임의 지정) + .page(postPage.getNumber() + 1) .size(postPage.getSize()) .totalPages(postPage.getTotalPages()) .isLast(postPage.isLast()) @@ -155,68 +193,60 @@ public PostAllResponseDTO getPostAll(PostAllRequestDTO postAllRequestDTO, String } /* 기록(게시글) 상세 조회 */ - @Transactional - public PostDTO getPostDetail(Long postId, String email){ + @Transactional(readOnly = true) + public PostDTO getPostDetail(Long postId, String email) { Post post = getPost(postId); - Book book = getBook(post.getBook().getIsbn()); - - // 멤버 정보(필요 정보만) DTO - PostDTO.MemberInfo memberInfo = PostDTO.MemberInfo.builder() - .memberId(post.getMember().getId()) - .nickname(post.getMember().getNickname()) - .profileImage(post.getMember().getProfileImage()) - .readingTasteType(post.getMember().getReadingTasteType()) - .build(); + List quotes = quoteRepository.findByPostId(postId); - // 책 정보(필요 정보만) DTO - PostDTO.BookInfo bookInfo = PostDTO.BookInfo.builder() - .isbn(book.getIsbn()) - .bookImage(book.getBookImage()) - .title(book.getTitle()) - .author(book.getAuthor()) - .publisher(book.getPublisher()) - .pubdate(book.getPubdate()) - .ratingAverage(book.getRatingAverage()) - .build(); - - // 인상깊은구절 조회 - List quotes = quoteRepository.findByPostId(postId); // 리스트 반환이기에 `orElseThrow()` 사용 x - List quoteDTOList = new ArrayList<>(); - for(Quote quote : quotes){ - PostDTO.QuoteDTO quoteDTO = PostDTO.QuoteDTO.builder() - .quoteContent(quote.getQuoteContent()) - .pageNumber(quote.getPageNumber()) - .build(); - quoteDTOList.add(quoteDTO); - } - - // 공감 개수 DTO - PostDTO.LikesCnt likesCnt = PostDTO.LikesCnt.builder() - .relatableCount(post.getRelatableCount()) - .sameTasteCount(post.getSameTasteCount()) - .impressiveExpressionCount(post.getImpressiveExpressionCount()) - .wantToReadCount(post.getWantToReadCount()) - .helpfulCount(post.getHelpfulCount()) - .build(); - - /* 내가 누른 공감 유형 정보 DTO */ boolean isAnonymous = (email == null || "anonymousUser".equals(email)); PostDTO.MyLikesStatus myLikesStatus = isAnonymous ? PostDTO.MyLikesStatus.empty() : convertToMyLikesStatus(email, postId); + return convertToPostDTO(post, myLikesStatus, quotes); + } + + // 메서드: PostDTO 조립 전용 Helper 메서드 + private PostDTO convertToPostDTO(Post post, PostDTO.MyLikesStatus myLikesStatus, List quotes) { + + List quoteDTOList = quotes.stream() + .map(q -> PostDTO.QuoteDTO.builder() + .quoteContent(q.getQuoteContent()) + .pageNumber(q.getPageNumber()) + .build()) + .toList(); + return PostDTO.builder() - .postId(postId) - .memberInfo(memberInfo) + .postId(post.getId()) + .memberInfo(PostDTO.MemberInfo.builder() + .memberId(post.getMember().getId()) + .nickname(post.getMember().getNickname()) + .profileImage(post.getMember().getProfileImage()) + .readingTasteType(post.getMember().getReadingTasteType()) + .build()) + .bookInfo(PostDTO.BookInfo.builder() + .isbn(post.getBook().getIsbn()) + .bookImage(post.getBook().getBookImage()) + .title(post.getBook().getTitle()) + .author(post.getBook().getAuthor()) + .publisher(post.getBook().getPublisher()) + .pubdate(post.getBook().getPubdate()) + .ratingAverage(post.getBook().getRatingAverage()) + .build()) .created(post.getCreatedAt()) - .bookInfo(bookInfo) .rating(post.getRating()) .content(post.getContent()) .readDate(post.getReadDate()) .quotesCnt(quoteDTOList.size()) .quotes(quoteDTOList) - .likesCnt(likesCnt) + .likesCnt(PostDTO.LikesCnt.builder() + .relatableCount(post.getRelatableCount()) + .sameTasteCount(post.getSameTasteCount()) + .impressiveExpressionCount(post.getImpressiveExpressionCount()) + .wantToReadCount(post.getWantToReadCount()) + .helpfulCount(post.getHelpfulCount()) + .build()) .myLikesStatus(myLikesStatus) .build(); } @@ -335,6 +365,7 @@ private void saveQuotes(PostRequestDTO postRequestDTO, Post post){ */ /* 공감 토글 */ + @Timer @Transactional public void likePost(Long postId, String email, LikeDTO likeDTO){ diff --git a/src/main/java/com/moongeul/backend/api/setting/service/NoticeSettingService.java b/src/main/java/com/moongeul/backend/api/setting/service/NoticeSettingService.java index 7d2f7ad..60eb5b6 100644 --- a/src/main/java/com/moongeul/backend/api/setting/service/NoticeSettingService.java +++ b/src/main/java/com/moongeul/backend/api/setting/service/NoticeSettingService.java @@ -3,7 +3,6 @@ import com.moongeul.backend.api.member.entity.Member; import com.moongeul.backend.api.member.entity.Role; import com.moongeul.backend.api.member.repository.MemberRepository; -import com.moongeul.backend.api.setting.dto.NoticeAllRequestDTO; import com.moongeul.backend.api.setting.dto.NoticeAllResponseDTO; import com.moongeul.backend.api.setting.dto.NoticeDTO; import com.moongeul.backend.api.setting.dto.NoticeRequestDTO; diff --git a/src/main/java/com/moongeul/backend/common/annotation/Timer.java b/src/main/java/com/moongeul/backend/common/annotation/Timer.java new file mode 100644 index 0000000..56e573f --- /dev/null +++ b/src/main/java/com/moongeul/backend/common/annotation/Timer.java @@ -0,0 +1,11 @@ +package com.moongeul.backend.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Timer { +} \ No newline at end of file diff --git a/src/main/java/com/moongeul/backend/common/aspect/TimerAspect.java b/src/main/java/com/moongeul/backend/common/aspect/TimerAspect.java new file mode 100644 index 0000000..f0137d9 --- /dev/null +++ b/src/main/java/com/moongeul/backend/common/aspect/TimerAspect.java @@ -0,0 +1,29 @@ +package com.moongeul.backend.common.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; +import org.springframework.util.StopWatch; + +@Aspect +@Component +@Slf4j +public class TimerAspect { + + @Around("@annotation(com.moongeul.backend.common.annotation.Timer)") + public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable { + StopWatch stopWatch = new StopWatch(); + + try { + stopWatch.start(); + return joinPoint.proceed(); + } finally { + stopWatch.stop(); + log.info("메서드 실행 시간 [{}]: {}ms", + joinPoint.getSignature().getName(), + stopWatch.getTotalTimeMillis()); + } + } +} \ No newline at end of file