1. N+1, JOIN FETCH, 1차 캐시, 영속성?
1) 개요
// TODO: 3. N+1 문제
public List<ReservationResponseDto> getReservations() {
List<Reservation> reservations = reservationRepository.findAll();
return reservations.stream().map(reservation -> {
User user = reservation.getUser();
Item item = reservation.getItem();
return new ReservationResponseDto(
reservation.getId(),
user.getNickname(),
item.getName(),
reservation.getStartAt(),
reservation.getEndAt()
);
}).toList();
}
이 코드를 N+1 문제를 해결해야한다.
추가적으로
- 스트림으로 원하는 칼럼을 핸들링 하는 것 부터 이상한 것 같음 -> 원하는 칼럼만 들고 올 것임
2) 문제상황
// TODO: 3. N+1 문제
public List<ReservationResponseDto> getReservations() {
List<Reservation> reservations = reservationRepository.findAll();
return reservations.stream().map(reservation -> {
User user = reservation.getUser();
Item item = reservation.getItem();
return new ReservationResponseDto(
reservation.getId(),
user.getNickname(),
item.getName(),
reservation.getStartAt(),
reservation.getEndAt()
);
}).toList();
}
코드를 자세히 보면 List<Reservation> reservations = reservationRepository.findAll();
-> jpa메소드 findAll()을 써서 다 들고 온다. 또한, 유저, 아이템을 추가적으로 들고오는 과정에서 N+1 문제 발생
그렇기에 jpql로 간단히 해결 가능할 것이라는 생각
// 1차로 생각한 방법
@Query("SELECT r " +
"from Reservation r " +
"join fetch r.item i " +
"join fetch r.user u")
List<ReservationResponseDto> findAllCustom();
잘 작동된다. 하지만 쓸모없는 칼럼까지 다 들고와서 서비스에서 스트림처리하는 것은 옳지 않다고 판단
// 2차로 생각한 방법
@Query("SELECT new com.example.demo.dto.ReservationResponseDto(r.id, u.nickname, i.name, r.startAt, r.endAt) " +
"from Reservation r " +
"join fetch r.item i " +
"join fetch r.user u")
List<ReservationResponseDto> findAllCustom();
빌드가 되지 않는다. 쿼리가 잘못 되었다는 에러로그가 출력이 된다.
3) 해결
// 3차로 생각한 방법
@Query("SELECT new com.example.demo.dto.ReservationResponseDto(r.id, u.nickname, i.name, r.startAt, r.endAt) " +
"from Reservation r " +
"join r.item i " +
"join r.user u")
List<ReservationResponseDto> findAllCustom();
join fetch -> join으로 교체
// TODO: 3. N+1 문제
public List<ReservationResponseDto> getReservations() {
return reservationRepository.findAllCustom();
}
=> 서비스 코드도 간단해졌다.
그렇다면 왜 join fetch가 오류를 발생시켰나 생각을 해보았고 검색을 좀 해보니
- join fetch는 entity 를 들고와서 1차 캐시에 저장을 하는데
저렇게 dto로 하나의 필드를 들고와서 영속성 전이를 시킨다는 것이 말이 안된다.
4) 결론
- 영속성 전이
- 1차 캐시
- N+1 문제
- jpql 지식
등이 많이 부족한 것 같다. 좀 더 쿼리를 작성해보며 쿼리 로그가 어떻게 올라오는지 한 번 더 확인 해보아야 할 것이다.
2. 쿼리 개선
1) 개요
// TODO: 4. find or save 예제 개선
@Transactional
public void reportUsers(List<Long> userIds) {
for (Long userId : userIds) {
User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("해당 ID에 맞는 값이 존재하지 않습니다."));
user.updateStatusToBlocked();
userRepository.save(user);
}
}
이 문제를 해결시켜야 한다. 이렇게 작동하면 쿼리는 유저수 만큼 쿼리가 돌 것이다. 그럼 유저를 100, 10000, 100_000을 신고한다면 find, update 두개가 있기에 쿼리 * 2 만큼 발생할 것이다.
2) 문제상황
// TODO: 4. find or save 예제 개선
@Transactional
public void reportUsers(List<Long> userIds) {
for (Long userId : userIds) {
User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("해당 ID에 맞는 값이 존재하지 않습니다."));
user.updateStatusToBlocked();
userRepository.save(user);
}
}
그렇다면 해당 유저를 id로 찾는것은
// TODO: 4. find or save 예제 개선
@Transactional
public void reportUsers(List<Long> userIds) {
List<User> userList = userRepository.findByIdIn(userIds);
}
간단히 해결할 수 있는데 해당 유저 stauts 필드를 어떻게 수정할 것인가에 대한 문제이다.
find는 in 이라는 메소드를 지원해 한번에 찾을 수 있지만 update는 저런게 있는지도 모르겠다.
3) 해결
// TODO: 4. find or save 예제 개선
@Transactional
public void reportUsers(List<Long> userIds) {
List<User> userList = userRepository.findByIdIn(userIds);
userRepository.reportUsers("BLOCKED", userList);
}
reportUsers를 jpql로 구현하기로 결정
@Modifying
@Query("Update User u Set u.status = :blocked WHERE u IN :userIds")
void reportUsers(String blocked, List<User> userIds);
이렇게하면 유저 수가 아무리 많아도 쿼리는 2번만 발생한다.
4) 결론
jpql로 해결할 수 있었지만, 분명 jpa 메소드로도 수정할 수 있을 것 같은데, jpql보다는 이렇게 간단한거는 jpa메소드를 활용해보아야겠다.
'스파르타(부트캠프) > Troubleshooting' 카테고리의 다른 글
[내일배움캠프] 스프링심화주차 트러블슈팅 (TBU) (1) | 2024.12.26 |
---|---|
[내일배움캠프] 뉴스피드 프로젝트에 대한 트러블슈팅 (0) | 2024.11.22 |
[내일배움캠프] 일정관리 앱(develop)에 대한 트러블슈팅 (4) | 2024.11.15 |
[내일배움캠프] 일정관리 앱에 대한 트러블슈팅 (1) | 2024.11.08 |