Blog

[Spring][258] BookApplyDonation에서의 N+1 문제 해결 및 성능 테스트

Category
Author
Tags
PinOnMain
1 more property

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%