JPA

(JPA) JPA 성능개선이란? 성능개선 적용기 (fetch join/BatchSize)

Developer RyanKim 2020. 3. 9. 16:34

JPA 성능개선 적용기 fetch join BatchSize

이전글: jpa 성능개선 목록 ↓

https://lion-king.tistory.com/entry/JPA-Performance-Improvement-list

마이페이지 개편 중 구매내역 api를 호출해보니 응답속도가 매우 느린 이슈가 있었습니다.
(서비스 메소드만 테스트 하였는데도 1분이상..)
테스트 계정이어서 구매내역이 많은 탓도 있었겠지만 개선이 필요할 것 같아 실행되는 쿼리를 확인해보니
엄청난 수의 select 문이 실행되는 것을 확인할 수 있었습니다.

업무시간 외에 이것저것 테스트를 하였고

fetch joinBatchSize를 사용해서 성능개선 되는 과정을 기록에 남깁니다.

(개선 전 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

 

(JPA) 프록시, 지연로딩, 즉시로딩 (Proxy / Lazy Loading / Eager Loading)

JPA 프록시 지연로딩 즉시로딩 Proxy / Lazy Loading / Eager Loading 지연로딩 (Lazy Loading) JPA 지연로딩은 프록시 객체의 메소드를 사용하는 시점에 데이터베이스에 쿼리문을 수행하여 엔티티를 조회하는 방..

lion-king.tistory.com