Skip to content

Commit 769617a

Browse files
EpicFnEpicFn
andauthored
[Refactor/OPS-380] 스페이스 목록 조회 개선 (#156)
* refactor : membership 조회 시 lazy loading에 의한 n+1 문제 개선 * refactor : space의 member 목록 조회 시 n+ 1 문제 해결 --------- Co-authored-by: EpicFn <[email protected]>
1 parent 263ce6c commit 769617a

File tree

3 files changed

+73
-19
lines changed

3 files changed

+73
-19
lines changed

src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/repository/MembershipRepository.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.springframework.data.domain.Pageable;
55
import org.springframework.data.jpa.repository.JpaRepository;
66
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
78
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
89
import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership;
910
import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority;
@@ -45,4 +46,17 @@ public interface MembershipRepository extends JpaRepository<Membership, Integer>
4546
where m.member.id = :memberId and m.space.id = :spaceId
4647
""")
4748
Optional<Membership> findByMemberIdAndSpaceId(Integer memberId, Integer spaceId);
49+
50+
@Query("SELECT m FROM Membership m JOIN FETCH m.space WHERE m.member = :member ORDER BY m.id ASC")
51+
Page<Membership> findAllByMemberWithSpace(@Param("member") Member member, Pageable pageable);
52+
53+
@Query("SELECT m FROM Membership m JOIN FETCH m.space WHERE m.member = :member AND m.authority = :authority ORDER BY m.id ASC")
54+
Page<Membership> findAllByMemberAndAuthorityWithSpace(@Param("member") Member member, @Param("authority") Authority authority, Pageable pageable);
55+
56+
@Query("SELECT m FROM Membership m JOIN FETCH m.space WHERE m.member = :member AND m.authority <> :authority ORDER BY m.id ASC")
57+
Page<Membership> findAllByMemberAndAuthorityIsNotWithSpace(@Param("member") Member member, @Param("authority") Authority authority, Pageable pageable);
58+
59+
// 여러 Space에 속한 Member 목록 한번에 조회 (JOIN FETCH로 Member 정보까지)
60+
@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")
61+
List<Membership> findAllMembersInSpaces(@Param("spaces") List<Space> spaces);
4862
}

src/main/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipService.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.tuna.zoopzoop.backend.domain.SSE.service.EmitterService;
1313
import org.tuna.zoopzoop.backend.domain.member.entity.Member;
1414
import org.tuna.zoopzoop.backend.domain.member.service.MemberService;
15+
import org.tuna.zoopzoop.backend.domain.space.membership.dto.etc.SpaceMemberInfo;
1516
import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership;
1617
import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority;
1718
import org.tuna.zoopzoop.backend.domain.space.membership.enums.JoinState;
@@ -21,8 +22,11 @@
2122
import org.tuna.zoopzoop.backend.global.rsData.RsData;
2223

2324
import java.nio.file.AccessDeniedException;
25+
import java.util.Collections;
2426
import java.util.List;
27+
import java.util.Map;
2528
import java.util.Optional;
29+
import java.util.stream.Collectors;
2630

2731
@Service
2832
@RequiredArgsConstructor
@@ -64,11 +68,11 @@ public Membership findByMemberAndSpace(Member member, Space space) {
6468
*/
6569
public Page<Membership> findByMember(Member member, String state, Pageable pageable) {
6670
if (state.equalsIgnoreCase("PENDING")) {
67-
return membershipRepository.findAllByMemberAndAuthorityOrderById(member, Authority.PENDING, pageable);
71+
return membershipRepository.findAllByMemberAndAuthorityWithSpace(member, Authority.PENDING, pageable);
6872
} else if (state.equalsIgnoreCase("JOINED")) {
69-
return membershipRepository.findAllByMemberAndAuthorityIsNotOrderById(member, Authority.PENDING, pageable);
73+
return membershipRepository.findAllByMemberAndAuthorityIsNotWithSpace(member, Authority.PENDING, pageable);
7074
} else {
71-
return membershipRepository.findAllByMemberOrderById(member, pageable);
75+
return membershipRepository.findAllByMemberWithSpace(member, pageable);
7276
}
7377
}
7478

@@ -107,6 +111,36 @@ public List<Membership> findMembersBySpace(Space space) {
107111
return membershipRepository.findAllBySpaceAndAuthorityIsNotOrderById(space, Authority.PENDING);
108112
}
109113

114+
/**
115+
* 여러 스페이스에 속한 멤버 목록을 한 번의 쿼리로 조회 (N+1 문제 해결용)
116+
* @param spaces 조회할 스페이스 목록
117+
* @return 스페이스 ID를 key로, 해당 스페이스의 멤버 정보 리스트를 value로 갖는 Map
118+
*/
119+
@Transactional(readOnly = true)
120+
public Map<Integer, List<SpaceMemberInfo>> findMembersBySpaces(List<Space> spaces) {
121+
if (spaces == null || spaces.isEmpty()) {
122+
return Collections.emptyMap();
123+
}
124+
125+
// 1. 한 번의 쿼리로 모든 스페이스의 멤버십 정보를 가져옴
126+
List<Membership> allMemberships = membershipRepository.findAllMembersInSpaces(spaces);
127+
128+
// 2. Space ID 별로 그룹핑하여 Map으로 변환
129+
return allMemberships.stream()
130+
.collect(Collectors.groupingBy(
131+
membership -> membership.getSpace().getId(), // Key: Space ID
132+
Collectors.mapping( // Value: List<SpaceMemberInfo> DTO로 변환
133+
membership -> new SpaceMemberInfo(
134+
membership.getMember().getId(),
135+
membership.getMember().getName(),
136+
membership.getMember().getProfileImageUrl(),
137+
membership.getAuthority()
138+
),
139+
Collectors.toList()
140+
)
141+
));
142+
}
143+
110144
// ======================== 권한 조회 ======================== //
111145
/**
112146
* 멤버가 스페이스의 어드민 권한을 가지고 있는지 확인

src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
import org.tuna.zoopzoop.backend.global.security.jwt.CustomUserDetails;
3333

3434
import java.nio.file.AccessDeniedException;
35+
import java.util.Collections;
3536
import java.util.List;
37+
import java.util.Map;
3638
import java.util.stream.Collectors;
3739

3840
@RestController
@@ -144,35 +146,39 @@ public RsData<ResBodyForSpaceListPage> getAllSpaces(
144146
String stateStr = (state == null) ? "ALL" : state.name();
145147
Page<Membership> membershipsPage = membershipService.findByMember(member, stateStr, pageable);
146148

149+
Map<Integer, List<SpaceMemberInfo>> membersBySpaceId = Collections.emptyMap();
150+
if (includeMembers) {
151+
// 현재 페이지에 포함된 Space 엔티티 목록 추출
152+
List<Space> spacesOnPage = membershipsPage.getContent().stream()
153+
.map(Membership::getSpace)
154+
.distinct()
155+
.collect(Collectors.toList());
156+
// 멤버 정보를 한 번의 쿼리로 조회
157+
membersBySpaceId = membershipService.findMembersBySpaces(spacesOnPage);
158+
}
159+
147160
// Page<Membership>를 Page<SpaceMembershipInfo>로 변환
161+
final Map<Integer, List<SpaceMemberInfo>> finalMembersMap = membersBySpaceId;
162+
148163
Page<SpaceInfo> spaceInfosPage = membershipsPage.map(membership -> {
149164
Space space = membership.getSpace();
150-
List<SpaceMemberInfo> memberInfos = null;
151-
152-
if (includeMembers) {
153-
// 스페이스에 속한 멤버 목록 조회 (가입 상태만)
154-
List<Membership> spaceMemberships = membershipService.findMembersBySpace(space);
155-
// 멤버 목록을 DTO로 변환
156-
memberInfos = spaceMemberships.stream()
157-
.map(spaceMembership -> new SpaceMemberInfo(
158-
spaceMembership.getMember().getId(),
159-
spaceMembership.getMember().getName(),
160-
spaceMembership.getMember().getProfileImageUrl(),
161-
spaceMembership.getAuthority()
162-
))
163-
.collect(Collectors.toList());
165+
166+
// Map에서 spaceId로 멤버 목록을 O(1)에 조회 (기존 N+1 유발 코드 대체)
167+
List<SpaceMemberInfo> memberInfos = finalMembersMap.get(space.getId());
168+
169+
if (includeMembers && memberInfos == null) {
170+
memberInfos = Collections.emptyList();
164171
}
165172

166173
return new SpaceInfo(
167174
space.getId(),
168175
space.getName(),
169176
space.getThumbnailUrl(),
170177
membership.getAuthority(),
171-
memberInfos // 조회된 멤버 목록 (null일 수도 있음)
178+
memberInfos
172179
);
173180
});
174181

175-
// 새로운 응답 DTO 생성
176182
ResBodyForSpaceListPage resBody = new ResBodyForSpaceListPage(spaceInfosPage);
177183

178184
return new RsData<>(

0 commit comments

Comments
 (0)