diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/repository/MembershipRepository.java b/src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/repository/MembershipRepository.java index bf6c5d0b..b92d8999 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/repository/MembershipRepository.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/repository/MembershipRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.tuna.zoopzoop.backend.domain.member.entity.Member; import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership; import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority; @@ -45,4 +46,17 @@ public interface MembershipRepository extends JpaRepository where m.member.id = :memberId and m.space.id = :spaceId """) Optional findByMemberIdAndSpaceId(Integer memberId, Integer spaceId); + + @Query("SELECT m FROM Membership m JOIN FETCH m.space WHERE m.member = :member ORDER BY m.id ASC") + Page findAllByMemberWithSpace(@Param("member") Member member, Pageable pageable); + + @Query("SELECT m FROM Membership m JOIN FETCH m.space WHERE m.member = :member AND m.authority = :authority ORDER BY m.id ASC") + Page findAllByMemberAndAuthorityWithSpace(@Param("member") Member member, @Param("authority") Authority authority, Pageable pageable); + + @Query("SELECT m FROM Membership m JOIN FETCH m.space WHERE m.member = :member AND m.authority <> :authority ORDER BY m.id ASC") + Page findAllByMemberAndAuthorityIsNotWithSpace(@Param("member") Member member, @Param("authority") Authority authority, Pageable pageable); + + // 여러 Space에 속한 Member 목록 한번에 조회 (JOIN FETCH로 Member 정보까지) + @Query("SELECT m FROM Membership m JOIN FETCH m.member WHERE m.space IN :spaces AND m.authority <> org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority.PENDING") + List findAllMembersInSpaces(@Param("spaces") List spaces); } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipService.java index cece27cc..733979ee 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipService.java @@ -12,6 +12,7 @@ import org.tuna.zoopzoop.backend.domain.SSE.service.EmitterService; import org.tuna.zoopzoop.backend.domain.member.entity.Member; import org.tuna.zoopzoop.backend.domain.member.service.MemberService; +import org.tuna.zoopzoop.backend.domain.space.membership.dto.etc.SpaceMemberInfo; import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership; import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority; import org.tuna.zoopzoop.backend.domain.space.membership.enums.JoinState; @@ -21,8 +22,11 @@ import org.tuna.zoopzoop.backend.global.rsData.RsData; import java.nio.file.AccessDeniedException; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -64,11 +68,11 @@ public Membership findByMemberAndSpace(Member member, Space space) { */ public Page findByMember(Member member, String state, Pageable pageable) { if (state.equalsIgnoreCase("PENDING")) { - return membershipRepository.findAllByMemberAndAuthorityOrderById(member, Authority.PENDING, pageable); + return membershipRepository.findAllByMemberAndAuthorityWithSpace(member, Authority.PENDING, pageable); } else if (state.equalsIgnoreCase("JOINED")) { - return membershipRepository.findAllByMemberAndAuthorityIsNotOrderById(member, Authority.PENDING, pageable); + return membershipRepository.findAllByMemberAndAuthorityIsNotWithSpace(member, Authority.PENDING, pageable); } else { - return membershipRepository.findAllByMemberOrderById(member, pageable); + return membershipRepository.findAllByMemberWithSpace(member, pageable); } } @@ -107,6 +111,36 @@ public List findMembersBySpace(Space space) { return membershipRepository.findAllBySpaceAndAuthorityIsNotOrderById(space, Authority.PENDING); } + /** + * 여러 스페이스에 속한 멤버 목록을 한 번의 쿼리로 조회 (N+1 문제 해결용) + * @param spaces 조회할 스페이스 목록 + * @return 스페이스 ID를 key로, 해당 스페이스의 멤버 정보 리스트를 value로 갖는 Map + */ + @Transactional(readOnly = true) + public Map> findMembersBySpaces(List spaces) { + if (spaces == null || spaces.isEmpty()) { + return Collections.emptyMap(); + } + + // 1. 한 번의 쿼리로 모든 스페이스의 멤버십 정보를 가져옴 + List allMemberships = membershipRepository.findAllMembersInSpaces(spaces); + + // 2. Space ID 별로 그룹핑하여 Map으로 변환 + return allMemberships.stream() + .collect(Collectors.groupingBy( + membership -> membership.getSpace().getId(), // Key: Space ID + Collectors.mapping( // Value: List DTO로 변환 + membership -> new SpaceMemberInfo( + membership.getMember().getId(), + membership.getMember().getName(), + membership.getMember().getProfileImageUrl(), + membership.getAuthority() + ), + Collectors.toList() + ) + )); + } + // ======================== 권한 조회 ======================== // /** * 멤버가 스페이스의 어드민 권한을 가지고 있는지 확인 diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java index ea8b286c..5be227f7 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java @@ -32,7 +32,9 @@ import org.tuna.zoopzoop.backend.global.security.jwt.CustomUserDetails; import java.nio.file.AccessDeniedException; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @RestController @@ -144,23 +146,28 @@ public RsData getAllSpaces( String stateStr = (state == null) ? "ALL" : state.name(); Page membershipsPage = membershipService.findByMember(member, stateStr, pageable); + Map> membersBySpaceId = Collections.emptyMap(); + if (includeMembers) { + // 현재 페이지에 포함된 Space 엔티티 목록 추출 + List spacesOnPage = membershipsPage.getContent().stream() + .map(Membership::getSpace) + .distinct() + .collect(Collectors.toList()); + // 멤버 정보를 한 번의 쿼리로 조회 + membersBySpaceId = membershipService.findMembersBySpaces(spacesOnPage); + } + // Page를 Page로 변환 + final Map> finalMembersMap = membersBySpaceId; + Page spaceInfosPage = membershipsPage.map(membership -> { Space space = membership.getSpace(); - List memberInfos = null; - - if (includeMembers) { - // 스페이스에 속한 멤버 목록 조회 (가입 상태만) - List spaceMemberships = membershipService.findMembersBySpace(space); - // 멤버 목록을 DTO로 변환 - memberInfos = spaceMemberships.stream() - .map(spaceMembership -> new SpaceMemberInfo( - spaceMembership.getMember().getId(), - spaceMembership.getMember().getName(), - spaceMembership.getMember().getProfileImageUrl(), - spaceMembership.getAuthority() - )) - .collect(Collectors.toList()); + + // Map에서 spaceId로 멤버 목록을 O(1)에 조회 (기존 N+1 유발 코드 대체) + List memberInfos = finalMembersMap.get(space.getId()); + + if (includeMembers && memberInfos == null) { + memberInfos = Collections.emptyList(); } return new SpaceInfo( @@ -168,11 +175,10 @@ public RsData getAllSpaces( space.getName(), space.getThumbnailUrl(), membership.getAuthority(), - memberInfos // 조회된 멤버 목록 (null일 수도 있음) + memberInfos ); }); - // 새로운 응답 DTO 생성 ResBodyForSpaceListPage resBody = new ResBodyForSpaceListPage(spaceInfosPage); return new RsData<>(