Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/main/java/com/sajang/devracebackend/domain/Room.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ public class Room extends BaseEntity implements Serializable{
@JoinColumn(name = "problem_id")
private Problem problem;

// 밑처럼 과도한 N+1 문제를 유발할 서비스 로직이 없을뿐더러 활용 빈도수도 낮기때문에, fetch join이나 @EntityGraph를 사용하지 않은채로 지연 로딩을 유지하도록 했음.
// 과도한 N+1 예시: Rooms를 JPA의 findAll()로 호출하고, 각 Room을 순회하며 room.getUserRoomList.get내부속성()으로 접근하는 경우.
@OneToMany(mappedBy = "room") // Room-UserRoom 양방향매핑 (읽기 전용 필드)
private List<UserRoom> userRoomList = new ArrayList<>(); // 방의 입장인원 전원 퇴장여부 확인용도에 활용.
private List<UserRoom> userRoomList = new ArrayList<>(); // '방의 입장인원 전원 퇴장여부 확인 용도'에 활용.


@Builder(builderClassName = "RoomSaveBuilder", builderMethodName = "RoomSaveBuilder")
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/sajang/devracebackend/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ public class User extends BaseEntity implements Serializable {
@Column(name = "refresh_token")
private String refreshToken;

// 밑처럼 과도한 N+1 문제를 유발할 서비스 로직이 없을뿐더러 활용 빈도수도 낮기때문에, fetch join이나 @EntityGraph를 사용하지 않은채로 지연 로딩을 유지하도록 했음.
// 과도한 N+1 예시: Users를 JPA의 findAll()로 호출하고, 각 User를 순회하며 user.getUserRoomList.get내부속성()으로 접근하는 경우.
@OneToMany(mappedBy = "user") // User-UserRoom 양방향매핑 (읽기 전용 필드)
private List<UserRoom> userRoomList = new ArrayList<>(); // 사용자의 참여중인 방 찾는 용도에 활용.
private List<UserRoom> userRoomList = new ArrayList<>(); // '사용자의 참여중인 방 찾는 용도 & 회원탈퇴 용도'에 활용.


@Builder(builderClassName = "UserSaveBuilder", builderMethodName = "UserSaveBuilder")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class UserRoom extends BaseEntity implements Serializable {
@JoinColumn(name = "user_id")
private User user;

@ManyToOne(fetch = FetchType.LAZY) // Room-UserRoom 양방향매핑
@ManyToOne(fetch = FetchType.LAZY) // Room-UserRoom 양방향매핑 (default로 Lazy를 적용하되, Eager가 필요할경우 따로 메소드를 만들어 사용함.)
@JoinColumn(name = "room_id")
private Room room;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@
import com.sajang.devracebackend.domain.mapping.UserRoom;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRoomRepository extends JpaRepository<UserRoom, Long> {
Optional<UserRoom> findByUserAndRoom(User user, Room room);

// 서비스 로직에 UserRoom과 내부의 Room 정보를 모두 조회하여 사용하는 경우가 많았기에, 이를 적용하였음.
// @EntityGraph를 통해 UserRoom 조회시, 내부의 Lazy 로딩으로 선언된 Room 또한 Eager로 한번에 조회하여, N+1 문제를 해결.
// ==> Eager 조회 : 'UserRoom + 내부 Room', Lazy 조회 : '내부 User'
@EntityGraph(attributePaths = {"room"})
Optional<UserRoom> findByUser_IdAndRoom_Id(Long userId, Long roomId);

boolean existsByUserAndRoom(User user, Room room);

Page<UserRoom> findAllByIsLeaveAndUser(Integer isLeave, User user, Pageable pageable);
Page<UserRoom> findAllByIsLeaveAndUser_Id(Integer isLeave, Long userId, Pageable pageable);
Page<UserRoom> findAllByIsLeaveAndIsPass(Integer isLeave, Integer isPass, Pageable pageable);
Page<UserRoom> findAllByIsLeaveAndRoom_Problem_Number(Integer isLeave, Integer number, Pageable pageable);
Page<UserRoom> findAllByIsLeaveAndRoom_Link(Integer isLeave, String link, Pageable pageable);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.sajang.devracebackend.service;

import com.sajang.devracebackend.domain.Room;
import com.sajang.devracebackend.domain.User;
import com.sajang.devracebackend.domain.mapping.UserRoom;
import com.sajang.devracebackend.dto.room.RoomWaitRequestDto;
import com.sajang.devracebackend.dto.room.RoomWaitResponseDto;
Expand All @@ -13,7 +11,7 @@
import org.springframework.data.domain.Pageable;

public interface UserRoomService {
UserRoom findUserRoom(User user, Room room);
UserRoom findUserRoom(Long userId, Long roomId); // Eager 조회 : 'UserRoom + 내부 Room', Lazy 조회 : '내부 User'
RoomWaitResponseDto userWaitRoom(RoomWaitRequestDto roomWaitRequestDto);
void usersEnterRoom(Long roomId);
void userStopWaitRoom(Long roomId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,12 @@ else if(chatRequestDto.getMessageType().equals(MessageType.TALK)) { // 방 채
if(chatRequestDto.getMessage() == null) throw new ChatBadRequestException("채팅시에는 반드시 message를 함께 보내주어야합니다.");
message = chatRequestDto.getMessage();
}
else { // 랭킹 상승의 경우 (MessageType.RANK 일때)
else if(chatRequestDto.getMessageType().equals(MessageType.RANK)) {
room.addRanking(user.getId());
}
else { // 잘못된 MessageType
throw new ChatBadRequestException("잘못된 MessageType으로 API를 요청하였습니다.");
}

Chat chat = chatRequestDto.toEntity(message);
chatRepository.save(chat);
Expand All @@ -78,20 +81,18 @@ else if(chatRequestDto.getMessageType().equals(MessageType.TALK)) { // 방 채
@Transactional(readOnly = true)
@Override
public Slice<ChatResponseDto> findChatsByRoom(Long roomId, Pageable pageable) {
User loginUser = userService.findUser(SecurityUtil.getCurrentMemberId());
Room room = roomService.findRoom(roomId);
UserRoom userRoom = userRoomService.findUserRoom(loginUser, room);
UserRoom userRoom = userRoomService.findUserRoom(SecurityUtil.getCurrentMemberId(), roomId);
LocalDateTime leaveTime = userRoom.getLeaveTime();

// 본인 퇴장시각 이하까지의 채팅 내역 조회
Slice<Chat> chatSlice = chatRepository.findAllByRoomIdAndCreatedTimeLessThanEqual(roomId, leaveTime, pageable);

// 사용자 캐싱을 위한 맵 생성 (이미 검색한것은 다시 검색하지않도록 성능 향상)
Map<Long, User> userCacheMap = new HashMap<>();
Map<Long, User> cacheUserMap = new HashMap<>();

return chatSlice.map(chat -> {
Long senderId = chat.getSenderId();
User senderUser = userCacheMap.computeIfAbsent(senderId, id -> userService.findUser(id)); // 만약 캐시에 senderId키의 데이터가 없다면, DB조회하고 캐시에 추가.
User senderUser = cacheUserMap.computeIfAbsent(senderId, id -> userService.findUser(id)); // 만약 캐시에 senderId키의 데이터가 없다면, DB조회하고 캐시에 추가.
return new ChatResponseDto(chat, senderUser.getNickname(), senderUser.getImageUrl());
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.sajang.devracebackend.service.RoomService;
import com.sajang.devracebackend.service.UserRoomService;
import com.sajang.devracebackend.service.UserService;
import com.sajang.devracebackend.util.SecurityUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -28,6 +29,7 @@

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
Expand All @@ -40,11 +42,14 @@ public class UserRoomServiceImpl implements UserRoomService {
private final ChatRepository chatRepository;


// 서비스 로직에 UserRoom과 내부의 Room 정보를 모두 조회하여 사용하는 경우가 많았기에, 이를 적용하였음.
// @EntityGraph를 통해 UserRoom 조회시, 내부의 Lazy 로딩으로 선언된 Room 또한 Eager로 한번에 조회하여, N+1 문제를 해결.
// ==> Eager 조회 : 'UserRoom + 내부 Room', Lazy 조회 : '내부 User'
@Transactional(readOnly = true)
@Override
public UserRoom findUserRoom(User user, Room room) {
return userRoomRepository.findByUserAndRoom(user, room).orElseThrow(
()->new NoSuchUserRoomException(String.format("userId = %d & roomId = %d", user.getId(), room.getId())));
public UserRoom findUserRoom(Long userId, Long roomId) {
return userRoomRepository.findByUser_IdAndRoom_Id(userId, roomId).orElseThrow(
()->new NoSuchUserRoomException(String.format("userId = %d & roomId = %d", userId, roomId)));
}

@Transactional
Expand Down Expand Up @@ -117,53 +122,60 @@ public void usersEnterRoom(Long roomId) {
@Transactional
@Override
public void userStopWaitRoom(Long roomId) {
User loginUser = userService.findLoginUser();
Room room = roomService.findRoom(roomId);

room.deleteWaiting(loginUser.getId(), false); // 대기자 목록에서 해당 사용자 제거.
room.deleteWaiting(SecurityUtil.getCurrentMemberId(), false); // 대기자 목록에서 해당 사용자 제거.
}

@Transactional(readOnly = true)
@Override
public SolvingPageResponseDto loadSolvingPage(Long roomId) {
User user = userService.findLoginUser();
Room room = roomService.findRoom(roomId);
UserRoom userRoom = findUserRoom(user, room);
UserRoom userRoom = findUserRoom(SecurityUtil.getCurrentMemberId(), roomId); // 어차피 문제풀이 페이지는 입장 이후이기에, 부모 Room을 갖고있는 자식 UserRoom은 반드시 존재함.

List<Long> rankUserIdList = room.getWaiting();
List<Long> rankUserIdList = userRoom.getRoom().getWaiting(); // @EntityGraph로 UserRoom 내부의 Room은 Eager 로딩 처리되어, N+1 문제가 발생하지 않음.
List<UserResponseDto> rankUserDtoList = userService.findUsersOriginal(rankUserIdList, true);

return new SolvingPageResponseDto(userRoom, rankUserDtoList);
return new SolvingPageResponseDto(userRoom, rankUserDtoList); // UserRoom의 Room의 Problem은 호출 빈도수가 낮기도하고, JPA메소드 네이밍이 겹치기에, eager 적용을 하지 않았음.
}

@Transactional(readOnly = true)
@Override
public RoomCheckAccessResponseDto checkAccess(Long roomId) {
User user = userService.findLoginUser();
Room room = roomService.findRoom(roomId);
UserRoom userRoom = userRoomRepository.findByUserAndRoom(user, room)
.orElse(null);
// System.out.println("========== !!! 메소드 시작 !!! ==========\n");

// System.out.println("===== UserRoom 조회 =====");
Optional<UserRoom> optionalUserRoom = userRoomRepository.findByUser_IdAndRoom_Id(SecurityUtil.getCurrentMemberId(), roomId);
// System.out.println("===== UserRoom 조회 완료. [1번의 쿼리 발생] =====\n");

Boolean isExistUserRoom = true;
if(userRoom == null) isExistUserRoom = false;
Integer isLeave = null;
if(userRoom != null) isLeave = userRoom.getIsLeave();
// UserRoom이 존재하면 해당 정보 사용. 그렇지 않다면 DB에 Room 조회 쿼리 날림.
// System.out.println("===== UserRoom.getRoom() 실행 =====");
Room room = optionalUserRoom
.map(UserRoom::getRoom) // 이 시점에는 아직 @EntityGraph의 영향을 받지않아, 아직 조회 쿼리가 1번으로 유지됨.
.orElseGet(() -> roomService.findRoom(roomId));
// System.out.println("===== UserRoom의 Room을 가져오지만 Room 내부의 변수는 사용하지않음. [추가쿼리 발생 X] =====\n");

Boolean isExistUserRoom = optionalUserRoom.isPresent();
// System.out.println("===== UserRoom.getIsLeave() 실행 =====");
Integer isLeave = optionalUserRoom.map(UserRoom::getIsLeave).orElse(null); // UserRoom이 없다면 isLeave는 null
// System.out.println("===== UserRoom의 Room 외의 타변수를 사용. [추가쿼리 발생 X] =====\n");

// System.out.println("===== UserRoom.getRoom().getRoomState() 실행 =====");
RoomCheckAccessResponseDto roomCheckAccessResponseDto = RoomCheckAccessResponseDto.builder()
.isExistUserRoom(isExistUserRoom)
.roomState(room.getRoomState())
.roomState(room.getRoomState()) // @EntityGraph로 UserRoom 내부의 Room은 Eager 로딩 처리되어, N+1 문제가 발생하지 않음.
.isLeave(isLeave)
.build();
// System.out.println("===== UserRoom의 Room 내부의 변수를 사용. [@EntityGraph 미처리시 추가쿼리 발생 O] =====\n");

// System.out.println("========== !!! 메소드 종료 !!! ==========");

return roomCheckAccessResponseDto;
}

@Transactional
@Override
public void passSolvingProblem(Long roomId, UserPassRequestDto userPassRequestDto) {
User user = userService.findLoginUser();
Room room = roomService.findRoom(roomId);
UserRoom userRoom = findUserRoom(user, room);
UserRoom userRoom = findUserRoom(SecurityUtil.getCurrentMemberId(), roomId); // 어차피 문제풀이 페이지는 입장 이후이기에, 부모 Room을 갖고있는 자식 UserRoom은 반드시 존재함.
Room room = userRoom.getRoom(); // 이 시점에는 아직 @EntityGraph의 영향을 받지않아, 아직 조회 쿼리가 1번으로 유지됨.

userRoom.updateCode(userPassRequestDto.getCode());
userRoom.updateIsLeave(1);
Expand All @@ -174,7 +186,7 @@ public void passSolvingProblem(Long roomId, UserPassRequestDto userPassRequestDt
userRoom.updateIsPass(userPassRequestDto.getIsPass());
}

List<UserRoom> userRoomList = room.getUserRoomList();
List<UserRoom> userRoomList = room.getUserRoomList(); // @EntityGraph로 UserRoom 내부의 Room은 Eager 로딩 처리되어, N+1 문제가 발생하지 않음.
Boolean isLeaveAllUsers = userRoomList.stream()
.allMatch(enterUserRoom -> enterUserRoom.getIsLeave() == 1); // 입장했던 모든 유저의 isLeave 값이 1인지 확인
if(isLeaveAllUsers == true) room.updateRoomState(RoomState.FINISH);
Expand All @@ -183,11 +195,10 @@ public void passSolvingProblem(Long roomId, UserPassRequestDto userPassRequestDt
@Transactional(readOnly = true)
@Override
public Page<CodeRoomResponseDto> findCodeRoom(Integer isPass, Integer number, String link, Pageable pageable) {
User loginUser = userService.findLoginUser();
Page<UserRoom> userRoomPage;

if(isPass == null && number == null && link == null) { // 전체 정렬 조회의 경우
userRoomPage = userRoomRepository.findAllByIsLeaveAndUser(1, loginUser, pageable);
userRoomPage = userRoomRepository.findAllByIsLeaveAndUser_Id(1, SecurityUtil.getCurrentMemberId(), pageable);
}
else if(isPass != null && number == null && link == null) { // 성공or실패 정렬 조회의 경우
userRoomPage = userRoomRepository.findAllByIsLeaveAndIsPass(1, isPass, pageable);
Expand Down