Skip to content

Commit

Permalink
Merge pull request #122 from Dev-Race/refactor/#121
Browse files Browse the repository at this point in the history
[#121] Refactor: Fetch Join 추가 리팩토링 및 성능 개선
  • Loading branch information
tkguswls1106 authored Jun 2, 2024
2 parents c1872f8 + 5c01103 commit 3bd6092
Show file tree
Hide file tree
Showing 17 changed files with 184 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import com.sajang.devracebackend.config.RabbitConfig;
import com.sajang.devracebackend.dto.problem.ProblemSaveRequestDto;
import com.sajang.devracebackend.dto.room.RoomSaveResponseDto;
import com.sajang.devracebackend.dto.room.RoomResponseDto;
import com.sajang.devracebackend.dto.room.RoomWaitRequestDto;
import com.sajang.devracebackend.dto.room.RoomWaitResponseDto;
import com.sajang.devracebackend.dto.userroom.CodeRoomResponseDto;
import com.sajang.devracebackend.dto.userroom.UserPassRequestDto;
import com.sajang.devracebackend.dto.userroom.RoomCheckAccessResponseDto;
import com.sajang.devracebackend.dto.room.RoomCheckStateResponseDto;
Expand All @@ -18,10 +17,6 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
Expand All @@ -41,9 +36,16 @@ public class RoomController {

@PostMapping("/rooms")
@Operation(summary = "방생성 Page - 방 생성 [jwt O]")
public ResponseEntity<ResponseData<RoomSaveResponseDto>> createRoom(@RequestBody ProblemSaveRequestDto problemSaveRequestDto) throws IOException {
RoomSaveResponseDto roomSaveResponseDto = roomService.createRoom(problemSaveRequestDto);
return ResponseData.toResponseEntity(ResponseCode.CREATED_ROOM, roomSaveResponseDto);
public ResponseEntity<ResponseData<RoomResponseDto>> createRoom(@RequestBody ProblemSaveRequestDto problemSaveRequestDto) throws IOException {
RoomResponseDto roomResponseDto = roomService.createRoom(problemSaveRequestDto);
return ResponseData.toResponseEntity(ResponseCode.CREATED_ROOM, roomResponseDto);
}

@GetMapping("/rooms")
@Operation(summary = "초대링크 방 조회 [jwt O]", description = "URI : /rooms?link={방링크 string}")
public ResponseEntity<ResponseData<RoomResponseDto>> findRoomByLink(@RequestParam(value = "link", required = true) String link) {
RoomResponseDto roomResponseDto = roomService.findRoomByLink(link);
return ResponseData.toResponseEntity(ResponseCode.READ_ROOM, roomResponseDto);
}

@GetMapping("/rooms/{roomId}")
Expand All @@ -68,7 +70,7 @@ public ResponseEntity<ResponseData<RoomCheckStateResponseDto>> checkState(@PathV
}

@PostMapping("/rooms/{roomId}") // 의미상 PUT보단 POST가 더 적합하다고 판단했음.
@Operation(summary = "문제풀이 Page - 문제풀이 실패성공 [jwt O]", description = "isRetry==0 : 첫풀이 경우 / isRetry==1 : 재풀이 경우")
@Operation(summary = "문제풀이 Page - 문제풀이 성공실패/퇴장 [jwt O]", description = "isRetry==0 : 첫풀이 경우 / isRetry==1 : 재풀이 경우")
public ResponseEntity<ResponseData> passSolvingProblem(
@PathVariable(value = "roomId") Long roomId,
@RequestBody UserPassRequestDto userPassRequestDto) {
Expand All @@ -84,24 +86,6 @@ public ResponseEntity<ResponseData> userStopWaitRoom(@PathVariable(value = "room
return ResponseData.toResponseEntity(ResponseCode.UPDATE_ROOM);
}

@GetMapping("/rooms")
@Operation(summary = "내 코드방 목록 조회/정렬/검색 [jwt O]",
description = """
- 전체 정렬 URI : /rooms?page={페이지번호 int}
- 성공or실패 정렬 URI : /rooms?isPass={풀이성공여부 int}&page={페이지번호 int}
- 문제번호 검색 URI : /rooms?number={문제번호 int}&page={페이지번호 int}
- 초대링크 탐색 URI : /rooms?link={방링크 string} \nsize, sort 필요X (기본값 : page=0 & size=9 & sort=createdTime,desc)
""")
public ResponseEntity<ResponseData<Page<CodeRoomResponseDto>>> findCodeRoom(
@RequestParam(value = "isPass", required = false) Integer isPass,
@RequestParam(value = "number", required = false) Integer number,
@RequestParam(value = "link", required = false) String link,
@PageableDefault(page=0, size=9, sort="createdTime", direction = Sort.Direction.DESC) Pageable pageable) {

Page<CodeRoomResponseDto> codeRoomResponseDtoPage = userRoomService.findCodeRoom(isPass, number, link, pageable);
return ResponseData.toResponseEntity(ResponseCode.READ_USERROOM, codeRoomResponseDtoPage);
}

@MessageMapping("wait.enter") // 웹소켓 메시지 처리 (백엔드로 '/pub/wait.enter'를 호출시 이 브로커에서 처리)
public void userWaitRoom(@Payload RoomWaitRequestDto roomWaitRequestDto) {
// '/exchange/wait.exchange/waitingroom.{roomId}' 구독되어있는 프론트엔드에게 메세지 전달.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@
import com.sajang.devracebackend.dto.user.UserResponseDto;
import com.sajang.devracebackend.dto.user.UserSolvedResponseDto;
import com.sajang.devracebackend.dto.user.UserUpdateRequestDto;
import com.sajang.devracebackend.dto.userroom.CodeRoomResponseDto;
import com.sajang.devracebackend.response.ResponseCode;
import com.sajang.devracebackend.response.ResponseData;
import com.sajang.devracebackend.service.UserRoomService;
import com.sajang.devracebackend.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
Expand All @@ -26,6 +29,7 @@
public class UserController {

private final UserService userService;
private final UserRoomService userRoomService;


@GetMapping("/users")
Expand All @@ -50,17 +54,33 @@ public ResponseEntity<ResponseData> updateUserProfile(
return ResponseData.toResponseEntity(ResponseCode.UPDATE_USER);
}

@GetMapping("/users/solved-count")
@Operation(summary = "백준 solvedCount값 조회 [jwt O]")
public ResponseEntity<ResponseData<UserSolvedResponseDto>> checkUserSolvedCount() {
UserSolvedResponseDto userSolvedResponseDto = userService.checkUserSolvedCount();
return ResponseData.toResponseEntity(ResponseCode.READ_SOLVEDCOUNT, userSolvedResponseDto);
@GetMapping("/users/rooms")
@Operation(summary = "코드방 목록 조회/정렬/검색 [jwt O]",
description = """
- 전체 정렬 URI : /users/rooms?page={페이지번호 int}
- 성공or실패 정렬 URI : /users/rooms?isPass={풀이성공여부 int}&page={페이지번호 int}
- 문제번호 검색 URI : /users/rooms?number={문제번호 int}&page={페이지번호 int} \nsize, sort 필요X (기본값 : size=9 & sort=createdTime,desc)
""")
public ResponseEntity<ResponseData<Page<CodeRoomResponseDto>>> findCodeRooms(
@RequestParam(value = "isPass", required = false) Integer isPass,
@RequestParam(value = "number", required = false) Integer number,
@PageableDefault(size=9, sort="createdTime", direction = Sort.Direction.DESC) Pageable pageable) {

Page<CodeRoomResponseDto> codeRoomResponseDtoPage = userRoomService.findCodeRooms(isPass, number, pageable);
return ResponseData.toResponseEntity(ResponseCode.READ_USERROOM, codeRoomResponseDtoPage);
}

@GetMapping("/users/room-check")
@GetMapping("/users/rooms-check")
@Operation(summary = "메인 Page - 참여중인 방 여부 검사 [jwt O]", description = "roomId == Long or null")
public ResponseEntity<ResponseData<UserCheckRoomResponseDto>> checkCurrentRoom() {
UserCheckRoomResponseDto userCheckRoomResponseDto = userService.checkCurrentRoom();
return ResponseData.toResponseEntity(ResponseCode.READ_ROOM, userCheckRoomResponseDto);
}

@GetMapping("/users/solved-count")
@Operation(summary = "백준 solvedCount값 조회 [jwt O]")
public ResponseEntity<ResponseData<UserSolvedResponseDto>> checkUserSolvedCount() {
UserSolvedResponseDto userSolvedResponseDto = userService.checkUserSolvedCount();
return ResponseData.toResponseEntity(ResponseCode.READ_SOLVEDCOUNT, userSolvedResponseDto);
}
}
6 changes: 3 additions & 3 deletions src/main/java/com/sajang/devracebackend/domain/Room.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ public class Room extends BaseEntity implements Serializable{
@Column(name = "room_state")
private RoomState roomState;

// Room 조회시 Room.problem N+1 문제 해결 ==> Eager 조회 : '@EntityGraph 적용 메소드 활용', Lazy 조회 : '기본 지연로딩 용도'
@ManyToOne(fetch = FetchType.LAZY) // Room-Problem 단방향매핑
@JoinColumn(name = "problem_id")
private Problem problem;

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


@Builder(builderClassName = "RoomSaveBuilder", builderMethodName = "RoomSaveBuilder")
Expand Down
5 changes: 2 additions & 3 deletions src/main/java/com/sajang/devracebackend/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,9 @@ 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내부속성()으로 접근하는 경우.
// User 조회시 User.userRoomList N+1 문제 해결 ==> Eager 조회 : '사용자의 참여중인 방 찾는 용도', Lazy 조회 : '기본 지연로딩 용도 & 회원탈퇴 용도'
@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 @@ -39,11 +39,13 @@ public class UserRoom extends BaseEntity implements Serializable {
@Column(name = "leave_time")
private LocalDateTime leaveTime; // 퇴장 시각 (반면, createdTime = 입장 시각)

// Eager 조회의 용도가 적어, 기본 Lazy 조회로만 사용함.
@ManyToOne(fetch = FetchType.LAZY) // User-UserRoom 양방향매핑
@JoinColumn(name = "user_id")
private User user;

@ManyToOne(fetch = FetchType.LAZY) // Room-UserRoom 양방향매핑 (default로 Lazy를 적용하되, Eager가 필요할경우 따로 메소드를 만들어 사용함.)
// UserRoom 조회시 UserRoom.room N+1 문제 해결 ==> Eager 조회 : '@EntityGraph 적용 메소드 활용', Lazy 조회 : '기본 지연로딩 용도'
@ManyToOne(fetch = FetchType.LAZY) // Room-UserRoom 양방향매핑
@JoinColumn(name = "room_id")
private Room room;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class RoomSaveResponseDto {
public class RoomResponseDto {

private Long roomId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
import com.sajang.devracebackend.domain.Room;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface RoomRepository extends JpaRepository<Room, Long> {
Optional<Room> findByLink(String link);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@
import com.sajang.devracebackend.domain.User;
import com.sajang.devracebackend.domain.enums.SocialType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
// '@EntityGraph' 또는 'Fetch Join'을 통해 User 조회시, 내부의 Lazy 로딩으로 선언된 속성 또한 Eager로 한번에 조회.
// ==> N+1 문제 해결 (간단한 로직은 '@EntityGraph'로, 복잡한 로직이거나 기존 JPA 메소드와의 네이밍 충돌을 방지하고자하는 경우는 'Fetch Join'을 활용.)

// - Eager 조회 : 'User + User.userRoomList'
@Query("SELECT u FROM User u JOIN FETCH u.userRoomList WHERE u.id = :userId")
Optional<User> findByIdWithEagerUserRoomList(@Param("userId") Long userId); // 기존의 JPA findById() 메소드를 @Override 하지않고 유지하기위해, @Query로 새로운 메소드 네이밍을 지어주었음. -> '@EntityGraph' 말고 'Fetch Join' 사용.

Optional<User> findBySocialTypeAndSocialId(SocialType socialType, String socialId); // '소셜 타입, 식별자'로 해당 회원을 찾기 위한 메소드 (차후 추가정보 입력 회원가입 가능)
boolean existsByBojId(String bojId);
List<User> findByIdIn(List<Long> userIdList);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,36 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface UserRoomRepository extends JpaRepository<UserRoom, Long> {
// '@EntityGraph' 또는 'Fetch Join'을 통해 UserRoom 조회시, 내부의 Lazy 로딩으로 선언된 속성 또한 Eager로 한번에 조회.
// ==> N+1 문제 해결 (간단한 로직은 '@EntityGraph'로, 복잡한 로직이거나 기존 JPA 메소드와의 네이밍 충돌을 방지하고자하는 경우는 'Fetch Join'을 활용.)

// 서비스 로직에 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);
// - Eager 조회 : 'UserRoom + UserRoom.room', Lazy 조회 : 'UserRoom.user'
@EntityGraph(attributePaths = {"room"}) // 반환값이 List가 아닌 고유한 하나의 값이기에, @EntityGraph임에도 DISTINCT 없이 작성했음.
Optional<UserRoom> findByUser_IdAndRoom_Id(Long userId, Long roomId); // 이는 @Query가 아니므로, JPA네이밍 규칙에 영향을 받음.

// - Eager 조회 : 'UserRoom + UserRoom.room + UserRoom.room.userRoomList', Lazy 조회 : 'UserRoom.user'
@Query("SELECT ur FROM UserRoom ur JOIN FETCH ur.room r JOIN FETCH r.userRoomList WHERE ur.user.id = :userId AND ur.room.id = :roomId")
Optional<UserRoom> findByUser_IdAndRoom_IdWithUserRoomList(@Param("userId") Long userId, @Param("roomId") Long roomId); // @Query 이므로, With문처럼 JPA네이밍 규칙을 지키지않아도됨.

// - Eager 조회 : 'UserRoom + UserRoom.room + UserRoom.room.problem', Lazy 조회 : 'UserRoom.user'
@Query("SELECT ur FROM UserRoom ur JOIN FETCH ur.room r JOIN FETCH r.problem WHERE ur.user.id = :userId AND ur.room.id = :roomId")
Optional<UserRoom> findByUser_IdAndRoom_IdWithProblem(@Param("userId") Long userId, @Param("roomId") Long roomId); // @Query 이므로, With문처럼 JPA네이밍 규칙을 지키지않아도됨.

boolean existsByUserAndRoom(User user, Room room);

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> findAllByRoom_Link(String link, Pageable pageable);
// 밑의 3가지 메소드 모두, 페이징에서 OneToMany 속성을 Eager 조회시키지 않을것이기에, '중복 제거 처리' 및 '배치 사이즈 선언'을 하지 않았음.
@EntityGraph(attributePaths = {"room", "room.problem"})
Page<UserRoom> findAllByUser_IdAndIsLeave(Long userId, Integer isLeave, Pageable pageable);

@EntityGraph(attributePaths = {"room", "room.problem"})
Page<UserRoom> findAllByUser_IdAndIsLeaveAndIsPass(Long userId, Integer isLeave, Integer isPass, Pageable pageable);

@EntityGraph(attributePaths = {"room", "room.problem"})
Page<UserRoom> findAllByUser_IdAndIsLeaveAndRoom_Problem_Number(Long userId, Integer isLeave, Integer number, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import com.sajang.devracebackend.domain.Room;
import com.sajang.devracebackend.dto.problem.ProblemSaveRequestDto;
import com.sajang.devracebackend.dto.room.RoomCheckStateResponseDto;
import com.sajang.devracebackend.dto.room.RoomSaveResponseDto;
import com.sajang.devracebackend.dto.room.RoomResponseDto;

import java.io.IOException;

public interface RoomService {
Room findRoom(Long roomId);
RoomSaveResponseDto createRoom(ProblemSaveRequestDto problemSaveRequestDto) throws IOException;
RoomResponseDto findRoomByLink(String link);
RoomResponseDto createRoom(ProblemSaveRequestDto problemSaveRequestDto) throws IOException;
RoomCheckStateResponseDto checkState(Long roomId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
import org.springframework.data.domain.Pageable;

public interface UserRoomService {
UserRoom findUserRoom(Long userId, Long roomId); // Eager 조회 : 'UserRoom + 내부 Room', Lazy 조회 : '내부 User'
UserRoom findUserRoomWithEagerRoom(Long userId, Long roomId, boolean isIncludeUserRoomList, boolean isIncludeProblem);
RoomWaitResponseDto userWaitRoom(RoomWaitRequestDto roomWaitRequestDto);
void usersEnterRoom(Long roomId);
void userStopWaitRoom(Long roomId);
SolvingPageResponseDto loadSolvingPage(Long roomId);
RoomCheckAccessResponseDto checkAccess(Long roomId);
void passSolvingProblem(Long roomId, UserPassRequestDto userPassRequestDto);
Page<CodeRoomResponseDto> findCodeRoom(Integer isPass, Integer number, String link, Pageable pageable);
Page<CodeRoomResponseDto> findCodeRooms(Integer isPass, Integer number, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
public interface UserService {
User findUser(Long userId);
User findLoginUser();
<T> List<T> findUsersOriginal(List<Long> userIdList, Boolean isDto);
<T> List<T> findUsersOriginal(List<Long> userIdList, boolean isDto);
UserResponseDto findUserProfile();
void updateUserProfile(MultipartFile imageFile, UserUpdateRequestDto userUpdateRequestDto) throws IOException;
UserSolvedResponseDto checkUserSolvedCount();
Expand Down
Loading

0 comments on commit 3bd6092

Please sign in to comment.