들어가며

이전 페이징 알아가기 1 (MySQL) 에서 OFFSET 방식의 문제점을 알아보았습니다. 이번엔 JPA에서 페이징을 처리할 때 어떻게 처리하는지 알아보겠습니다.

JPA 페이징

MySQL에서 페이징을 처리하는 방법에 대해 알아보았습니다.. 이제부턴 JPA에서는 어떻게 페이징을 처리하는지, 어떤 문제가 있는지 알아보겠습니다.

JPQL JOIN

EntityManagersetFirstResult(), setMaxResults() 메서드를 사용하면 간단하게 페이징 처리를 할 수 있습니다.

List<Reservation> result = entityManager.createQuery("SELECT r "
                + "FROM Reservation AS r "
                + "JOIN FETCH r.coupon AS c ", Reservation.class)
        .setFirstResult(100)
        .setMaxResults(50)
        .getResultList();

스크린샷 2022-08-31 오전 1 27 30

기본적으로 limitoffset이 사용되는 것을 볼 수 있습니다. 위는 MySQL에서 지원하는 방법이기 때문에 다른 데이터베이스를 사용할 경우 해당 데이터베이스에 지원하는 언어대로 처리가 됩니다.


Spring Data JPA

SpringDataJpa의 경우 Pageable 객체를 사용한다면 더 간편하게 조회가 가능합니다.

Page<Reservation> findAll(Pageable pageable);
Page<Reservation> result = reservationRepository.findAll(PageRequest.of(0, 10));


🚨하지만 문제가 있다.

  1. count 쿼리 발생

Page를 반환할 경우 getTotalPages()getTotalElements() 를 위해 추가로 count 쿼리가 발생합니다.

스크린샷 2022-08-31 오전 1 28 07

👉 불필요한 count 쿼리를 제거하려면 Page를 반환받지 않고 Slice를 반환받으면 됩니다.

Slice<Reservation> findSliceBy(Pageable pageable);

스크린샷 2022-08-31 오전 1 28 28

추가 count 쿼리가 발생하지 않게 되었네요. 총 데이터 갯수가 필요하지 않다면 Slice를 반환받아야겠네요.

  1. N + 1 문제

Reservation의 경우 Coupon을 Lazy 전략으로 가지고 있기 때문에 조회해온 뒤 다시 쿠폰을 조회하기 위해 select 쿼리가 발생합니다. 10개의 쿠폰을 조회하기 위해 10개의 select 문이 추가로 발생하게 되는거죠. 이를 N + 1 문제라고 합니다.

스크린샷 2022-08-31 오전 1 29 07

XXXToOne 관계에서는 fetch join을 사용해도 문제가 되지 않습니다.

@Query("select r from Reservation r join fetch r.coupon")
List<Reservation> findAllWithFetch(Pageable pageable);
@EntityGraph(attributePaths = "coupon", type = EntityGraphType.LOAD)
Page<Reservation> findAll(Pageable pageable);

스크린샷 2022-08-31 오전 1 29 37

여전히 offset을 사용하여 조회중이네요.


XXXToMany 관계에서는 fetch join으로 데이터를 조회하는 데 문제가 발생합니다. 컬렉션을 기준으로 조회를 하기때문에 One 객체가 중복이 발생하고 jpa 입장에서는 어떤것을 기준으로 페이징처리를 해야하는지 알 수가 없어요. 따라서 데이터를 모두 조회한 뒤 어플리케이션 영역에서 페이징 작업을 처리합니다.

NativeQuery

일반 쿼리로 해결하려 했지만 역시나 Lazy 로딩인 쿠폰때문에 N+1이 발생합니다.

@Query(value = "select * "
            + "from reservation as r "
            + "join coupon as c "
            + "on r.coupon_id = c.id "
            + "where r.id > :offset "
            + "limit :limit",
            nativeQuery = true)
List<Reservation> findAll(@Param("offset") int offset, @Param("limit") int limit);

땡쿠는 어떻게 해야할까?

페이징을 공부할수록 아직 땡쿠팀이 페이징을 적용할 때인가? 라는 의문이 들었어요. 자신의 쿠폰이 100개가 넘는 일도 쉽지 않을 듯하고 적은 데이터에서 쿼리를 개선해도 큰 의미가 있지 않기 때문이죠.

OneToMany 관계에서 페이징 처리를 할 경우 문제가 발생할 수 있다는 것을 인지하였고 페이징을 적용해야 한다면 jdbc를 사용하는 방식을 사용하다가 추후 QueryDsl로 넘어가는 게 낫지 않을까 생각도 들었습니다.

땡쿠에서 페이징을 적용하는 시점이 다가올 때 여러 문제점들을 어떻게 해결했는지로 찾아올게요👊


참고 : 페이징 기능을 성능 최적화하기

카테고리:

업데이트: