(JPA) JPA 성능개선이란? 성능개선 적용기 (fetch join/BatchSize)
JPA 성능개선 적용기 fetch join BatchSize
이전글: jpa 성능개선 목록 ↓
https://lion-king.tistory.com/entry/JPA-Performance-Improvement-list
마이페이지 개편 중 구매내역 api를 호출해보니 응답속도가 매우 느린 이슈가 있었습니다.
(서비스 메소드만 테스트 하였는데도 1분이상..)
테스트 계정이어서 구매내역이 많은 탓도 있었겠지만 개선이 필요할 것 같아 실행되는 쿼리를 확인해보니
엄청난 수의 select 문이 실행되는 것을 확인할 수 있었습니다.
업무시간 외에 이것저것 테스트를 하였고
fetch join과 BatchSize를 사용해서 성능개선 되는 과정을 기록에 남깁니다.
(개선 전 77초 -> 개선 후 2초)
기존 Entity
@Entity
public class OgEx implements Serializable {
...
@OneToMany(cascade = CascadeType.PERSIST)
@JoinColumn(name = "og_id")
private List<Option> options;
@OneToMany(cascade = CascadeType.PERSIST)
@JoinColumn(name = "og_id")
private List<Translation> translations;
@OneToMany
@JoinColumn(name = "og_id")
private List<Ticket> tickets;
...
}
기존 Repository 메소드 (fetch join 미사용)
@Repository
public interface OgExRepository extends CustomRepository<OgEx, Long> {
@Query(value = "SELECT OG FROM OgEx AS OG "
+ "JOIN Order AS OB ON OG.orderNumber = OB.orderNumber "
+ "WHERE OB.buyerId = :memberId")
Optional<List<OgEx>> findAllByMemberId(@Param("memberId") Long memberId,
Pageable pageable);
...
}
테스트 코드
@Test
public void 주문내역() {
long start = System.currentTimeMillis();
System.out.println("@@@ getOgExByMemberId 시작");
List<OgEx> ogList =
service.getOgExByMemberId(3L, PageRequest.of(0, 3000));
long end = System.currentTimeMillis();
System.out.println("@@@ getOgExByMemberId 완료 실행 시간 : " + (end - start) / 1000.0);
assertThat(ogList.size(), greaterThan(0));
}
간단하게 테스트코드를 만들었습니다.
service.getOgExByMemberId() 가 테스트할 메소드이고, 수행 후 걸린시간까지 확인합니다.
getOgExByMemberId 메소드 안에서 OgExRepository.findAllByMemberId()가 호출되고
결과로 받은 OgEx 엔티티의 대부분의 필드에 접근하여 사용합니다.
1. 개선 전 테스트
select 문이 OgEx 엔티티 수 * 3 만큼 사용됩니다 (연관된 엔티티 리스트인 options, translations, tickets를 가져오기위해)
77.3초 소요
2. fetch join 전략 사용
2.1 MultipleBagFetchException
@Query(value = "SELECT distinct OG FROM OgEx AS OG "
+ "JOIN Order AS OB ON OG.orderNumber = OB.orderNumber "
+ "join fetch OG.options "
+ "join fetch OG.translations "
+ "join fetch OG.tickets "
+ "WHERE OB.buyerId = :memberId")
Optional<List<OgEx>> findAllByMemberId(@Param("memberIdx") Long memberId,
Pageable pageable);
@OneToMany 일대다 관계 매핑이 되어있는 모든 엔티티 리스트를 fetch join을 통해 가져오도록 수정하였습니다.
결과 ↓
MultipleBagFetchException이 발생하였습니다.
fetch join 설정은 OneToMany(일대다) 관계 하나에만 적용할 수 있고, 두번 이상 사용시에는 위 예외를 보게됩니다.
(두번이상 사용시 막는 이유가 있습니다) ManyToOne(다대일), OneToOne(일대일) 관계는 여러번 적용이 가능하니 참고해주세요.
2.2 fetch join 전략이 사용될 매핑 리스트 선택 및 적용
여러 ManyToOne 관계 중, 한곳에만 fetch join 사용이 가능하기 때문에
어떤 부분에 fetch join을 사용할 것인지 선택해야합니다.
options
: 56초
translations
: 15초
tickets
: 28초
테스트 결과 translations를 fetch join으로 가져오는것이 가장 성능이 좋아서
해당 관계에 fetch join을 사용하기로 하였습니다.
3. BatchSize 적용
위 과정을 거쳐도 나머지 OneToMany 관계가 남았기 때문에 N+1 문제가 완전히 해결되지 않았습니다.
나머지에는 BatchSize를 적용하기로 하였습니다.
BatchSize를 적용하면 쿼리가 N번 수행되지 않고, where 조건절이 in 절로 바뀌어 수행되기 때문에 성능이 향상됩니다.
BatchSize 적용 전 생성 쿼리 (수행시간: 15초)
select orderoptio0_.order_good_idx as order_g25_18_0_, orderoptio0_.order_option_idx as order_op1_18_0_, orderoptio0_.order_option_idx as order_op1_18_1_,
...
from order_option orderoptio0_
where orderoptio0_.order_good_idx = ?
option을 가져오기 위해 위 쿼리가 OgEx 수 만큼 사용됩니다.
ticket을 가져오는 것도 함께 그 수 만큼 수행되겠지요.
@BatchSize(size=10) 적용 후 생성 쿼리
select orderoptio0_.order_good_idx as order_g25_18_0_, orderoptio0_.order_option_idx as order_op1_18_0_, orderoptio0_.order_option_idx as order_op1_18_1_,
...
from order_option orderoptio0_
where orderoptio0_.order_good_idx in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
in 절이 생긴것이 보이시나요?
만약 OgEx 결과가 100개 였다면 select 문이 100번에서 10번으로 감소될 것입니다.
BatchSize 까지 적용 후 테스트
수행시간: 2.3초
최종코드
@Query(value = "SELECT distinct OG FROM OgEx AS OG "
+ "JOIN Order AS OB ON OG.orderNumber = OB.orderNumber "
+ "join fetch OG.translations "
+ "WHERE OB.buyerId = :memberId")
Optional<List<OgEx>> findByMemberIdWithFetchJoin(@Param("memberId") Long memberId,
Pageable pageable);
: 성능이 가장 좋았던 translations 관계에 fetch join 전략 사용
@OneToMany(cascade = CascadeType.PERSIST)
@JoinColumn(name = "og_id")
@BatchSize(size = 10)
private List<Option> options = new ArrayList<>();
@OneToMany
@JoinColumn(name = "og_id")
private List<Translation> translations = new ArrayList<>();
@OneToMany
@JoinColumn(name = "og_id")
@BatchSize(size = 10)
private List<Ticket> tickets = new ArrayList<>();
: 나머지 관계에 BatchSize 적용
* fetchType.EAGER 제거: JPQL에서는 N+1 문제를 야기함
* new ArrayList 추가: 결과가 null 인 것보다 List가 empty인 것이 직관적임
위 과정으로 JPA 성능을 개선하여
api의 응답속도를 크게 개선할 수 있었습니다.
도움이 되셨으면 좋겠습니다!
읽어주셔서 감사합니다~
By RyanKim.
관련글: https://lion-king.tistory.com/57