본문 바로가기
개인공부/실전 스프링 부트 (Spring-Boot-In-Practice)

3.4.3 ~ 3.4.4 스프링 터이터를 사용한 데이터베이스 접근 - PaginAndSortingRepository

by 응가1414 2024. 3. 11.

3.4.3 PaginAndSortingRepository를 활용한 페이징

  • 페이징(pagination)은 많은 양의 데이터를 ㄹ여러 페이지로 잘게 나눠 조회하는 기법이다.
  • 서버 자원을 효율적으로 이용하면서 사용자에게 필요한 결과를 반환 해줄수 있다.
  • 그래서 많은 양의 데이터를 전부 조회해서 처리하고 반환하는데 대역표과 CPU같은 귀한 자원을 낭비할 필요가 없다.
  • 카테고리의 제품 전부를 항상 조회한다면 자원 낭비와 좋지 않은 사용자 경험을 피하기 어려울 것이다.
  • 스프링 테이터는 페이지 단위로 데이터를 자르고 정렬할 수 있는 PagingAndSortingRepository 인터페이스를 제공한다.
  • PaginAndSortingRepository 인터페이스도 CrudRepository 인터페이스를 상속 받으므로 CRUD 연산을 수행할 수 있다.

3.4.4 PagingAndSortingRepository 인터페이스로 데이터 페이징 및 정렬

  • 이번에는 스프링의 PagingAndSortingRepository 인터페이스를 사용해서 데이터를 페이지 단위로 나누고 정렬하는 방법을 알아본다.

요구사항

  • 많은 양의 데이터를 페이지 단위로 나눠 처리해서 서버 자원 낭비와 애플리케이션 사용자 경험 악화를 방지해야 한다.

해법

  • 페이징은 데이터를 페이지 page라고 부르는 작은 단위로 나눠서 처리하는 기법이다.
  • 하나의 페이지에 몇 개의 데이터를 포함할지는 원하는 대로 정할 수 있다.
  • 선택적으로 페이지 안에 들어 있는 데이터를 오름차순 또는 내림차순으로 정렬하면 사용자 경험 수준을 높일 수 있다.
  • 스프링에서 제공하는 PagingAndSortingRepository 구현체를 사용해서 페이지을 구현하는 방법을 과정 데이터를
  • 페이지 단위로 나눠서 사용자에게 반환하는 예제를 통해 알아본다.

먼제 예제 3.23에 나온 것처럼 PaginAndSortingRepository 인터페이스를 상속받는 CourseRepository인터페이스를 정의한다.

예제 3.23 PaginAndSortingRepository 확장

@Repository
public interface CourseRepository extends PagingAndSortingRepository<Course, Long> {

   // Methods to be added
}

예제 3.24 PagingAndSortingRepository을 사용하는 단위 테스트

@DataJpaTest
class CourseTrackerSpringBootApplicationTests {

    @Autowired
    private CourseRepository courseRepository;

    @Test
    void givenDataAvailableWhenLoadFirstPageThenGetFiveRecords() {
        // 첫 번째 페이지를 가져오는 Pageable 객체를 생성합니다. 페이지 크기는 5로 설정됩니다.
          // 정적 메서드인 PageRequest.of()을 사용해서 페이지 번호와 한 페이지에 나타낼 데이터의 갯수를 지장
        Pageable pageable = PageRequest.of(0, 5);

        // 첫 번째 페이지의 데이터를 가져와서 그 크기가 5인지 확인합니다.
          // CourseRepository의 findAll() 메서드에 pageable 인스턴스를 인자로 전달하면서
          // 첫번째 데이터를 조회한다. Pageable 타입을 인자로 받는 findAll() 메서드는 PagingAndSortingRepository 인터페이스에 정의
        assertThat(courseRepository.findAll(pageable)).hasSize(5);

        // 현재 페이지 번호가 0인지 확인합니다.
        assertThat(pageable.getPageNumber()).isEqualTo(0);

        // 다음 페이지를 가져오는 Pageable 객체를 생성합니다.
          // Pageable 인스턴스의 next() 메서드를 사용해서 다음 페이지의 데이터를 조회해 페이지 번호와 
          // 데이터를 조회해 페이지 번호와 데이터 개수를 판정
        Pageable nextPageable = pageable.next();

        // 다음 페이지의 데이터를 가져와서 그 크기가 4인지 확인합니다.
        assertThat(courseRepository.findAll(nextPageable)).hasSize(4);

        // 다음 페이지의 페이지 번호가 1인지 확인합니다.
        assertThat(nextPageable.getPageNumber()).isEqualTo(1);
    }
}

예제 3.25 같이 PagingAndSortingRepository인터페이스의 정렬 기능을 사용해보자

예제 3.25 페이징과 정렬 예제 - Name 기준으로 정렬

/**
    과정 이름 기준으로 오름차순으로 정렬한 후 목록의 첫 번째 요소를 사용해서 정렬이 바르게 동작 했는지 판정
*/
@Test
void givenDataAvailableWhenSortsFirstPageThenGetSortedSData() {
    // 첫 번째 페이지를 가져오고 이름으로 오름차순 정렬하는 Pageable 객체를 생성합니다. 페이지 크기는 5로 설정됩니다.
    Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Order.asc("Name")));

    // 첫 번째 페이지의 첫 번째 항목이 특정 조건을 만족하는지 확인하는 Condition 객체를 생성합니다.
    Condition<Course> sortedFirstCourseCondition = new Condition<Course>() {
        @Override
        public boolean matches(Course course) {
            // 첫 번째 항목이 ID가 4이고 이름이 "Cloud Native Spring Boot Application Development"인지 확인합니다.
            return course.getId() == 4 && course.getName().equals("Cloud Native Spring Boot Application Development");
        }
    };

    // 첫 번째 페이지를 정렬하여 가져온 후, 첫 번째 항목이 특정 조건을 만족하는지 확인합니다.
    assertThat(courseRepository.findAll(pageable)).first().has(sortedFirstCourseCondition);
}

