Skip to content

Commit f55d000

Browse files
Merge pull request #144 from prgrms-aibe-devcourse/feat/#139-solve-N+1
[ Refact ] N+1 문제 전체 파악 및 개선
2 parents c1acbd9 + 058cd77 commit f55d000

7 files changed

Lines changed: 67 additions & 38 deletions

File tree

src/main/java/com/knoc/dashboard/DashboardService.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.Set;
2324
import java.util.stream.Collectors;
2425

2526
@Service
@@ -37,18 +38,21 @@ public JuniorDashboardDto getJuniorDashboard(String email) {
3738
Member member = memberRepository.findByEmail(email)
3839
.orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND));
3940

40-
List<Order> orders = orderRepository.findByJunior_IdOrderByCreatedAtDesc(member.getId());
41+
List<Order> orders = orderRepository.findByJuniorIdWithSenior(member.getId());
4142

4243
long pendingCount = orders.stream().filter(o -> o.getStatus() == OrderStatus.PENDING).count();
4344
long inProgressCount = orders.stream().filter(o -> o.getStatus() == OrderStatus.PAID).count();
4445
long completedCount = orders.stream().filter(o -> o.getStatus() == OrderStatus.SETTLED).count();
4546

47+
List<Long> orderIds = orders.stream().map(Order::getId).toList();
48+
Set<Long> reviewedOrderIds = reviewFeedbackRepository.findReviewedOrderIds(orderIds);
49+
4650
List<JuniorDashboardDto.OrderSummaryDto> orderSummaryDtos = orders.stream()
4751
.map(o -> JuniorDashboardDto.OrderSummaryDto.builder()
4852
.orderId(o.getId())
4953
.seniorNickname(o.getSenior().getNickname())
5054
.status(o.getStatus())
51-
.hasReview(reviewFeedbackRepository.existsByOrderId(o.getId()))
55+
.hasReview(reviewedOrderIds.contains(o.getId()))
5256
.build())
5357
.toList();
5458

