1. 개요
기부된 책을 신청하는 과정에서 N+1 문제가 발생하였다.
N+1이 발생하지 않도록 코드를 수정하고 Jmeter로 성능테스트를 하여 결과를 비교하고자 한다.
2. N+1 발생 상황
기부된 책을 신청하는 로직은 아래와 같다.
@Transactional
public MessageDto createBookApplyDonation(BookApplyDonationRequestDto bookApplyDonationRequestDto) {
Book book = getBook(bookApplyDonationRequestDto);
MessageDto x = getMessageDto(book);
if (x != null) return x;
BookDonationEvent bookDonationEvent = getBookDonationEvent(bookApplyDonationRequestDto);
MessageDto x1 = getMessageDto(bookDonationEvent);
if (x1 != null) return x1;
User user = getUser();
BookApplyDonation bookApplyDonation = new BookApplyDonation(bookApplyDonationRequestDto);
bookApplyDonationRepository.save(bookApplyDonation);
bookApplyDonation.addBook(book);
user.getBookApplyDonations().add(bookApplyDonation);
bookDonationEvent.getBookApplyDonations().add(bookApplyDonation);
book.changeStatus(BookStatusEnum.SOLD_OUT);
return new MessageDto("책 나눔 신청이 완료되었습니다.");
}
private BookDonationEvent getBookDonationEvent(BookApplyDonationRequestDto bookApplyDonationRequestDto) {
BookDonationEvent bookDonationEvent = bookDonationEventRepository.findById(bookApplyDonationRequestDto.getDonationId())
.orElseThrow(()->new IllegalArgumentException("해당 이벤트가 존재하지 않습니다."));
return bookDonationEvent;
}
private MessageDto getMessageDto(Book book) {
if(book.getBookApplyDonation()!=null){
return new MessageDto("이미 누군가 먼저 신청했습니다.");
}
return null;
}
private User getUser() {
User user = userRepository.findnById(SecurityUtil.getPrincipal().get().getUserId()).orElseThrow(
()->new IllegalArgumentException("해당 사용자는 도서관 사용자가 아닙니다.")
);
return user;
}
Plain Text
복사
책 신청 요청을 보내면 아래와 같이 N+1 문제가 발생한다.
3. 해결
코드를 아래와 같이 수정하여 fetch join시켜 문제를 해결하였다.
User와 BookDonationEvent를 fetch join시켜서 가져오도록 변경하였음.
private BookDonationEvent getBookDonationEvent(BookApplyDonationRequestDto bookApplyDonationRequestDto) {
BookDonationEvent bookDonationEvent = bookDonationEventRepository.findFetchJoinById(bookApplyDonationRequestDto.getDonationId())
.orElseThrow(()->new IllegalArgumentException("해당 이벤트가 존재하지 않습니다."));
return bookDonationEvent;
}
private User getUser() {
User user = userRepository.findFetchJoinById(SecurityUtil.getPrincipal().get().getUserId()).orElseThrow(
()->new IllegalArgumentException("해당 사용자는 도서관 사용자가 아닙니다.")
);
return user;
}
//////////Repository@Repository
public interface BookDonationEventRepository extends JpaRepository<BookDonationEvent, Long>, QuerydslPredicateExecutor<BookDonationEvent> {
//추가
@Query("select bde from book_donation_event bde " +
"left join fetch bde.bookApplyDonations bad " +
"left join fetch bad.book b " +
"where bde.donationId = :donationId")
Optional<BookDonationEvent> findFetchJoinById(@Param("donationId") Long donationId);
}
@Repository
public interface UserRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {
//추가
@Query("select u from users u " +
"left join fetch u.bookApplyDonations bad " +
"left join fetch bad.book " +
"where u.userId = :userId")
Optional<User> findFetchJoinById(@Param("userId") Long userId);
}
Plain Text
복사
변경후 책 신청 요청을 보내면 아래와 같이 N+1 문제가 발생하지 않는 모습이다.
3. 결과 TEST
N+1을 해결한 결과 어느 정도의 성능 개선이 있는지 알아보기 위해 JMETER로 테스트를 진행하였다.
코드를 수정해야 하는 번거로움이 있어 서버에 올리지 않고 localhost, 로컬 DB환경에서 TEST를 진행하였다.
테스트 방식은 서버를 실행한 뒤 100개의 요청을 동시에 보내서 평균값 측정을 여러번 수행하였다.
자바의 콜드스타트로 인한 시행 4개를 제외하고 이후 4개의 시행을 기록하고 4개에 대한 평균값을 측정하였다.
결과는 아래와 같다.
구분 | bookDonation 평균값 | 시행 | 표본수 | 평균 | 중간값 | 90% | 95% | 99% | 최소값 | 최대값 |
N+1발생 X | 136 | 1 | 100 | 139 | 138 | 197 | 204 | 208 | 67 | 213 |
2 | 100 | 139 | 138 | 195 | 201 | 214 | 68 | 215 | ||
3 | 100 | 123 | 122 | 178 | 190 | 193 | 53 | 194 | ||
4 | 100 | 143 | 148 | 199 | 204 | 210 | 55 | 210 | ||
N+1발생 | 147.25 | 1 | 100 | 168 | 166 | 223 | 236 | 239 | 103 | 247 |
2 | 100 | 146 | 145 | 202 | 210 | 218 | 79 | 221 | ||
3 | 100 | 142 | 138 | 206 | 217 | 223 | 75 | 225 | ||
4 | 100 | 133 | 136 | 184 | 191 | 195 | 68 | 198 | ||
차이 | 11.25 | |||||||||
차이(%) | 8.3% | |||||||||