본문 바로가기
JPA

Data Jpa 페이징

by 혀눅짱 2023. 10. 12.

오늘은 Spring Data JPA에서 제공하는 페이징에 대해 복습한다.

내가 JPA를 공부하면서 가장 기억에남는 기능이 페이징관련한 인터페이스였다.

취업준비를 하면서 실무에서까지 리스트 페이징을 위해선 항상 공식화된 방법으로 클래스를 구성하고 쿼리를 짰던 것 같다.

 

Data JPA에서 제공하는 페이징인터페이스를 사용하면 비즈니스로직에 필요한 쿼리만 정의하면 알아서 페이징 처리를 해주기 때문에

개발 향상성이 폭발적으로 증대되는것 같다.

 

예제를 구성해보았다.

 

Order 와 Delivery에서 연관관계는 1:1이다  오더엔티티를 기준으로 left join 하였으며 order_id를 기준으로 내림차순 정렬한다.

이 쿼리를 페이징하기위해선 Pageable을 인자로 받고 Page로 래핑해서 리턴하면된다. 

그러면 자동으로 토탈카운트를 세는 쿼리가 같이 날아가게된다.

 

@Query(value = "SELECT o FROM Order o LEFT JOIN o.delivery ORDER BY o.id DESC")
Page<Order> findOrderPage(Pageable pageable);
@GetMapping("jpaPagingTest")
public Page<OrderDto> jpaPagingTest(int pageNum){
    PageRequest page = PageRequest.of(pageNum - 1, 5);
    Page<Order> orderPage = orderDataJpaRepository.findOrderPage(page);
    return orderPage.map(OrderDto::new);
}

다음은 호출하는 쪽이다.

먼저 리포지토리에 보내줄 페이저 객체를 생성한다.

클라이언트로 부터 페이지 번호를 받고 각페이지당 5개씩 보여주도록 세팅하였다.

여기서 페이지번호에 -1을 하였다.

 

jpa에서 페이징은 0부터 시작한다. 클라이언트에서 첫번째 페이지를 요구하면 1을 던져주게 되므로 -1하여 맞춰준것이다.

페이징 객체를 만들어서 리포지토리에 메소드를 호출한 결과는

{
"content": [
{
"orderId": 831,
"name": "userB",
"orderDate": "2023-10-12T14:42:10.971415",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderItems": [
{
"itemName": "SPRING1 BOOK",
"orderPrice": 20000,
"count": 1
},
{
"itemName": "SPRING2 BOOK",
"orderPrice": 40000,
"count": 2
}
]
},
{
"orderId": 828,
"name": "userB",
"orderDate": "2023-10-12T14:42:10.968858",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderItems": [
{
"itemName": "SPRING1 BOOK",
"orderPrice": 20000,
"count": 1
},
{
"itemName": "SPRING2 BOOK",
"orderPrice": 40000,
"count": 2
}
]
},
{
"orderId": 825,
"name": "userB",
"orderDate": "2023-10-12T14:42:10.965677",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderItems": [
{
"itemName": "SPRING1 BOOK",
"orderPrice": 20000,
"count": 1
},
{
"itemName": "SPRING2 BOOK",
"orderPrice": 40000,
"count": 2
}
]
},
{
"orderId": 822,
"name": "userB",
"orderDate": "2023-10-12T14:42:10.962626",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderItems": [
{
"itemName": "SPRING1 BOOK",
"orderPrice": 20000,
"count": 1
},
{
"itemName": "SPRING2 BOOK",
"orderPrice": 40000,
"count": 2
}
]
},
{
"orderId": 819,
"name": "userB",
"orderDate": "2023-10-12T14:42:10.961444",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderItems": [
{
"itemName": "SPRING1 BOOK",
"orderPrice": 20000,
"count": 1
},
{
"itemName": "SPRING2 BOOK",
"orderPrice": 40000,
"count": 2
}
]
}
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"pageNumber": 1,
"pageSize": 5,
"offset": 5,
"paged": true,
"unpaged": false
},
"totalPages": 48,
"totalElements": 240,
"last": false,
"numberOfElements": 5,
"size": 5,
"number": 1,
"first": false,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"empty": false
}

 

결과에서 주목할점은 해당 컨텐츠 뿐만아니라 페이징에 필요한

페이지번호, 토탈페이지 개수, 토탈데이터로우 개수, 마지막,처음페이지여부등 다양한 정보를 리턴받을 수 있다. 

또한 앞서말한거처럼 조인쿼리 뿐만아니라 해당 쿼리를 이용하여 디폴트로 카운트쿼리가 나간다.

 

그리고 컨트롤러에선 page로 감싸져있는 Order는 엔티티 클래스이므로 외부에 절대 노출해서는 안된다.

몇번이고 강조하지만 엔티티를 리턴하는 api는 엔티티가 변경되면 api스펙자체가 변경되므로 클라이언트로에 장애를 유발시킬수 있다.

이를 방지하기위해 page에선 map을 지원하여 다른 오브젝트 타입으로 편리하게 변경시킬 수 있다.

 

 

난 예제의 쿼리를 살짝 변경해보았다. 기존 left join을 left join fetch로 변경하여 쿼리를 실행시켜보려고한다.

delivery 정보를  지연로딩이 아니라 바로 가져오기 위함이다.

 

 

@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.delivery ORDER BY o.id DESC")
Page<Order> findOrderPage(Pageable pageable);

 

이렇게 변경하고 실행했더니 에러가 발생하였다.

 

에러를 보니 해당쿼리가 잘못되었다는 것인데...

 

에러를 보니 

[select count(o) FROM com.example.jpabook.domain.Order o LEFT JOIN FETCH o.delivery]

페이징을 이용했으니 카운트쿼리가 자동으로 발생한 건 정상적이다..

근데 왜 쿼리에서 오류가 났을까?

 

Spring Data JPA가 생성해주는 count 쿼리는 @Query에 명시된 JPQL을 그대로 사용하여 count 쿼리를 생성한다. @Query에서 페치 조인을 사용였기 때문에, count 쿼리에도 페치 조인이 사용된다. 페치 조인은 객체 그래프를 탐색하여 조회하는 기능이기 때문에 조회 결과도 객체 그래프의 엔티티 여야한다. 하지만 카운트 쿼리는 Long타입의 정수를 조회하기 때문에 예외가 발생하는 것이다.
엔티티가 아닌, DTO로 조회할 경우 페치 조인을 사용했을때 예외가 발생하는 것과 같은 이유다.

 

다행히 jpa 페이징에선 카운트 쿼리를 분리, 즉 직접 개발자가 명시할 수 있다.

카운트 쿼리에서 조인자체를 빼버렸다. 왜냐하면 left join은 결국 order를 기준으로 조인하므로 order의 데이터는 절대로 제외 되지 않는다. 그렇기 때문에 카운트 쿼리에 오더의 개수를 구하는 쿼리만 명시한다.

 

@Query(value = "SELECT o FROM Order o LEFT JOIN FETCH o.delivery ORDER BY o.id DESC",
        countQuery = "SELECT count(o) FROM Order o")
Page<Order> findOrderPage(Pageable pageable);

 

 

확실히 jpa에서 페이징은 너무 간편하다. 실무에서도 분명 사용할테니 잘 숙지해야겠다.

'JPA' 카테고리의 다른 글

Data JPA 반환타입  (0) 2023.10.11
Data JPA collection 파라미터 바인딩  (0) 2023.10.11
@Query 값, DTO로 반환받기  (0) 2023.10.11
Data JPA 쿼리메소드  (0) 2023.10.10
변경감지  (0) 2023.10.06