From d5402235851c17e74731cd022e0dbb79b8a55d11 Mon Sep 17 00:00:00 2001 From: juhyun Date: Fri, 13 Feb 2026 15:32:17 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor=20:=20=EB=AA=A8=EC=9E=84=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20IN=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/GatheringCountProjection.java | 6 + .../repository/GatheringMemberRepository.java | 13 ++ .../gathering/service/GatheringService.java | 65 +++++- .../meeting/repository/MeetingRepository.java | 11 + .../service/GatheringServiceN1Test.java | 195 ++++++++++++++++++ 5 files changed, 280 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/dokdok/gathering/repository/GatheringCountProjection.java create mode 100644 src/test/java/com/dokdok/gathering/service/GatheringServiceN1Test.java diff --git a/src/main/java/com/dokdok/gathering/repository/GatheringCountProjection.java b/src/main/java/com/dokdok/gathering/repository/GatheringCountProjection.java new file mode 100644 index 0000000..eb612ee --- /dev/null +++ b/src/main/java/com/dokdok/gathering/repository/GatheringCountProjection.java @@ -0,0 +1,6 @@ +package com.dokdok.gathering.repository; + +public interface GatheringCountProjection { + Long getGatheringId(); + Long getCount(); +} diff --git a/src/main/java/com/dokdok/gathering/repository/GatheringMemberRepository.java b/src/main/java/com/dokdok/gathering/repository/GatheringMemberRepository.java index 0870154..57881a7 100644 --- a/src/main/java/com/dokdok/gathering/repository/GatheringMemberRepository.java +++ b/src/main/java/com/dokdok/gathering/repository/GatheringMemberRepository.java @@ -168,4 +168,17 @@ int countMembersByStatus( @Param("gatheringId") Long gatheringId, @Param("status") GatheringMemberStatus status ); + + /** + * 여러 모임의 ACTIVE 멤버 수를 한번에 조회 + */ + @Query("SELECT gm.gathering.id AS gatheringId, COUNT(gm) AS count " + + "FROM GatheringMember gm " + + "WHERE gm.gathering.id IN :gatheringIds " + + "AND gm.memberStatus = 'ACTIVE' " + + "AND gm.removedAt IS NULL " + + "GROUP BY gm.gathering.id") + List countActiveMembersByGatherings( + @Param("gatheringIds") List gatheringIds + ); } diff --git a/src/main/java/com/dokdok/gathering/service/GatheringService.java b/src/main/java/com/dokdok/gathering/service/GatheringService.java index 945282a..da93528 100644 --- a/src/main/java/com/dokdok/gathering/service/GatheringService.java +++ b/src/main/java/com/dokdok/gathering/service/GatheringService.java @@ -8,6 +8,7 @@ import com.dokdok.gathering.entity.*; import com.dokdok.gathering.exception.GatheringErrorCode; import com.dokdok.gathering.exception.GatheringException; +import com.dokdok.gathering.repository.GatheringCountProjection; import com.dokdok.gathering.repository.GatheringMemberRepository; import com.dokdok.gathering.repository.GatheringRepository; import com.dokdok.global.response.CursorResponse; @@ -129,12 +130,19 @@ public FavoriteGatheringListResponse getFavoriteGatherings() { List favoriteMembers = gatheringMemberRepository.findFavoriteGatheringsByUserId(userId); + List gatheringIds = favoriteMembers.stream() + .map(gm -> gm.getGathering().getId()) + .toList(); + + Map memberCountMap = getActiveMemberCountMap(gatheringIds); + Map meetingCountMap = getMeetingCountMap(gatheringIds); + List gatheringResponses = favoriteMembers.stream() - .map(gatheringMember -> { - Gathering gathering = gatheringMember.getGathering(); - int totalMembers = getActiveMemberCount(gathering.getId()); - int totalMeetings = getMeetingCount(gathering.getId()); - return GatheringListItemResponse.from(gatheringMember, totalMembers, totalMeetings, gatheringMember.getRole()); + .map(gm -> { + Long gatheringId = gm.getGathering().getId(); + int totalMembers = memberCountMap.getOrDefault(gatheringId, 0); + int totalMeetings = meetingCountMap.getOrDefault(gatheringId, 0); + return GatheringListItemResponse.from(gm, totalMembers, totalMeetings, gm.getRole()); }) .toList(); @@ -163,12 +171,20 @@ public CursorResponse getMyGatheri boolean hasNext = members.size() > pageSize; List pageMembers = hasNext ? members.subList(0, pageSize) : members; + List gatheringIds = pageMembers.stream() + .map(gm -> gm.getGathering().getId()) + .toList(); + + Map memberCountMap = getActiveMemberCountMap(gatheringIds); + Map meetingCountMap = getMeetingCountMap(gatheringIds); + List items = pageMembers.stream() - .map(gm -> GatheringListItemResponse.from( - gm, - getActiveMemberCount(gm.getGathering().getId()), - getMeetingCount(gm.getGathering().getId()), - gm.getRole())) + .map(gm ->{ + Long gatheringId = gm.getGathering().getId(); + int totalMembers = memberCountMap.getOrDefault(gatheringId, 0); + int totalMeetings = meetingCountMap.getOrDefault(gatheringId, 0); + return GatheringListItemResponse.from(gm, totalMembers, totalMeetings, gm.getRole()); + }) .toList(); GatheringMember lastMember = pageMembers.isEmpty() ? null : pageMembers.get(pageMembers.size() - 1); @@ -419,4 +435,33 @@ private int getMeetingCount(Long gatheringId) { return meetingRepository.countByGatheringIdAndMeetingStatus(gatheringId, MeetingStatus.DONE); } + /** + * 여러 모임의 활성 멤버 수를 Map으로 조회 + */ + private Map getActiveMemberCountMap(List gatheringIds) { + if(gatheringIds.isEmpty()) { + return Map.of(); + } + return gatheringMemberRepository.countActiveMembersByGatherings(gatheringIds) + .stream() + .collect(Collectors.toMap( + GatheringCountProjection::getGatheringId, + p -> p.getCount().intValue() + )); + } + + /** + * 여러 모임의 완료된 미팅 수를 Map으로 조회 + */ + private Map getMeetingCountMap(List gatheringIds) { + if (gatheringIds.isEmpty()) { + return Map.of(); + } + return meetingRepository.countByGatheringIdsAndStatus(gatheringIds, MeetingStatus.DONE) + .stream() + .collect(Collectors.toMap( + GatheringCountProjection::getGatheringId, + p -> p.getCount().intValue() + )); + } } diff --git a/src/main/java/com/dokdok/meeting/repository/MeetingRepository.java b/src/main/java/com/dokdok/meeting/repository/MeetingRepository.java index f375352..84a8a16 100644 --- a/src/main/java/com/dokdok/meeting/repository/MeetingRepository.java +++ b/src/main/java/com/dokdok/meeting/repository/MeetingRepository.java @@ -1,5 +1,6 @@ package com.dokdok.meeting.repository; +import com.dokdok.gathering.repository.GatheringCountProjection; import com.dokdok.meeting.entity.Meeting; import com.dokdok.meeting.entity.MeetingStatus; import org.springframework.data.domain.Page; @@ -113,4 +114,14 @@ Optional findTopByGatheringIdAndBookIdAndMeetingStatusOrderByMeetingSta """) List findByIdInWithGathering(@Param("meetingIds") List meetingIds); + /** + * 여러 모임의 완료된 미팅 수를 한번에 조회 + */ + @Query("SELECT m.gathering.id AS gatheringId, COUNT (m) AS count " + + "FROM Meeting m " + + "WHERE m.gathering.id IN :gatheringIds " + + "AND m.meetingStatus = :status " + + "GROUP BY m.gathering.id") + List countByGatheringIdsAndStatus(@Param("gatheringIds") List gatheringIds, @Param("status") MeetingStatus status); + } diff --git a/src/test/java/com/dokdok/gathering/service/GatheringServiceN1Test.java b/src/test/java/com/dokdok/gathering/service/GatheringServiceN1Test.java new file mode 100644 index 0000000..d969016 --- /dev/null +++ b/src/test/java/com/dokdok/gathering/service/GatheringServiceN1Test.java @@ -0,0 +1,195 @@ +package com.dokdok.gathering.service; + +import com.dokdok.gathering.entity.*; +import com.dokdok.gathering.repository.GatheringMemberRepository; +import com.dokdok.gathering.repository.GatheringRepository; +import com.dokdok.global.util.SecurityUtil; +import com.dokdok.meeting.entity.Meeting; +import com.dokdok.meeting.entity.MeetingStatus; +import com.dokdok.meeting.repository.MeetingRepository; +import com.dokdok.user.entity.User; +import com.dokdok.user.repository.UserRepository; +import jakarta.persistence.EntityManager; +import org.hibernate.Session; +import org.hibernate.stat.Statistics; +import org.junit.jupiter.api.*; +import org.mockito.MockedStatic; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +import static org.mockito.Mockito.mockStatic; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +@DisplayName("Gathering N+1 쿼리 테스트") +class GatheringServiceN1Test { + + @Autowired + private GatheringService gatheringService; + + @Autowired + private UserRepository userRepository; + + @Autowired + private GatheringRepository gatheringRepository; + + @Autowired + private GatheringMemberRepository gatheringMemberRepository; + + @Autowired + private MeetingRepository meetingRepository; + + @Autowired + private EntityManager entityManager; + + private MockedStatic securityUtilMock; + private User testUser; + private Statistics statistics; + + @BeforeEach + void setUp() { + // Hibernate Statistics 활성화 + Session session = entityManager.unwrap(Session.class); + statistics = session.getSessionFactory().getStatistics(); + statistics.setStatisticsEnabled(true); + + // SecurityUtil Mock + securityUtilMock = mockStatic(SecurityUtil.class); + + // 테스트 유저 생성 + testUser = userRepository.save(User.builder() + .kakaoId(12345L) + .nickname("테스트유저") + .profileImageUrl("test.jpg") + .build()); + + securityUtilMock.when(SecurityUtil::getCurrentUserId).thenReturn(testUser.getId()); + } + + @AfterEach + void tearDown() { + securityUtilMock.close(); + statistics.clear(); + } + + @Test + @DisplayName("getFavoriteGatherings - N+1 쿼리 발생 확인") + void getFavoriteGatherings_N1_Problem() { + // given : 즐겨찾기 모임 5개 생성 + int gatheringCount = 5; + createFavoriteGatherings(gatheringCount); + + // 영속성 컨텍스트 초기화 + entityManager.flush(); + entityManager.clear(); + statistics.clear(); + + // when + gatheringService.getFavoriteGatherings(); + + // then: 쿼리 수 확인 + long queryCount = statistics.getQueryExecutionCount(); + + System.out.println("========================================"); + System.out.println("즐겨찾기 모임 수: " + gatheringCount); + System.out.println("실행된 쿼리 수: " + queryCount); + System.out.println("예상 쿼리 수 (N+1 발생 시): " + (1 + gatheringCount * 2)); + System.out.println("예상 쿼리 수 (최적화 후): 3"); + System.out.println("========================================"); + + // N+1 발생 시: 1(목록조회) + N(멤버카운트) + N(미팅카운트) = 1 + 2N + // 최적화 후: 1(목록조회) + 1(멤버카운트) + 1(미팅카운트) = 3 + + // 현재 N+1 문제가 있으므로 쿼리가 많이 발생함을 확인 + System.out.println("N+1 문제 발생 여부: " + (queryCount > 3 ? "YES" : "NO")); + } + + @Test + @DisplayName("getMyGatherings - N+1 쿼리 발생 확인") + void getMyGatherings_N1_Problem() { + // given: 내 모임 10개 생성 + int gatheringCount = 10; + createMyGatherings(gatheringCount); + + entityManager.flush(); + entityManager.clear(); + statistics.clear(); + + // when + gatheringService.getMyGatherings(10, null, null); + + // then + long queryCount = statistics.getQueryExecutionCount(); + + System.out.println("========================================"); + System.out.println("내 모임 수: " + gatheringCount); + System.out.println("실행된 쿼리 수: " + queryCount); + System.out.println("예상 쿼리 수 (N+1 발생 시): " + (2 + gatheringCount * 2)); + System.out.println("예상 쿼리 수 (최적화 후): 4"); + System.out.println("========================================"); + + System.out.println("N+1 문제 발생 여부: " + (queryCount > 4 ? "YES" : "NO")); + } + + private void createFavoriteGatherings(int count) { + for (int i = 0; i < count; i++) { + Gathering gathering = gatheringRepository.save(Gathering.builder() + .gatheringName("모임" + i) + .description("설명" + i) + .gatheringStatus(GatheringStatus.ACTIVE) + .invitationLink("invite" + i) + .gatheringLeader(testUser) + .build()); + + gatheringMemberRepository.save(GatheringMember.builder() + .gathering(gathering) + .user(testUser) + .role(GatheringRole.LEADER) + .memberStatus(GatheringMemberStatus.ACTIVE) + .isFavorite(true) + .joinedAt(LocalDateTime.now()) + .build()); + + // 미팅도 몇 개 생성 + meetingRepository.save(Meeting.builder() + .gathering(gathering) + .meetingStatus(MeetingStatus.DONE) + .meetingStartDate(LocalDateTime.now().minusDays(1)) + .meetingEndDate(LocalDateTime.now()) + .build()); + } + } + + private void createMyGatherings(int count) { + for (int i = 0; i < count; i++) { + Gathering gathering = gatheringRepository.save(Gathering.builder() + .gatheringName("모임" + i) + .description("설명" + i) + .gatheringStatus(GatheringStatus.ACTIVE) + .invitationLink("invite" + i) + .gatheringLeader(testUser) + .build()); + + gatheringMemberRepository.save(GatheringMember.builder() + .gathering(gathering) + .user(testUser) + .role(GatheringRole.LEADER) + .memberStatus(GatheringMemberStatus.ACTIVE) + .isFavorite(false) + .joinedAt(LocalDateTime.now().minusDays(i)) + .build()); + + meetingRepository.save(Meeting.builder() + .gathering(gathering) + .meetingStatus(MeetingStatus.DONE) + .meetingStartDate(LocalDateTime.now().minusDays(1)) + .meetingEndDate(LocalDateTime.now()) + .build()); + } + } +} From b99f02d9248f9101df0e694a638b69e1d667c0fd Mon Sep 17 00:00:00 2001 From: juhyun Date: Fri, 13 Feb 2026 15:45:43 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor=20:=20CI=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/GatheringServiceN1Test.java | 195 ------------------ .../service/GatheringServiceTest.java | 59 ++++-- 2 files changed, 43 insertions(+), 211 deletions(-) delete mode 100644 src/test/java/com/dokdok/gathering/service/GatheringServiceN1Test.java diff --git a/src/test/java/com/dokdok/gathering/service/GatheringServiceN1Test.java b/src/test/java/com/dokdok/gathering/service/GatheringServiceN1Test.java deleted file mode 100644 index d969016..0000000 --- a/src/test/java/com/dokdok/gathering/service/GatheringServiceN1Test.java +++ /dev/null @@ -1,195 +0,0 @@ -package com.dokdok.gathering.service; - -import com.dokdok.gathering.entity.*; -import com.dokdok.gathering.repository.GatheringMemberRepository; -import com.dokdok.gathering.repository.GatheringRepository; -import com.dokdok.global.util.SecurityUtil; -import com.dokdok.meeting.entity.Meeting; -import com.dokdok.meeting.entity.MeetingStatus; -import com.dokdok.meeting.repository.MeetingRepository; -import com.dokdok.user.entity.User; -import com.dokdok.user.repository.UserRepository; -import jakarta.persistence.EntityManager; -import org.hibernate.Session; -import org.hibernate.stat.Statistics; -import org.junit.jupiter.api.*; -import org.mockito.MockedStatic; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; - -import static org.mockito.Mockito.mockStatic; - -@SpringBootTest -@ActiveProfiles("test") -@Transactional -@DisplayName("Gathering N+1 쿼리 테스트") -class GatheringServiceN1Test { - - @Autowired - private GatheringService gatheringService; - - @Autowired - private UserRepository userRepository; - - @Autowired - private GatheringRepository gatheringRepository; - - @Autowired - private GatheringMemberRepository gatheringMemberRepository; - - @Autowired - private MeetingRepository meetingRepository; - - @Autowired - private EntityManager entityManager; - - private MockedStatic securityUtilMock; - private User testUser; - private Statistics statistics; - - @BeforeEach - void setUp() { - // Hibernate Statistics 활성화 - Session session = entityManager.unwrap(Session.class); - statistics = session.getSessionFactory().getStatistics(); - statistics.setStatisticsEnabled(true); - - // SecurityUtil Mock - securityUtilMock = mockStatic(SecurityUtil.class); - - // 테스트 유저 생성 - testUser = userRepository.save(User.builder() - .kakaoId(12345L) - .nickname("테스트유저") - .profileImageUrl("test.jpg") - .build()); - - securityUtilMock.when(SecurityUtil::getCurrentUserId).thenReturn(testUser.getId()); - } - - @AfterEach - void tearDown() { - securityUtilMock.close(); - statistics.clear(); - } - - @Test - @DisplayName("getFavoriteGatherings - N+1 쿼리 발생 확인") - void getFavoriteGatherings_N1_Problem() { - // given : 즐겨찾기 모임 5개 생성 - int gatheringCount = 5; - createFavoriteGatherings(gatheringCount); - - // 영속성 컨텍스트 초기화 - entityManager.flush(); - entityManager.clear(); - statistics.clear(); - - // when - gatheringService.getFavoriteGatherings(); - - // then: 쿼리 수 확인 - long queryCount = statistics.getQueryExecutionCount(); - - System.out.println("========================================"); - System.out.println("즐겨찾기 모임 수: " + gatheringCount); - System.out.println("실행된 쿼리 수: " + queryCount); - System.out.println("예상 쿼리 수 (N+1 발생 시): " + (1 + gatheringCount * 2)); - System.out.println("예상 쿼리 수 (최적화 후): 3"); - System.out.println("========================================"); - - // N+1 발생 시: 1(목록조회) + N(멤버카운트) + N(미팅카운트) = 1 + 2N - // 최적화 후: 1(목록조회) + 1(멤버카운트) + 1(미팅카운트) = 3 - - // 현재 N+1 문제가 있으므로 쿼리가 많이 발생함을 확인 - System.out.println("N+1 문제 발생 여부: " + (queryCount > 3 ? "YES" : "NO")); - } - - @Test - @DisplayName("getMyGatherings - N+1 쿼리 발생 확인") - void getMyGatherings_N1_Problem() { - // given: 내 모임 10개 생성 - int gatheringCount = 10; - createMyGatherings(gatheringCount); - - entityManager.flush(); - entityManager.clear(); - statistics.clear(); - - // when - gatheringService.getMyGatherings(10, null, null); - - // then - long queryCount = statistics.getQueryExecutionCount(); - - System.out.println("========================================"); - System.out.println("내 모임 수: " + gatheringCount); - System.out.println("실행된 쿼리 수: " + queryCount); - System.out.println("예상 쿼리 수 (N+1 발생 시): " + (2 + gatheringCount * 2)); - System.out.println("예상 쿼리 수 (최적화 후): 4"); - System.out.println("========================================"); - - System.out.println("N+1 문제 발생 여부: " + (queryCount > 4 ? "YES" : "NO")); - } - - private void createFavoriteGatherings(int count) { - for (int i = 0; i < count; i++) { - Gathering gathering = gatheringRepository.save(Gathering.builder() - .gatheringName("모임" + i) - .description("설명" + i) - .gatheringStatus(GatheringStatus.ACTIVE) - .invitationLink("invite" + i) - .gatheringLeader(testUser) - .build()); - - gatheringMemberRepository.save(GatheringMember.builder() - .gathering(gathering) - .user(testUser) - .role(GatheringRole.LEADER) - .memberStatus(GatheringMemberStatus.ACTIVE) - .isFavorite(true) - .joinedAt(LocalDateTime.now()) - .build()); - - // 미팅도 몇 개 생성 - meetingRepository.save(Meeting.builder() - .gathering(gathering) - .meetingStatus(MeetingStatus.DONE) - .meetingStartDate(LocalDateTime.now().minusDays(1)) - .meetingEndDate(LocalDateTime.now()) - .build()); - } - } - - private void createMyGatherings(int count) { - for (int i = 0; i < count; i++) { - Gathering gathering = gatheringRepository.save(Gathering.builder() - .gatheringName("모임" + i) - .description("설명" + i) - .gatheringStatus(GatheringStatus.ACTIVE) - .invitationLink("invite" + i) - .gatheringLeader(testUser) - .build()); - - gatheringMemberRepository.save(GatheringMember.builder() - .gathering(gathering) - .user(testUser) - .role(GatheringRole.LEADER) - .memberStatus(GatheringMemberStatus.ACTIVE) - .isFavorite(false) - .joinedAt(LocalDateTime.now().minusDays(i)) - .build()); - - meetingRepository.save(Meeting.builder() - .gathering(gathering) - .meetingStatus(MeetingStatus.DONE) - .meetingStartDate(LocalDateTime.now().minusDays(1)) - .meetingEndDate(LocalDateTime.now()) - .build()); - } - } -} diff --git a/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java b/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java index a90066a..54fa17b 100644 --- a/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java +++ b/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java @@ -24,6 +24,7 @@ import com.dokdok.gathering.entity.GatheringStatus; import com.dokdok.gathering.exception.GatheringErrorCode; import com.dokdok.gathering.exception.GatheringException; +import com.dokdok.gathering.repository.GatheringCountProjection; import com.dokdok.gathering.repository.GatheringMemberRepository; import com.dokdok.gathering.repository.GatheringRepository; import com.dokdok.gathering.util.InvitationCodeGenerator; @@ -308,10 +309,16 @@ void getFavoriteGatherings_Success() { List favoriteMembers = List.of(member1, member2); given(gatheringMemberRepository.findFavoriteGatheringsByUserId(userId)).willReturn(favoriteMembers); - given(gatheringMemberRepository.countActiveMembersByStatus(1L)).willReturn(1); - given(gatheringMemberRepository.countActiveMembersByStatus(2L)).willReturn(1); - given(meetingRepository.countByGatheringIdAndMeetingStatus(1L, MeetingStatus.DONE)).willReturn(3); - given(meetingRepository.countByGatheringIdAndMeetingStatus(2L, MeetingStatus.DONE)).willReturn(5); + given(gatheringMemberRepository.countActiveMembersByGatheringIds(List.of(1L, 2L))) + .willReturn(List.of( + createCountProjection(1L, 1L), + createCountProjection(2L, 1L) + )); + given(meetingRepository.countByGatheringIdsAndStatus(List.of(1L, 2L), MeetingStatus.DONE)) + .willReturn(List.of( + createCountProjection(1L, 3L), + createCountProjection(2L, 5L) + )); // when FavoriteGatheringListResponse response = gatheringService.getFavoriteGatherings(); @@ -341,10 +348,17 @@ void getFavoriteGatherings_Success() { securityUtilMock.verify(SecurityUtil::getCurrentUserId, times(1)); verify(gatheringMemberRepository, times(1)).findFavoriteGatheringsByUserId(eq(userId)); - verify(gatheringMemberRepository, times(1)).countActiveMembersByStatus(1L); - verify(gatheringMemberRepository, times(1)).countActiveMembersByStatus(2L); - verify(meetingRepository, times(1)).countByGatheringIdAndMeetingStatus(1L, MeetingStatus.DONE); - verify(meetingRepository, times(1)).countByGatheringIdAndMeetingStatus(2L, MeetingStatus.DONE); + verify(gatheringMemberRepository, times(1)).countActiveMembersByGatheringIds(List.of(1L, 2L)); + verify(meetingRepository, times(1)).countByGatheringIdsAndStatus(List.of(1L, 2L), MeetingStatus.DONE); + } + + private GatheringCountProjection createCountProjection(Long gatheringId, Long count) { + return new GatheringCountProjection() { + @Override + public Long getGatheringId() { return gatheringId; } + @Override + public Long getCount() { return count; } + }; } @Test @@ -366,8 +380,8 @@ void getFavoriteGatherings_EmptyList() { securityUtilMock.verify(SecurityUtil::getCurrentUserId, times(1)); verify(gatheringMemberRepository, times(1)).findFavoriteGatheringsByUserId(eq(userId)); - verify(gatheringMemberRepository, times(0)).countActiveMembersByStatus(any()); - verify(meetingRepository, times(0)).countByGatheringIdAndMeetingStatus(any(), any()); + verify(gatheringMemberRepository, times(0)).countActiveMembersByGatheringIds(any()); + verify(meetingRepository, times(0)).countByGatheringIdsAndStatus(any(), any()); } @Test @@ -401,10 +415,17 @@ void getMyGatherings_Success_FirstPage() { given(gatheringMemberRepository.findMyGatheringsFirstPage(eq(userId), any(Pageable.class))) .willReturn(members); - given(gatheringMemberRepository.countActiveMembersByStatus(1L)).willReturn(1); - given(gatheringMemberRepository.countActiveMembersByStatus(2L)).willReturn(1); - given(meetingRepository.countByGatheringIdAndMeetingStatus(1L, MeetingStatus.DONE)).willReturn(3); - given(meetingRepository.countByGatheringIdAndMeetingStatus(2L, MeetingStatus.DONE)).willReturn(5); + given(gatheringMemberRepository.countMyGatherings(userId)).willReturn(2); + given(gatheringMemberRepository.countActiveMembersByGatheringIds(List.of(1L, 2L))) + .willReturn(List.of( + createCountProjection(1L, 1L), + createCountProjection(2L, 1L) + )); + given(meetingRepository.countByGatheringIdsAndStatus(List.of(1L, 2L), MeetingStatus.DONE)) + .willReturn(List.of( + createCountProjection(1L, 3L), + createCountProjection(2L, 5L) + )); // when CursorResponse response = @@ -418,6 +439,8 @@ void getMyGatherings_Success_FirstPage() { assertThat(response.nextCursor()).isNull(); verify(gatheringMemberRepository).findMyGatheringsFirstPage(eq(userId), any(Pageable.class)); + verify(gatheringMemberRepository).countActiveMembersByGatheringIds(List.of(1L, 2L)); + verify(meetingRepository).countByGatheringIdsAndStatus(List.of(1L, 2L), MeetingStatus.DONE); } @Test @@ -452,8 +475,12 @@ void getMyGatherings_Success_HasNextPage() { given(gatheringMemberRepository.findMyGatheringsFirstPage(eq(userId), any(Pageable.class))) .willReturn(members); - given(gatheringMemberRepository.countActiveMembersByStatus(any())).willReturn(1); - given(meetingRepository.countByGatheringIdAndMeetingStatus(any(), eq(MeetingStatus.DONE))).willReturn(0); + given(gatheringMemberRepository.countMyGatherings(userId)).willReturn(2); + // pageSize=1이므로 pageMembers는 member1만 포함, gatheringIds = List.of(1L) + given(gatheringMemberRepository.countActiveMembersByGatheringIds(List.of(1L))) + .willReturn(List.of(createCountProjection(1L, 1L))); + given(meetingRepository.countByGatheringIdsAndStatus(List.of(1L), MeetingStatus.DONE)) + .willReturn(List.of(createCountProjection(1L, 0L))); // when CursorResponse response = From 9fb6abf7048cad8fdb6bd6aa6c2beafaf1d16705 Mon Sep 17 00:00:00 2001 From: juhyun Date: Fri, 13 Feb 2026 15:50:58 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor=20:=20CI=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gathering/service/GatheringServiceTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java b/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java index 54fa17b..faf3958 100644 --- a/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java +++ b/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java @@ -309,7 +309,7 @@ void getFavoriteGatherings_Success() { List favoriteMembers = List.of(member1, member2); given(gatheringMemberRepository.findFavoriteGatheringsByUserId(userId)).willReturn(favoriteMembers); - given(gatheringMemberRepository.countActiveMembersByGatheringIds(List.of(1L, 2L))) + given(gatheringMemberRepository.countActiveMembersByGatherings(List.of(1L, 2L))) .willReturn(List.of( createCountProjection(1L, 1L), createCountProjection(2L, 1L) @@ -348,7 +348,7 @@ void getFavoriteGatherings_Success() { securityUtilMock.verify(SecurityUtil::getCurrentUserId, times(1)); verify(gatheringMemberRepository, times(1)).findFavoriteGatheringsByUserId(eq(userId)); - verify(gatheringMemberRepository, times(1)).countActiveMembersByGatheringIds(List.of(1L, 2L)); + verify(gatheringMemberRepository, times(1)).countActiveMembersByGatherings(List.of(1L, 2L)); verify(meetingRepository, times(1)).countByGatheringIdsAndStatus(List.of(1L, 2L), MeetingStatus.DONE); } @@ -380,7 +380,7 @@ void getFavoriteGatherings_EmptyList() { securityUtilMock.verify(SecurityUtil::getCurrentUserId, times(1)); verify(gatheringMemberRepository, times(1)).findFavoriteGatheringsByUserId(eq(userId)); - verify(gatheringMemberRepository, times(0)).countActiveMembersByGatheringIds(any()); + verify(gatheringMemberRepository, times(0)).countActiveMembersByGatherings(any()); verify(meetingRepository, times(0)).countByGatheringIdsAndStatus(any(), any()); } @@ -416,7 +416,7 @@ void getMyGatherings_Success_FirstPage() { given(gatheringMemberRepository.findMyGatheringsFirstPage(eq(userId), any(Pageable.class))) .willReturn(members); given(gatheringMemberRepository.countMyGatherings(userId)).willReturn(2); - given(gatheringMemberRepository.countActiveMembersByGatheringIds(List.of(1L, 2L))) + given(gatheringMemberRepository.countActiveMembersByGatherings(List.of(1L, 2L))) .willReturn(List.of( createCountProjection(1L, 1L), createCountProjection(2L, 1L) @@ -439,7 +439,7 @@ void getMyGatherings_Success_FirstPage() { assertThat(response.nextCursor()).isNull(); verify(gatheringMemberRepository).findMyGatheringsFirstPage(eq(userId), any(Pageable.class)); - verify(gatheringMemberRepository).countActiveMembersByGatheringIds(List.of(1L, 2L)); + verify(gatheringMemberRepository).countActiveMembersByGatherings(List.of(1L, 2L)); verify(meetingRepository).countByGatheringIdsAndStatus(List.of(1L, 2L), MeetingStatus.DONE); } @@ -477,7 +477,7 @@ void getMyGatherings_Success_HasNextPage() { .willReturn(members); given(gatheringMemberRepository.countMyGatherings(userId)).willReturn(2); // pageSize=1이므로 pageMembers는 member1만 포함, gatheringIds = List.of(1L) - given(gatheringMemberRepository.countActiveMembersByGatheringIds(List.of(1L))) + given(gatheringMemberRepository.countActiveMembersByGatherings(List.of(1L))) .willReturn(List.of(createCountProjection(1L, 1L))); given(meetingRepository.countByGatheringIdsAndStatus(List.of(1L), MeetingStatus.DONE)) .willReturn(List.of(createCountProjection(1L, 0L)));