/**
    과정 평점(rating) 기준 내림차순, 과정 이름 기준 오름차순으로 정렬한 후, 목록의 첫 번째 요소를 사용해서 정렬이 바르게 동작했는지 판정한다.
*/
@Test
void givenDataAvailableWhenApplyCustomSortThenGetSortedResult() {
    // 첫 번째 페이지를 가져오고 Rating을 내림차순으로, 그 다음 Name을 오름차순으로 정렬하는 Pageable 객체를 생성합니다. 페이지 크기는 5로 설정됩니다.
    Pageable customSortPageable = PageRequest.of(0, 5, Sort.by("Rating").descending().and(Sort.by("Name")));

    // 정렬된 결과에서 첫 번째 항목이 특정 조건을 만족하는지 확인하는 Condition 객체를 생성합니다.
    Condition<Course> customSortFirstCourseCondition = new Condition<Course>() {
        @Override
        public boolean matches(Course course) {
            // 첫 번째 항목이 ID가 2이고 이름이 "Getting Started with Spring Security DSL"인지 확인합니다.
            return course.getId() == 2 && course.getName().equals("Getting Started with Spring Security DSL");
        }
    };

    // 사용자 지정 정렬을 적용하여 가져온 결과에서 첫 번째 항목이 특정 조건을 만족하는지 확인합니다.
    assertThat(courseRepository.findAll(customSortPageable)).first().has(customSortFirstCourseCondition);
}

shema.sql

CREATE TABLE COURSES (
  id          BIGINT NOT NULL auto_increment,
  category    VARCHAR(255),
  description VARCHAR(255),
  name        VARCHAR(255),
  rating      INTEGER NOT NULL,
  PRIMARY KEY (id)
);

data.sql

INSERT INTO COURSES(ID, NAME, CATEGORY, RATING, DESCRIPTION)  VALUES(1, 'Rapid Spring Boot Application Development', 'Spring', 4, 'Spring Boot gives all the power of the Spring Framework without all of the complexity');
INSERT INTO COURSES(ID, NAME, CATEGORY, RATING, DESCRIPTION)  VALUES(2, 'Getting Started with Spring Security DSL', 'Spring', 5, 'Learn Spring Security DSL in easy steps');
INSERT INTO COURSES(ID, NAME, CATEGORY, RATING, DESCRIPTION)  VALUES(3, 'Getting Started with Spring Cloud Kubernetes', 'Spring', 3, 'Master Spring Boot application deployment with Kubernetes');
INSERT INTO COURSES(ID, NAME, CATEGORY, RATING, DESCRIPTION)  VALUES(4, 'Cloud Native Spring Boot Application Development', 'Spring', 4, 'Cloud Native Spring Boot');
INSERT INTO COURSES(ID, NAME, CATEGORY, RATING, DESCRIPTION)  VALUES(5, 'Getting Started with Spring Security Oauth', 'Spring', 5, 'Learn Spring Security Oauth in easy steps');
INSERT INTO COURSES(ID, NAME, CATEGORY, RATING, DESCRIPTION)  VALUES(6, 'Spring Boot with Kotlin', 'Spring', 3, 'Master Spring Boot with Kotlin');
INSERT INTO COURSES(ID, NAME, CATEGORY, RATING, DESCRIPTION)  VALUES(7, 'Mastering JS', 'JavaScript', 4, 'Mastering JS');
INSERT INTO COURSES(ID, NAME, CATEGORY, RATING, DESCRIPTION)  VALUES(8, 'Spring Boot with React', 'Spring', 5, 'Spring Boot with React');
INSERT INTO COURSES(ID, NAME, CATEGORY, RATING, DESCRIPTION)  VALUES(9, 'Spring Boot Microservices', 'Spring', 3, 'Spring Boot Microservices');

토론

  • PagingAndSortingRepository 인터페이스를 사용하면 페이징과 정렬을 쉽게 구현할 수 있으므로 아주 유용하다.
  • 예제 3.26에 PagingAndSortingRepository 인터페이스의 소스 코드가 나와 있다.

예제 3.26 PagingAndSortingRepository 인터페이스

@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends Repository<T, ID> {

    /**
     * Returns all entities sorted by the given options.
     *
     * @param sort the {@link Sort} specification to sort the results by, can be {@link Sort#unsorted()}, must not be
     *          {@literal null}.
     * @return all entities sorted by the given options
     */
  // findAll() 메서드는 Sort 인스턴스를 인자로 받는다. 
  // Sort 클래스는 여러 가지 방법으로 정렬 순서를 정의할 수 있도록 아주 다양한 메서드가 포함돼 있다.
  // 예제 3.25의 두 번째 테스트 케이스에서는 Sort의 메서드를 사용해서 두개의 컬럼을 기준으로 
  // 하나는 오름차순, 다른 하나는 내림차순으로 정렬 할 수 있다.
    Iterable<T> findAll(Sort sort);

    /**
     * Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object.
     *
     * @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
     *          {@literal null}.
     * @return a page of entities
     */
    // Pageable 인스턴스를 인자로 받으며,
  // Pageable 인터페이스에는 페이지 요청 객체를 만들 수 있고 페이징 관련 정보를 조회할 수 있는 여러가지 메서드가 포함되어 있다.
    Page<T> findAll(Pageable pageable);
}