diff --git a/src/main/java/com/back/domain/studyroom/controller/RoomController.java b/src/main/java/com/back/domain/studyroom/controller/RoomController.java index d14a70ea..fcf58e7e 100644 --- a/src/main/java/com/back/domain/studyroom/controller/RoomController.java +++ b/src/main/java/com/back/domain/studyroom/controller/RoomController.java @@ -64,7 +64,7 @@ public ResponseEntity> createRoom( currentUserId ); - RoomResponse response = RoomResponse.from(room); + RoomResponse response = roomService.toRoomResponse(room); return ResponseEntity .status(HttpStatus.CREATED) @@ -139,9 +139,7 @@ public ResponseEntity>> getRooms( Pageable pageable = PageRequest.of(page, size); Page rooms = roomService.getJoinableRooms(pageable); - List roomList = rooms.getContent().stream() - .map(RoomResponse::from) - .collect(Collectors.toList()); + List roomList = roomService.toRoomResponseList(rooms.getContent()); Map response = new HashMap<>(); response.put("rooms", roomList); @@ -175,11 +173,7 @@ public ResponseEntity> getRoomDetail( Room room = roomService.getRoomDetail(roomId, currentUserId); List members = roomService.getRoomMembers(roomId, currentUserId); - List memberResponses = members.stream() - .map(RoomMemberResponse::from) - .collect(Collectors.toList()); - - RoomDetailResponse response = RoomDetailResponse.of(room, memberResponses); + RoomDetailResponse response = roomService.toRoomDetailResponse(room, members); return ResponseEntity .status(HttpStatus.OK) @@ -201,12 +195,7 @@ public ResponseEntity>> getMyRooms() { List rooms = roomService.getUserRooms(currentUserId); - List roomList = rooms.stream() - .map(room -> MyRoomResponse.of( - room, - roomService.getUserRoomRole(room.getId(), currentUserId) - )) - .collect(Collectors.toList()); + List roomList = roomService.toMyRoomResponseList(rooms, currentUserId); return ResponseEntity .status(HttpStatus.OK) @@ -313,9 +302,7 @@ public ResponseEntity>> getPopularRooms( Pageable pageable = PageRequest.of(page, size); Page rooms = roomService.getPopularRooms(pageable); - List roomList = rooms.getContent().stream() - .map(RoomResponse::from) - .collect(Collectors.toList()); + List roomList = roomService.toRoomResponseList(rooms.getContent()); Map response = new HashMap<>(); response.put("rooms", roomList); diff --git a/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java b/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java index 78b90d55..7a5c710d 100644 --- a/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java +++ b/src/main/java/com/back/domain/studyroom/dto/MyRoomResponse.java @@ -20,12 +20,12 @@ public class MyRoomResponse { private RoomRole myRole; private LocalDateTime createdAt; - public static MyRoomResponse of(Room room, RoomRole myRole) { + public static MyRoomResponse of(Room room, long currentParticipants, RoomRole myRole) { return MyRoomResponse.builder() .roomId(room.getId()) .title(room.getTitle()) .description(room.getDescription() != null ? room.getDescription() : "") - .currentParticipants(room.getCurrentParticipants()) + .currentParticipants((int) currentParticipants) // Redis에서 조회한 실시간 값 .maxParticipants(room.getMaxParticipants()) .status(room.getStatus()) .myRole(myRole) diff --git a/src/main/java/com/back/domain/studyroom/dto/RoomDetailResponse.java b/src/main/java/com/back/domain/studyroom/dto/RoomDetailResponse.java index f282d46e..803d99f8 100644 --- a/src/main/java/com/back/domain/studyroom/dto/RoomDetailResponse.java +++ b/src/main/java/com/back/domain/studyroom/dto/RoomDetailResponse.java @@ -25,14 +25,14 @@ public class RoomDetailResponse { private LocalDateTime createdAt; private List members; - public static RoomDetailResponse of(Room room, List members) { + public static RoomDetailResponse of(Room room, long currentParticipants, List members) { return RoomDetailResponse.builder() .roomId(room.getId()) .title(room.getTitle()) .description(room.getDescription() != null ? room.getDescription() : "") .isPrivate(room.isPrivate()) .maxParticipants(room.getMaxParticipants()) - .currentParticipants(room.getCurrentParticipants()) + .currentParticipants((int) currentParticipants) // Redis에서 조회한 실시간 값 .status(room.getStatus()) .allowCamera(room.isAllowCamera()) .allowAudio(room.isAllowAudio()) diff --git a/src/main/java/com/back/domain/studyroom/dto/RoomResponse.java b/src/main/java/com/back/domain/studyroom/dto/RoomResponse.java index 905a35ad..bd3960fb 100644 --- a/src/main/java/com/back/domain/studyroom/dto/RoomResponse.java +++ b/src/main/java/com/back/domain/studyroom/dto/RoomResponse.java @@ -19,12 +19,12 @@ public class RoomResponse { private String createdBy; private LocalDateTime createdAt; - public static RoomResponse from(Room room) { + public static RoomResponse from(Room room, long currentParticipants) { return RoomResponse.builder() .roomId(room.getId()) .title(room.getTitle()) .description(room.getDescription() != null ? room.getDescription() : "") - .currentParticipants(room.getCurrentParticipants()) + .currentParticipants((int) currentParticipants) // Redis에서 조회한 실시간 값 .maxParticipants(room.getMaxParticipants()) .status(room.getStatus()) .createdBy(room.getCreatedBy().getNickname()) diff --git a/src/main/java/com/back/domain/studyroom/service/RoomService.java b/src/main/java/com/back/domain/studyroom/service/RoomService.java index b6cfb956..21d25907 100644 --- a/src/main/java/com/back/domain/studyroom/service/RoomService.java +++ b/src/main/java/com/back/domain/studyroom/service/RoomService.java @@ -40,6 +40,7 @@ public class RoomService { private final RoomMemberRepository roomMemberRepository; private final UserRepository userRepository; private final StudyRoomProperties properties; + private final com.back.global.websocket.service.WebSocketSessionManager sessionManager; /** * 방 생성 메서드 @@ -67,7 +68,7 @@ public Room createRoom(String title, String description, boolean isPrivate, RoomMember hostMember = RoomMember.createHost(savedRoom, creator); roomMemberRepository.save(hostMember); - savedRoom.incrementParticipant(); + // savedRoom.incrementParticipant(); // Redis로 이관 - DB 업데이트 제거 log.info("방 생성 완료 - RoomId: {}, Title: {}, CreatorId: {}", savedRoom.getId(), title, creatorId); @@ -125,6 +126,7 @@ public RoomMember joinRoom(Long roomId, String password, Long userId) { RoomMember member = existingMember.get(); // TODO: Redis에서 온라인 여부 확인하도록 변경 // 현재는 기존 멤버 재입장 허용 + // room.incrementParticipant(); // Redis로 이관 - DB 업데이트 제거 room.incrementParticipant(); return member; } @@ -132,7 +134,7 @@ public RoomMember joinRoom(Long roomId, String password, Long userId) { RoomMember newMember = RoomMember.createVisitor(room, user); RoomMember savedMember = roomMemberRepository.save(newMember); - room.incrementParticipant(); + // room.incrementParticipant(); // Redis로 이관 - DB 업데이트 제거 log.info("방 입장 완료 - RoomId: {}, UserId: {}, Role: {}", roomId, userId, newMember.getRole()); @@ -166,7 +168,7 @@ public void leaveRoom(Long roomId, Long userId) { handleHostLeaving(room, member); } else { // TODO: Redis에서 제거하도록 변경 - room.decrementParticipant(); + // room.decrementParticipant(); // Redis로 이관 - DB 업데이트 제거 } log.info("방 퇴장 완료 - RoomId: {}, UserId: {}", roomId, userId); @@ -183,7 +185,7 @@ private void handleHostLeaving(Room room, RoomMember hostMember) { if (otherOnlineMembers.isEmpty()) { room.terminate(); // TODO: Redis에서 제거하도록 변경 - room.decrementParticipant(); + // room.decrementParticipant(); // Redis로 이관 - DB 업데이트 제거 } else { RoomMember newHost = otherOnlineMembers.stream() .filter(m -> m.getRole() == RoomRole.SUB_HOST) @@ -195,7 +197,7 @@ private void handleHostLeaving(Room room, RoomMember hostMember) { if (newHost != null) { newHost.updateRole(RoomRole.HOST); // TODO: Redis에서 제거하도록 변경 - room.decrementParticipant(); + // room.decrementParticipant(); // Redis로 이관 - DB 업데이트 제거 log.info("새 방장 지정 - RoomId: {}, NewHostId: {}", room.getId(), newHost.getUser().getId()); @@ -339,9 +341,81 @@ public void kickMember(Long roomId, Long targetUserId, Long requesterId) { Room room = roomRepository.findById(roomId) .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); - room.decrementParticipant(); + // room.decrementParticipant(); // Redis로 이관 - DB 업데이트 제거 log.info("멤버 추방 완료 - RoomId: {}, TargetUserId: {}, RequesterId: {}", roomId, targetUserId, requesterId); } + + // ==================== DTO 생성 헬퍼 메서드 ==================== + + /** + * RoomResponse 생성 (Redis에서 실시간 참가자 수 조회) + */ + public com.back.domain.studyroom.dto.RoomResponse toRoomResponse(Room room) { + long onlineCount = sessionManager.getRoomOnlineUserCount(room.getId()); + return com.back.domain.studyroom.dto.RoomResponse.from(room, onlineCount); + } + + /** + * RoomResponse 리스트 생성 (일괄 조회로 N+1 방지) + */ + public java.util.List toRoomResponseList(java.util.List rooms) { + java.util.List roomIds = rooms.stream() + .map(Room::getId) + .collect(java.util.stream.Collectors.toList()); + + java.util.Map participantCounts = sessionManager.getBulkRoomOnlineUserCounts(roomIds); + + return rooms.stream() + .map(room -> com.back.domain.studyroom.dto.RoomResponse.from( + room, + participantCounts.getOrDefault(room.getId(), 0L) + )) + .collect(java.util.stream.Collectors.toList()); + } + + /** + * RoomDetailResponse 생성 (Redis에서 실시간 참가자 수 조회) + */ + public com.back.domain.studyroom.dto.RoomDetailResponse toRoomDetailResponse( + Room room, + java.util.List members) { + long onlineCount = sessionManager.getRoomOnlineUserCount(room.getId()); + + java.util.List memberResponses = members.stream() + .map(com.back.domain.studyroom.dto.RoomMemberResponse::from) + .collect(java.util.stream.Collectors.toList()); + + return com.back.domain.studyroom.dto.RoomDetailResponse.of(room, onlineCount, memberResponses); + } + + /** + * MyRoomResponse 생성 (Redis에서 실시간 참가자 수 조회) + */ + public com.back.domain.studyroom.dto.MyRoomResponse toMyRoomResponse(Room room, RoomRole myRole) { + long onlineCount = sessionManager.getRoomOnlineUserCount(room.getId()); + return com.back.domain.studyroom.dto.MyRoomResponse.of(room, onlineCount, myRole); + } + + /** + * MyRoomResponse 리스트 생성 (일괄 조회로 N+1 방지) + */ + public java.util.List toMyRoomResponseList( + java.util.List rooms, + Long userId) { + java.util.List roomIds = rooms.stream() + .map(Room::getId) + .collect(java.util.stream.Collectors.toList()); + + java.util.Map participantCounts = sessionManager.getBulkRoomOnlineUserCounts(roomIds); + + return rooms.stream() + .map(room -> { + RoomRole role = getUserRoomRole(room.getId(), userId); + long count = participantCounts.getOrDefault(room.getId(), 0L); + return com.back.domain.studyroom.dto.MyRoomResponse.of(room, count, role); + }) + .collect(java.util.stream.Collectors.toList()); + } } diff --git a/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java b/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java index 9a29f2fb..92cd4818 100644 --- a/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java +++ b/src/main/java/com/back/global/websocket/service/WebSocketSessionManager.java @@ -84,4 +84,13 @@ public Long getUserCurrentRoomId(Long userId) { public boolean isUserInRoom(Long userId, Long roomId) { return roomParticipantService.isUserInRoom(userId, roomId); } + + // 여러 방의 온라인 사용자 수 일괄 조회 (N+1 방지) + public java.util.Map getBulkRoomOnlineUserCounts(java.util.List roomIds) { + return roomIds.stream() + .collect(java.util.stream.Collectors.toMap( + roomId -> roomId, + this::getRoomOnlineUserCount + )); + } } \ No newline at end of file diff --git a/src/test/java/com/back/domain/studyroom/controller/RoomControllerTest.java b/src/test/java/com/back/domain/studyroom/controller/RoomControllerTest.java index e1be5d07..63fc1475 100644 --- a/src/test/java/com/back/domain/studyroom/controller/RoomControllerTest.java +++ b/src/test/java/com/back/domain/studyroom/controller/RoomControllerTest.java @@ -105,9 +105,13 @@ void createRoom() { anyInt(), eq(1L) )).willReturn(testRoom); + + RoomResponse roomResponse = RoomResponse.from(testRoom, 1); + given(roomService.toRoomResponse(any(Room.class))).willReturn(roomResponse); // when ResponseEntity> response = roomController.createRoom(request); + // then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody()).isNotNull(); @@ -123,6 +127,7 @@ void createRoom() { anyInt(), eq(1L) ); + verify(roomService, times(1)).toRoomResponse(any(Room.class)); } @Test @@ -174,6 +179,9 @@ void getRooms() { 1 ); given(roomService.getJoinableRooms(any())).willReturn(roomPage); + + List roomResponses = Arrays.asList(RoomResponse.from(testRoom, 1)); + given(roomService.toRoomResponseList(anyList())).willReturn(roomResponses); // when ResponseEntity>> response = roomController.getRooms(0, 20); @@ -185,6 +193,7 @@ void getRooms() { assertThat(response.getBody().getData().get("rooms")).isNotNull(); verify(roomService, times(1)).getJoinableRooms(any()); + verify(roomService, times(1)).toRoomResponseList(anyList()); } @Test @@ -195,6 +204,13 @@ void getRoomDetail() { given(roomService.getRoomDetail(eq(1L), eq(1L))).willReturn(testRoom); given(roomService.getRoomMembers(eq(1L), eq(1L))).willReturn(Arrays.asList(testMember)); + + RoomDetailResponse roomDetailResponse = RoomDetailResponse.of( + testRoom, + 1, + Arrays.asList(RoomMemberResponse.from(testMember)) + ); + given(roomService.toRoomDetailResponse(any(Room.class), anyList())).willReturn(roomDetailResponse); // when ResponseEntity> response = roomController.getRoomDetail(1L); @@ -208,6 +224,7 @@ void getRoomDetail() { verify(currentUser, times(1)).getUserId(); verify(roomService, times(1)).getRoomDetail(eq(1L), eq(1L)); verify(roomService, times(1)).getRoomMembers(eq(1L), eq(1L)); + verify(roomService, times(1)).toRoomDetailResponse(any(Room.class), anyList()); } @Test @@ -226,7 +243,11 @@ void getMyRooms() { } given(roomService.getUserRooms(eq(1L))).willReturn(Arrays.asList(testRoom)); - given(roomService.getUserRoomRole(eq(1L), eq(1L))).willReturn(RoomRole.HOST); + + List myRoomResponses = Arrays.asList( + MyRoomResponse.of(testRoom, 1, RoomRole.HOST) + ); + given(roomService.toMyRoomResponseList(anyList(), eq(1L))).willReturn(myRoomResponses); // when ResponseEntity>> response = roomController.getMyRooms(); @@ -240,6 +261,7 @@ void getMyRooms() { verify(currentUser, times(1)).getUserId(); verify(roomService, times(1)).getUserRooms(eq(1L)); + verify(roomService, times(1)).toMyRoomResponseList(anyList(), eq(1L)); } @Test @@ -328,6 +350,9 @@ void getPopularRooms() { 1 ); given(roomService.getPopularRooms(any())).willReturn(roomPage); + + List roomResponses = Arrays.asList(RoomResponse.from(testRoom, 1)); + given(roomService.toRoomResponseList(anyList())).willReturn(roomResponses); // when ResponseEntity>> response = roomController.getPopularRooms(0, 20); @@ -339,5 +364,6 @@ void getPopularRooms() { assertThat(response.getBody().getData().get("rooms")).isNotNull(); verify(roomService, times(1)).getPopularRooms(any()); + verify(roomService, times(1)).toRoomResponseList(anyList()); } }