@@ -70,18 +74,21 @@ public SeniorDashBoardDto getSeniorDashboard(String email) {
7074
SeniorProfile seniorProfile = seniorProfileRepository.findByMemberId(member.getId())
7175
.orElseThrow(() -> new BusinessException(ErrorCode.SENIOR_PROFILE_NOT_FOUND));
7276

73-
List<Order> orders = orderRepository.findBySenior_IdOrderByCreatedAtDesc(member.getId());
77+
List<Order> orders = orderRepository.findBySeniorIdWithJunior(member.getId());
7478

7579
long pendingCount = orders.stream().filter(o -> o.getStatus() == OrderStatus.PENDING).count();
7680
long inProgressCount = orders.stream().filter(o -> o.getStatus() == OrderStatus.PAID).count();
7781
long completedCount = orders.stream().filter(o -> o.getStatus() == OrderStatus.SETTLED).count();
7882

83+
List<Long> orderIds = orders.stream().map(Order::getId).toList();
84+
Set<Long> reviewedOrderIds = reviewFeedbackRepository.findReviewedOrderIds(orderIds);
85+
7986
List<SeniorDashBoardDto.OrderSummaryDto> orderSummaryDtos = orders.stream()
8087
.map(o -> SeniorDashBoardDto.OrderSummaryDto.builder()
8188
.orderId(o.getId())
8289
.juniorNickname(o.getJunior().getNickname())
8390
.status(o.getStatus())
84-
.hasReview(reviewFeedbackRepository.existsByOrderId(o.getId()))
91+
.hasReview(reviewedOrderIds.contains(o.getId()))
8592
.build())
8693
.toList();
8794

@@ -93,7 +100,7 @@ public SeniorDashBoardDto getSeniorDashboard(String email) {
93100

94101
// 최근 후기 3개
95102
List<SeniorDashBoardDto.ReviewSummeryDto> reviewSummeryDtos =
96-
reviewFeedbackRepository.findTop3BySeniorProfile_IdOrderByCreatedAtDesc(seniorProfile.getId())
103+
reviewFeedbackRepository.findTop3WithJuniorBySeniorProfileId(seniorProfile.getId(), PageRequest.of(0, 3))
97104
.stream()
98105
.map(r -> SeniorDashBoardDto.ReviewSummeryDto.builder()
99106
.reviewId(r.getId())
@@ -135,9 +142,7 @@ public SeniorReviewPageDto getSeniorReviews(String email, int pageNumber) {
135142
SeniorProfile profile = seniorProfileRepository.findByMemberId(member.getId())
136143
.orElseThrow(() -> new BusinessException(ErrorCode.SENIOR_PROFILE_NOT_FOUND));
137144

138-
Page<ReviewFeedback> reviewPage = reviewFeedbackRepository
139-
.findBySeniorProfile_IdOrderByCreatedAtDesc(
140-
profile.getId(), PageRequest.of(pageNumber, 10));
145+
Page<ReviewFeedback> reviewPage = reviewFeedbackRepository.findWithJuniorBySeniorProfileId(profile.getId(), PageRequest.of(pageNumber, 10));
141146

142147
List<SeniorDashBoardDto.ReviewSummeryDto> reviews = reviewPage.getContent().stream()
143148
.map(r -> SeniorDashBoardDto.ReviewSummeryDto.builder()

src/main/java/com/knoc/order/repository/OrderRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import com.knoc.order.entity.Order;
55
import com.knoc.order.entity.OrderStatus;
66
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
79

810
import java.util.List;
911
import java.util.Optional;
@@ -16,6 +18,12 @@ public interface OrderRepository extends JpaRepository<Order, Long> {
1618

1719
List<Order> findBySenior_IdOrderByCreatedAtDesc(Long seniorId);
1820

21+
@Query("SELECT o FROM Order o JOIN FETCH o.junior WHERE o.senior.id = :seniorId ORDER BY o.createdAt DESC")
22+
List<Order> findBySeniorIdWithJunior(@Param("seniorId") Long seniorId);
23+
24+
@Query("SELECT o FROM Order o JOIN FETCH o.senior WHERE o.junior.id = :juniorId ORDER BY o.createdAt DESC")
25+
List<Order> findByJuniorIdWithSenior(@Param("juniorId") Long juniorId);
26+
1927
// 해당 채팅방에 결제 요청(Order)이 이미 발행된 적이 있는지 여부.
2028
// 시니어의 '결제 요청하기' 버튼 초기 노출 제어에 사용 (한 채팅방당 한 번만 요청하는 정책).
2129
boolean existsByChatRoom_Id(Long chatRoomId);

src/main/java/com/knoc/reviewFeedback/repository/ReviewFeedbackRepository.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
import com.knoc.senior.entity.SeniorProfile;
44
import com.knoc.reviewFeedback.entity.ReviewFeedback;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
57
import org.springframework.data.jpa.repository.JpaRepository;
68
import org.springframework.data.jpa.repository.Query;
79
import org.springframework.data.repository.query.Param;
810

911
import java.time.LocalDateTime;
1012
import java.util.List;
13+
import java.util.Set;
1114

1215
public interface ReviewFeedbackRepository extends JpaRepository<ReviewFeedback,Long> {
1316
boolean existsByOrderId(Long orderId);
@@ -22,10 +25,25 @@ public interface ReviewFeedbackRepository extends JpaRepository<ReviewFeedback,L
2225

2326
org.springframework.data.domain.Page<ReviewFeedback> findBySeniorProfile_IdOrderByCreatedAtDesc(Long seniorProfileId, org.springframework.data.domain.Pageable pageable);
2427

25-
List<ReviewFeedback> findAllByOrderByCreatedAtDesc();
28+
@Query("SELECT r.order.id FROM ReviewFeedback r WHERE r.order.id IN :orderIds")
29+
Set<Long> findReviewedOrderIds(@Param("orderIds") List<Long> orderIds);
2630

27-
@Query("SELECT r.seniorProfile FROM ReviewFeedback r WHERE r.createdAt >= :startOfMonth GROUP BY r.seniorProfile ORDER BY COUNT(r) DESC")
28-
List<SeniorProfile> findTop3ActiveSeniorsThisMonth(@Param("startOfMonth") LocalDateTime startOfMonth, org.springframework.data.domain.Pageable pageable);
31+
@Query("SELECT r FROM ReviewFeedback r JOIN FETCH r.junior WHERE r.seniorProfile.id = :id ORDER BY r.createdAt DESC")
32+
List<ReviewFeedback> findTop3WithJuniorBySeniorProfileId(@Param("id") Long seniorProfileId, Pageable pageable);
2933

30-
List<ReviewFeedback> findByJunior_IdOrderByCreatedAtDesc(Long juniorId);
34+
// 페이지네이션 대체
35+
@Query(
36+
value = "SELECT r FROM ReviewFeedback r JOIN FETCH r.junior WHERE r.seniorProfile.id = :id ORDER BY r.createdAt DESC",
37+
countQuery = "SELECT COUNT(r) FROM ReviewFeedback r WHERE r.seniorProfile.id = :id"
38+
)
39+
Page<ReviewFeedback> findWithJuniorBySeniorProfileId(@Param("id") Long seniorProfileId, Pageable pageable);
40+
41+
@Query("SELECT r FROM ReviewFeedback r JOIN FETCH r.junior JOIN FETCH r.seniorProfile sp JOIN FETCH sp.member JOIN FETCH r.order ORDER BY r.createdAt DESC ")
42+
List<ReviewFeedback> findAllWithRelationsOrderByCreatedAtDesc();
43+
44+
@Query("SELECT r FROM ReviewFeedback r JOIN FETCH r.junior JOIN FETCH r.seniorProfile sp JOIN FETCH sp.member JOIN FETCH r.order WHERE r.junior.id = :juniorId ORDER BY r.createdAt DESC")
45+
List<ReviewFeedback> findByJuniorIdWithRelations(@Param("juniorId") Long juniorId);
46+
47+
@Query("SELECT sp FROM SeniorProfile sp JOIN FETCH sp.member " + "WHERE sp.id IN (" + "SELECT r.seniorProfile.id FROM ReviewFeedback r " + "WHERE r.createdAt >= :startOfMonth " + "GROUP BY r.seniorProfile.id " + "ORDER BY COUNT(r) DESC" + ")")
48+
List<SeniorProfile> findTop3ActiveSeniorsThisMonthWithMember(@Param("startOfMonth") LocalDateTime startOfMonth, Pageable pageable);
3149
}

src/main/java/com/knoc/reviewFeedback/service/ReviewFeedbackService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ public void updateReview(Long orderId, ReviewFeedbackRequestDto dto, Long junior
8484
}
8585

8686
public ReviewPageDto getReviewPage() {
87-
List<ReviewFeedback> feedbacks = reviewFeedbackRepository.findAllByOrderByCreatedAtDesc();
87+
List<ReviewFeedback> feedbacks = reviewFeedbackRepository.findAllWithRelationsOrderByCreatedAtDesc();
8888

8989
List<ReviewPageDto.ReviewCardDto> reviewCards = mapToCards(feedbacks);
9090
LocalDateTime startOfMonth = LocalDate.now().withDayOfMonth(1).atStartOfDay();
9191
List<ReviewPageDto.TopSeniorDto> topSeniors = mapToTopSeniors(
92-
reviewFeedbackRepository.findTop3ActiveSeniorsThisMonth(startOfMonth, PageRequest.of(0, 3)));
92+
reviewFeedbackRepository.findTop3ActiveSeniorsThisMonthWithMember(startOfMonth, PageRequest.of(0, 3)));
9393

9494
return ReviewPageDto.builder()
9595
.reviews(reviewCards)
@@ -138,7 +138,7 @@ public MyReviewPageResponse getMyReviewCards(String email) {
138138
.map(Member::getId)
139139
.orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND));
140140

141-
List<ReviewFeedback> myFeedbacks = reviewFeedbackRepository.findByJunior_IdOrderByCreatedAtDesc(juniorId);
141+
List<ReviewFeedback> myFeedbacks = reviewFeedbackRepository.findByJuniorIdWithRelations(juniorId);
142142
return new MyReviewPageResponse(mapToCards(myFeedbacks), myFeedbacks.size());
143143
}
144144
}

src/main/resources/application.properties

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ springdoc.swagger-ui.disable-swagger-default-url=true
2020
springdoc.swagger-ui.display-request-duration=true
2121

2222
# 4. 로깅 공통 설정
23-
logging.level.org.hibernate.orm.jdbc.bind=DEBUG
23+
logging.level.org.hibernate.orm.jdbc.bind=TRACE
24+
logging.level.org.hibernate.SQL=DEBUG
25+
logging.level.org.springframework.web=DEBUG
2426

2527
# 인증 메일 설정
2628
spring.mail.host=smtp.gmail.com
@@ -50,4 +52,7 @@ toss.payments.connect-timeout=3s
5052
toss.payments.read-timeout=10s
5153

5254
# GitHub API (미설정 시 공개 저장소 60req/h 제한, prod는 환경변수로 주입)
53-
github.token=${GITHUB_TOKEN:}
55+
github.token=${GITHUB_TOKEN:}
56+
57+
spring.jpa.show-sql=true
58+
spring.jpa.properties.hibernate.format_sql=true

src/main/resources/templates/index.html

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,6 @@ <h1 class="display-5 fw-bold mb-4">성장을 이끌어줄 현업 <span class="te
227227
careerSlider.addEventListener('change', loadSeniors);
228228
priceSlider.addEventListener('change', loadSeniors);
229229

230-
// 필터가 적용된 상태면 패널 자동 열기
231-
const hasFilter = parseInt(careerSlider.value) > 0
232-
|| parseInt(priceSlider.value) < MAX_PRICE
233-
|| currentSkill !== '';
234-
if (hasFilter) {
235-
new bootstrap.Collapse(document.getElementById('filterPanel'), { toggle: false }).show();
236-
document.getElementById('filterToggleBtn').classList.add('active');
237-
}
238230
});
239231
</script>
240232
</th:block>

src/test/java/com/knoc/dashboard/DashboardServiceTest.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
import java.time.LocalDateTime;
3030
import java.util.List;
3131
import java.util.Optional;
32+
import java.util.Set;
3233

3334
import static org.assertj.core.api.Assertions.assertThat;
3435
import static org.assertj.core.api.Assertions.assertThatThrownBy;
36+
import static org.mockito.ArgumentMatchers.anyList;
3537
import static org.mockito.BDDMockito.given;
3638
import static org.mockito.Mockito.mock;
3739

@@ -92,11 +94,10 @@ void getJuniorDashboard_Success() {
9294
settledOrder.updateStatus(OrderStatus.SETTLED);
9395
ReflectionTestUtils.setField(settledOrder, "id", 3L);
9496

95-
given(orderRepository.findByJunior_IdOrderByCreatedAtDesc(1L))
97+
given(orderRepository.findByJuniorIdWithSenior(1L))
9698
.willReturn(List.of(settledOrder, paidOrder, pendingOrder));
97-
given(reviewFeedbackRepository.existsByOrderId(1L)).willReturn(false);
98-
given(reviewFeedbackRepository.existsByOrderId(2L)).willReturn(false);
99-
given(reviewFeedbackRepository.existsByOrderId(3L)).willReturn(true);
99+
given(reviewFeedbackRepository.findReviewedOrderIds(anyList()))
100+
.willReturn(Set.of(3L));
100101

101102
// when
102103
JuniorDashboardDto result = dashboardService.getJuniorDashboard(email);
@@ -127,7 +128,7 @@ void getJuniorDashboard_Success_NoOrders() {
127128
given(junior.getId()).willReturn(1L);
128129
given(junior.getNickname()).willReturn("코딩초보");
129130
given(memberRepository.findByEmail(email)).willReturn(Optional.of(junior));
130-
given(orderRepository.findByJunior_IdOrderByCreatedAtDesc(1L)).willReturn(List.of());
131+
given(orderRepository.findByJuniorIdWithSenior(1L)).willReturn(List.of());
131132

132133
// when
133134
JuniorDashboardDto result = dashboardService.getJuniorDashboard(email);
@@ -203,10 +204,10 @@ void getSeniorDashboard_Success() {
203204
paidOrder.updateStatus(OrderStatus.PAID);
204205
ReflectionTestUtils.setField(paidOrder, "id", 2L);
205206

206-
given(orderRepository.findBySenior_IdOrderByCreatedAtDesc(2L))
207+
given(orderRepository.findBySeniorIdWithJunior(2L))
207208
.willReturn(List.of(settledOrder, paidOrder));
208-
given(reviewFeedbackRepository.existsByOrderId(1L)).willReturn(true);
209-
given(reviewFeedbackRepository.existsByOrderId(2L)).willReturn(false);
209+
given(reviewFeedbackRepository.findReviewedOrderIds(anyList()))
210+
.willReturn(Set.of(1L));
210211

211212
// 별점 분포용 전체 후기
212213
ReviewFeedback review5 = mock(ReviewFeedback.class);
@@ -227,7 +228,7 @@ void getSeniorDashboard_Success() {
227228
given(top3Review.getComment()).willReturn("정말 도움이 됐어요!");
228229
given(top3Review.getCreatedAt()).willReturn(LocalDateTime.of(2025, 4, 1, 12, 0));
229230

230-
given(reviewFeedbackRepository.findTop3BySeniorProfile_IdOrderByCreatedAtDesc(10L))
231+
given(reviewFeedbackRepository.findTop3WithJuniorBySeniorProfileId(10L, PageRequest.of(0, 3)))
231232
.willReturn(List.of(top3Review));
232233

233234
// when
@@ -279,9 +280,9 @@ void getSeniorDashboard_Success_NoReviews() {
279280
given(profile.getSkills()).willReturn(List.of());
280281
given(seniorProfileRepository.findByMemberId(2L)).willReturn(Optional.of(profile));
281282

282-
given(orderRepository.findBySenior_IdOrderByCreatedAtDesc(2L)).willReturn(List.of());
283+
given(orderRepository.findBySeniorIdWithJunior(2L)).willReturn(List.of());
283284
given(reviewFeedbackRepository.findBySeniorProfile_Id(10L)).willReturn(List.of());
284-
given(reviewFeedbackRepository.findTop3BySeniorProfile_IdOrderByCreatedAtDesc(10L)).willReturn(List.of());
285+
given(reviewFeedbackRepository.findTop3WithJuniorBySeniorProfileId(10L, PageRequest.of(0, 3))).willReturn(List.of());
285286

286287
// when
287288
SeniorDashBoardDto result = dashboardService.getSeniorDashboard(email);
@@ -362,7 +363,7 @@ void getSeniorReviews_Success() {
362363

363364
PageRequest pageable = PageRequest.of(0, 10);
364365
Page<ReviewFeedback> reviewPage = new PageImpl<>(List.of(review1, review2), pageable, 2);
365-
given(reviewFeedbackRepository.findBySeniorProfile_IdOrderByCreatedAtDesc(10L, pageable))
366+
given(reviewFeedbackRepository.findWithJuniorBySeniorProfileId(10L, pageable))
366367
.willReturn(reviewPage);
367368

368369
// when
@@ -405,7 +406,7 @@ void getSeniorReviews_Success_NoReviews() {
405406

406407
PageRequest pageable = PageRequest.of(0, 10);
407408
Page<ReviewFeedback> emptyPage = new PageImpl<>(List.of(), pageable, 0);
408-
given(reviewFeedbackRepository.findBySeniorProfile_IdOrderByCreatedAtDesc(10L, pageable))
409+
given(reviewFeedbackRepository.findWithJuniorBySeniorProfileId(10L, pageable))
409410
.willReturn(emptyPage);
410411

411412
// when

0 commit comments

Comments
 (0)