be_better

[Springboot] Spring Data JPA 활용 - 정렬과 페이징 본문

Springboot/이론

[Springboot] Spring Data JPA 활용 - 정렬과 페이징

NiceTry 2023. 11. 10. 16:46

목차

    정렬과 페이징 처리

    애플리케이션에서 자주 사용되는 정렬과 페이징 처리는 쿼리 메서드를 작성하는 방법을 기반으로 수행할 수 있다.

     

    정렬 처리하기

    일반적인 쿼리문에서 정렬을 사용할 때는 ORDER BY 구문을 사용한다. 쿼리 메서드도 정렬 기능에 동일한 키워드가 사용된다.

    // Asc : 오름차순, Desc : 내림차순
    List<Product> findByNameOrderByNumberAsc(String name);
    List<Product> findByNameOrderByNumberDesc(String name);

    기본 쿼리 메서드를 작성한 후 OrderBy 키워드를 삽입하고 정렬하고자 하는 칼럼과 오름차순/내림차순을 설정하면 정렬이 수행된다. 오름차순으로 정렬하려면 Asc 키워드를, 내림차순으로 정렬하려면 Desc 키워드를 사용한다.

     

    다른 쿼리 메서드들과는 달리 정렬 구문은 And나 Or 키워드를 사용하지 않고 우선순위를 기준으로 차례대로 작성할 수 있다.

    // And를 붙이지 않음
    List<Product> findByNameOrderByPriceAscStockDesc(String name);

    먼저 Price를 기준으로 오름차순 정렬한 후 후순위로 재고수량을 기준으로 내림차순 정렬을 수행한다.

     

    이렇게 쿼리 메서드의 이름에 정렬 키워드를 삽입해서 정렬을 수행하는 것도 가능하지만 메서드의 이름이 길어질수록 가독성이 떨어지는 문제가 생긴다. 이를 해결하기 위해 매개변수를 활용해 정렬할 수도 있다.

    List<Product> findByName(String name, Sort sort);

    위 코드는 앞서 소개한 정렬 키워드가 들어간 메서드와 거의 동일한 기능을 수행하지만 이름에 키워드를 넣지 않고 Sort 객체를 활용해 매개변수로 받아들인 정렬 기준을 가지고 쿼리문을 작성하게 된다.

     

    Test를 위한 ProductRespositoryTest

    ProductRespository에서 command+shift+T 를 눌러 Test코드를 생성할 수 있다.

    package com.springboot.jpa.data.repository;
    
    import com.springboot.jpa.data.entity.Product;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.domain.Sort;
    
    import java.time.LocalDateTime;
    
    @SpringBootTest
    class ProductRepositoryTest {
    
        @Autowired
        ProductRepository productRepository;
    
        @Test
        void sortingAndPagingTest() {
            Product product1 = new Product();
            product1.setName("펜");
            product1.setPrice(1000);
            product1.setStock(100);
            product1.setCreatedAt(LocalDateTime.now());
            product1.setUpdatedAt(LocalDateTime.now());
    
            Product product2 = new Product();
            product2.setName("펜");
            product2.setPrice(5000);
            product2.setStock(300);
            product2.setCreatedAt(LocalDateTime.now());
            product2.setUpdatedAt(LocalDateTime.now());
    
            Product product3 = new Product();
            product3.setName("펜");
            product3.setPrice(500);
            product3.setStock(50);
            product3.setCreatedAt(LocalDateTime.now());
            product3.setUpdatedAt(LocalDateTime.now());
    
            Product savedProduct1 = productRepository.save(product1);
            Product savedProduct2 = productRepository.save(product2);
            Product savedProduct3 = productRepository.save(product3);
    
            productRepository.findByName("펜", Sort.by(Sort.Order.asc("price")));
            productRepository.findByName("펜", Sort.by(Sort.Order.asc("price"), Sort.Order.desc("stock")));
        }
    
    }

    Sort 클래스는 내부 클래스로 정의돼 있는 Order 객체를 활용해 정렬 기준을 생성한다. Order 객체에는 asc와 desc 메서드가 포함돼 있어 이 메서드를 통해 오름차순/내림차순을 지정하며, 여러 정렬 기준을 사용할 경우에는 콤마(,)를 사용해 구분한다.

     

    하이버네이트에서 생성한 쿼리는 다음과 같다.

    Hibernate: 
        select
            product0_.number as number1_0_,
            product0_.createdAt as createda2_0_,
            product0_.name as name3_0_,
            product0_.price as price4_0_,
            product0_.stock as stock5_0_,
            product0_.updatedAt as updateda6_0_ 
        from
            product product0_ 
        where
            product0_.name=? 
        order by
            product0_.price asc
    Hibernate: 
        select
            product0_.number as number1_0_,
            product0_.createdAt as createda2_0_,
            product0_.name as name3_0_,
            product0_.price as price4_0_,
            product0_.stock as stock5_0_,
            product0_.updatedAt as updateda6_0_ 
        from
            product product0_ 
        where
            product0_.name=? 
        order by
            product0_.price asc,
            product0_.stock desc

    매개변수를 활용한 쿼리 메서드를 사용하면 쿼리 메서드를 정의하는 단계에서 코드가 줄어든 장점이 있다. 그러나 호출하는 위치에서는 여전히 정렬 기준이 길어져 가독성이 떨어진다. 정렬 기준을 설정하는 코드는 필수적인 구문이기 때문에 생략할 수 는 없다. 이와 같은 문제는 다음과 같이 Sort 부분을 하나의 메서드로 분리해서 쿼리 메서드를 호출하는 코드를 작성하는 방법도 가능하다.

     

    @SpringBootTest
    class ProductRepositoryTest {
    
        @Autowired
        ProductRepository productRepository;
    
        @Test
        void sortingAndPagingTest() {
        	.. 상단 코드 생략..
            
            productRepository.findByName("펜", getSort());
        }
        
        private Sort getSort() {
            return Sort.by(
                    Sort.Order.asc("price"),
                    Sort.Order.desc("stock")
            );
        }
    
    }

     

    페이징 처리

    페이징이란 데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것을 의미한다. 예를 들면, 25개의 레코드가 있다면 레코드를 7개씩, 총 4개의 페이지로 구분하고 그중에서 특정 페이지를 가져오는 것이다.

     

    JPA에서는 이 같은 페이징 처리를 위해 Page와 Pageable을 사용한다.

    Page<Product> findByName(String name, Pageable pageable);

    위와 같이 리턴 타입으로 Page를 설정하고 매개변수는 Pageable 타입의 객체를 정의한다. 해당 메서드를 사용하기 위해서는 다음과 같이 호출한다.

    Page<Product> productPage = productRepository.findByName("펜", PageRequest.of(0, 2));

    Pageable 파라미터를 전달하기 위해 사용된 PageRequest 구현체는 of 메서드를 통해 PageRequest 객체를 생성하고 of 메서드는 매개변수에 따라 다양한 형태로 오버로딩돼 있는데 다음과 같은 매개변수 조합을 지원한다.

    of 메서드 매개변수 설명 비고
    of(int page, int size) 페이지 번호(0부터 시작), 페이지당 데이터 개수 데이터를 정렬하지 않음
    of(int page, int size, Sort) 페이지 번호, 페이지당 데이터 개수, 정렬 sort에 의해 정렬
    of(int page, int size, Direction, String... properties) 페이지 번호, 페이지당 데이터 개수, 정렬 방향, 속성(칼럼) Sort.by(direction, properties)에 의해 정렬

     

    위의 메서드가 호출될 때 하이버네이트에서 생성하는 쿼리를 확인하면 limit과 offset으로 레코드 목록을 구분해서 가져오게 한다.

    Hibernate: 
        select
            product0_.number as number1_0_,
            product0_.createdAt as createda2_0_,
            product0_.name as name3_0_,
            product0_.price as price4_0_,
            product0_.stock as stock5_0_,
            product0_.updatedAt as updateda6_0_ 
        from
            product product0_ 
        where
            product0_.name=? limit ?
    Hibernate: 
        select
            count(product0_.number) as col_0_0_ 
        from
            product product0_ 
        where
            product0_.name=?