[Spring] How - Page, Pageable, PageRequest, Sort를 이용해서 페이징처리.

하쮸·2025년 9월 16일

Error, Why, What, How

목록 보기
29/68

1. 페이징처리.


1-1. Repository.

@Repository
public interface ArticleRepository extends JpaRepository<Article, Integer> {
    @Query("SELECT a FROM Article a WHERE a.status = 1")
    Page<Article> findAllActiveArticle(Pageable pageable);
}
  • 반환타입을 Page<Article> 설정했음.

  • 매개변수에 Pageable이 들어간 이유는 JPA가 SQL 쿼리에 페이징과 정렬 조건을 동적으로 추가.

    • Ex) MySQL일 경우 LIMIT, OFFSET, ORDER BY

1-2. Service.

  @Service
@RequiredArgsConstructor
public class ArticleService {
    private final ArticleRepository articleRepository;

    public Page<Article> getList(int page) {
        Pageable pageable = PageRequest.of(page, 10);
        return articleRepository.findAllActiveArticle(pageable);
    }
    
				.....

}    
  • Controller로부터 전달받은 page를 이용해서 PageRequest 클래스static메서드 of를 호출함.

↓ PageRequest

public class PageRequest extends AbstractPageRequest {
    private final Sort sort;

    protected PageRequest(int pageNumber, int pageSize, Sort sort) {
        super(pageNumber, pageSize);
        Assert.notNull(sort, "Sort must not be null");
        this.sort = sort;
    }
	
    // 1
    public static PageRequest of(int pageNumber, int pageSize) {
        return of(pageNumber, pageSize, Sort.unsorted());
    }

	// 2
    public static PageRequest of(int pageNumber, int pageSize, Sort sort) {
        return new PageRequest(pageNumber, pageSize, sort);
    }
    			
                ....
                
}
  • PageRequest.of(page, 10)코드에서 인자(argument)가 2개이므로 1번 메서드가 호출됨.

    • 1번 메서드는 다시 2번 메서드를 호출해서 최종적으로 PageRequest 생성자를 통해 PageRequest 객체가 생성됨.
  • 생성자 내부에는 super(pageNumber, pageSize);가 있으니 부모클래스인 AbstractPageRequest(추상클래스)의 생성자를 호출함.

↓ AbstractPageRequest

public abstract class AbstractPageRequest implements Pageable, Serializable {
    private static final long serialVersionUID = 1232825578694716871L;
    private final int pageNumber;
    private final int pageSize;

    public AbstractPageRequest(int pageNumber, int pageSize) {
        if (pageNumber < 0) {
            throw new IllegalArgumentException("Page index must not be less than zero");
        } else if (pageSize < 1) {
            throw new IllegalArgumentException("Page size must not be less than one");
        } else {
            this.pageNumber = pageNumber;
            this.pageSize = pageSize;
        }
    }
    
    		.....
            
}
  • 추상클래스의 경우 객체를 생성할 수 없고 AbstractPageRequest의 멤버필드 값을 초기화하는 용도임.
    • 즉, PageRequest는 AbstractPageRequest(추상클래스)를 상속받고 있으므로 상속받을 멤버필드를 초기화하는 용도.
  • 또한 PageRequestPageable 인터페이스를 구현한 구현체이므로 Pageable타입의 참조변수로 참조할 수 있음.
    • Pageable pageable = PageRequest.of(page, 10);
    • 위 코드에서
      pagepageNumber, 즉 조회할 페이지 번호이고.
      10pageSize, 즉 한 페이지당 보여줄 개수를 뜻함.

1-3. Controller

@RequestMapping("/article")
@RequiredArgsConstructor
@Controller
public class ArticleController {
    private final ArticleService articleService;

    /*
        전체 게시판 글 조회.
        
        @Param
        page : 클라이언트에서 요청한 페이지 번호.
        
    */
    @GetMapping("/list")
    public String list(Model model, @RequestParam(value = "page", defaultValue = "0") int page) {
        Page<Article> articlePage = articleService.getList(page);
        model.addAttribute("articlePage", articlePage);
        return "article_list";
    }
    
    			....
                
}
  • ?page=x형태의 쿼리스트링으로 GET 요청이 올 것이기 때문에 URL에서 page(key)에 해당하는 값(value)를 가져오기 위해서 @RequestParam에노테이션을 사용.

    • 만약 URL로부터 page 값을 전달받지 못했을 경우 기본값으로 0을 사용함.
  • Page 인터페이스를 구현한 PageImpl코드를 참고하면 사용할 수 있는 필드와 메서드를 확인할 수 있음.


1-4. View.

					....


        <table class="table">
            <thead class="table-dark">
                <tr>
                    <th>번호</th>
                    <th>제목</th>
                    <th>작성일시</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="article, loop : ${articlePage}">
                    <!-- 번호 -->
                    <td th:text="${loop.count}"></td>
                    <!-- 제목 -->
                    <td>
                        <a th:href="@{|/article/detail/${article.id}|}" th:text="${article.title}"></a>
                    </td>
                    <!-- 작성일시 (#temporals.format(날짜 객체, 날짜 포맷)) -->
                    <td th:text="${#temporals.format(article.createDate, 'yyyy-MM-dd HH:mm')}"></td>
                </tr>
            </tbody>
        </table>

					....

  • http://localhost:8080/article/list?page=0
    • 0 페이지에 해당하는 데이터 10개가 뷰템플릿을 통해 출력되었음.
			.....


        </table>
        <!-- 페이징 처리. -->
        <div th:if="${!articlePage.isEmpty()}">
            <ul class="pagination justify-content-center">
                <li class="page-item" th:classappend="${!articlePage.hasPrevious()} ? 'disabled'">
                    <a class="page-link" th:href="@{|?page=0|}">&laquo;</a>
                </li>
                <li class="page-item" th:classappend="${!articlePage.hasPrevious()} ? 'disabled'">
                    <a class="page-link" th:href="@{|?page=${articlePage.number - 1}|}">
                        <span>&lt;</span>
                    </a>
                </li>
                <li th:each="page : ${#numbers.sequence(0, articlePage.totalPages - 1)}"
                    th:if="${page >= articlePage.number - 5 and page <= articlePage.number + 5}"
                    th:classappend="${page == articlePage.number} ? 'active'" class="page-item">
                    <a th:text="${page + 1}" class="page-link" th:href="@{|?page=${page}|}"></a>
                </li>
                <li class="page-item" th:classappend="${!articlePage.hasNext()} ? 'disabled'">
                    <a class="page-link" th:href="@{|?page=${articlePage.number + 1}|}">
                        <span>&gt;</span>
                    </a>
                </li>
                <li class="page-item" th:classappend="${!articlePage.hasNext()} ? 'disabled'">
                    <a class="page-link" th:href="@{|?page=${articlePage.totalPages - 1}|}">&raquo;</a>
                </li>
            </ul>
        </div>
        <div class="text-end">
            <a th:href="@{/article/create}" class="btn btn-primary">글쓰기</a>
        </div>
    </div>
</html>

PageImpl

public class PageImpl<T> extends Chunk<T> implements Page<T> {
    private static final long serialVersionUID = 867755909294344406L;
    private final long total;

				....

    public int getTotalPages() {
        return this.getSize() == 0 ? 1 : (int)Math.ceil((double)this.total / (double)this.getSize());
    }

    public long getTotalElements() {
        return this.total;
    }

    public boolean hasNext() {
        return this.getNumber() + 1 < this.getTotalPages();
    }
    
    		.....
            
}

Chunk

abstract class Chunk<T> implements Slice<T>, Serializable {
    private static final long serialVersionUID = 867755909294344406L;
    private final List<T> content = new ArrayList();
    private final Pageable pageable;

			....

    public int getNumber() {
        return this.pageable.isPaged() ? this.pageable.getPageNumber() : 0;
    }

    public int getSize() {
        return this.pageable.isPaged() ? this.pageable.getPageSize() : this.content.size();
    }

    public int getNumberOfElements() {
        return this.content.size();
    }

    public boolean hasPrevious() {
        return this.getNumber() > 0;
    }
    
    		....
            
}
th:each="page : ${#numbers.sequence(0, articlePage.totalPages - 1)}"
  • 해당 문장은 반복문임.
  • #numbers : Thymeleaf에서 제공하는 숫자 관련 유틸 객체.
    • sequence(start, end) : start부터 end까지 연속된 숫자 배열 생성함.
${#numbers.sequence(0, 4)} ====>> [0, 1, 2, 3, 4]
  • 따라서 articlePage.totalPages의 값이 5라면, 반복 변수 page에 0, 1, 2, 3, 4가 순서대로 들어감.
    • 즉, 페이지 번호 버튼을 동적으로 생성하는 역할임.
  • -1 하는 이유
    Page 객체는 첫 페이지가 0이라서 두 번째 페이지 = 1,,,,마지막 페이지 = totalPages - 1이 되기 때문임.

1-5. 중간 점검.

    @Test
    void CreateData() {
        for (int i = 0; i < 100; i++) {
            ArticleDto articleDto = new ArticleDto();
            articleDto.setTitle(String.format("임시 데이터 : %05d" ,i));
            articleDto.setContent("임시 데이터");
            articleRepository.save(Article.create(articleDto));
        }
    }
  • 테스트 코드를 이용해서 임시 데이터를 채워줌.


1-6. Service.

  • 기존 Service 코드를 보면 인수(argument)를 2개만 사용해서

↓ PageRequest

public class PageRequest extends AbstractPageRequest {
    private final Sort sort;

    protected PageRequest(int pageNumber, int pageSize, Sort sort) {
        super(pageNumber, pageSize);
        Assert.notNull(sort, "Sort must not be null");
        this.sort = sort;
    }
	
    // 1
    public static PageRequest of(int pageNumber, int pageSize) {
        return of(pageNumber, pageSize, Sort.unsorted());
    }

	// 2
    public static PageRequest of(int pageNumber, int pageSize, Sort sort) {
        return new PageRequest(pageNumber, pageSize, sort);
    }
    			
                ....
                
}
  • 1번 메서드를 호출 -> 2번 메서드가 호출되는 구조였음.
  • 이번에는 정렬조건을 인수(argument)로 하나 추가해서 즉시 2번 메서드를 호출하도록 수정.
    • pageNumber, pageSizesuper()를 통해 부모클래스에 정의된 생성자를 호출해서 필드 값을 초기화하고, sortPageRequest에 있는 필드 값을 초기화하는 용도.
@Service
@RequiredArgsConstructor
@Slf4j
public class ArticleService {
    private final ArticleRepository articleRepository;

    public Page<Article> getList(int page, int size) {
        List<Sort.Order> orderList = new ArrayList<>();
        orderList.add(Sort.Order.desc("createDate"));
        Pageable pageable = PageRequest.of(page, size, Sort.by(orderList));
        Page<Article> articlePage = articleRepository.findAllActiveArticle(pageable);
        if (articlePage.getContent().isEmpty()) {
            log.info("size : {}, page : {}", size, page);
            throw new DataNotFoundException("존재하지 않는 페이지 요청.");
        }
        return articlePage;
    }
    
    ....
    
}
  • Sort 클래스 내부에 있는 Order (Inner Class)에 정의 되어 있는 desc() 메서드를 이용함.
public class Sort implements Streamable<Order>, Serializable {
    private static final long serialVersionUID = 5737186511678863905L;
    private static final Sort UNSORTED = by();
    public static final Direction DEFAULT_DIRECTION;
    private final List<Order> orders;

    protected Sort(List<Order> orders) {
        this.orders = orders;
    }

    private Sort(Direction direction, @Nullable List<String> properties) {
        if (properties != null && !properties.isEmpty()) {
            this.orders = (List)properties.stream().map((it) -> {
                return new Order(direction, it);
            }).collect(Collectors.toList());
        } else {
            throw new IllegalArgumentException("You have to provide at least one property to sort by");
        }
    }
    				.....
                    
    public static class Order implements Serializable {
        private static final long serialVersionUID = 1522511010900108987L;
        private static final boolean DEFAULT_IGNORE_CASE = false;
        private static final NullHandling DEFAULT_NULL_HANDLING;
        private final Direction direction;
        private final String property;
        private final boolean ignoreCase;
        private final NullHandling nullHandling;

        public Order(@Nullable Direction direction, String property) {
            this(direction, property, false, DEFAULT_NULL_HANDLING);
        }

        public Order(@Nullable Direction direction, String property, NullHandling nullHandlingHint) {
            this(direction, property, false, nullHandlingHint);
        }

        public Order(@Nullable Direction direction, String property, boolean ignoreCase, NullHandling nullHandling) {
            if (!StringUtils.hasText(property)) {
                throw new IllegalArgumentException("Property must not be null or empty");
            } else {
                this.direction = direction == null ? Sort.DEFAULT_DIRECTION : direction;
                this.property = property;
                this.ignoreCase = ignoreCase;
                this.nullHandling = nullHandling;
            }
        }

        public static Order by(String property) {
            return new Order(Sort.DEFAULT_DIRECTION, property);
        }

        public static Order asc(String property) {
            return new Order(Sort.Direction.ASC, property, DEFAULT_NULL_HANDLING);
        }

        public static Order desc(String property) {
            return new Order(Sort.Direction.DESC, property, DEFAULT_NULL_HANDLING);
        }
        
     		   .....
    }

				.....
}
  • 바로 위에 있는 asc()는 오름차순 정렬이고 사용할 desc()는 내림차순임.
public static Order desc(String property) {
    return new Order(Sort.Direction.DESC, property, DEFAULT_NULL_HANDLING);
}
        
        ↓ 호출

public Order(@Nullable Direction direction, String property, NullHandling nullHandlingHint) {
    this(direction, property, false, nullHandlingHint);
}
        
        ↓ 호출

public Order(@Nullable Direction direction, String property, boolean ignoreCase, NullHandling nullHandling) {
    if (!StringUtils.hasText(property)) {
        throw new IllegalArgumentException("Property must not be null or empty");
    } else {
        this.direction = direction == null ? Sort.DEFAULT_DIRECTION : direction;
        this.property = property;
        this.ignoreCase = ignoreCase;
        this.nullHandling = nullHandling;
    }
}
  • desc() 호출.

    new Order(Sort.Direction.DESC, property, DEFAULT_NULL_HANDLING) 생성자 호출.

    this(direction, property, false, nullHandlingHint); this()를 이용해서 같은 클래스의 다른 생성자를 호출.

    매개변수의 값들이 각 멤버필드에 저장되는 구조임.

1-7. 뷰템플릿.

			....

        <table class="table">
            <thead class="table-dark">
                <tr>
                    <th>번호</th>
                    <th>제목</th>
                    <th>작성일시</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="article, loop : ${articlePage}">
                    <!-- 번호 -->
                    <td th:text="${articlePage.getTotalElements() - (articlePage.number * articlePage.size) - loop.index}"></td>
                    <!-- 제목 -->
                    <td>
                        <a th:href="@{|/article/detail/${article.id}|}" th:text="${article.title}"></a>
                    </td>
                    <!-- 작성일시 (#temporals.format(날짜 객체, 날짜 포맷)) -->
                    <td th:text="${#temporals.format(article.createDate, 'yyyy-MM-dd HH:mm')}"></td>
                </tr>
            </tbody>
        </table>

			....
  • articlePage.getTotalElements() : 전체 데이터의 개수.
  • articlePage.number : 현재 페이지 번호.
  • articlePage.size : 페이지당 글 수.
  • loop.index : 나열 인덱스 (0부터 시작, 페이지가 바뀔때마다 값이 초기화됨.)
Ex) 현재 총데이터는 503개인데 한 페이지당 50개씩해서 4페이지를 보면?
  • 503 - (4 * 50) = 303. 즉, 5페이지의 첫 게시글 번호는 303.
    • 그 이후부턴 - loop.index로 인해 숫자가 1씩 감소하는식.
    • loop.index는 0부터 시작한다고 했으니
      첫 게시글에서는 -0, 마지막 게시글에서는 -49해서 마지막 게시글 번호는 254번임.

1-8. 최종.



3. PageImpl 클래스

상속계층도는 아래와 같음.

PageImpl 클래스
         
Page 인터페이스, Chunk<T> 추상 클래스

Page 인터페이스
         
Slice<T> 인터페이스
         
Streamable<T> 인터페이스
         
Iterable<T>, Supplier<Stream<T>> 인터페이스

  • 추상클래스 Chunk 상속, 인터페이스 Page 구현한 클래스.
package org.springframework.data.domain;

import java.util.List;
import java.util.function.Function;
import org.springframework.lang.Nullable;

public class PageImpl<T> extends Chunk<T> implements Page<T> {
    private static final long serialVersionUID = 867755909294344406L;
    private final long total;

    public PageImpl(List<T> content, Pageable pageable, long total) {
        super(content, pageable);
        this.total = (Long)pageable.toOptional().filter((it) -> {
            return !content.isEmpty();
        }).filter((it) -> {
            return it.getOffset() + (long)it.getPageSize() > total;
        }).map((it) -> {
            return it.getOffset() + (long)content.size();
        }).orElse(total);
    }

    public PageImpl(List<T> content) {
        this(content, Pageable.unpaged(), null == content ? 0L : (long)content.size());
    }

    public int getTotalPages() {
        return this.getSize() == 0 ? 1 : (int)Math.ceil((double)this.total / (double)this.getSize());
    }

    public long getTotalElements() {
        return this.total;
    }

    public boolean hasNext() {
        return this.getNumber() + 1 < this.getTotalPages();
    }

    public boolean isLast() {
        return !this.hasNext();
    }

    public <U> Page<U> map(Function<? super T, ? extends U> converter) {
        return new PageImpl(this.getConvertedContent(converter), this.getPageable(), this.total);
    }

    public String toString() {
        String contentType = "UNKNOWN";
        List<T> content = this.getContent();
        if (!content.isEmpty() && content.get(0) != null) {
            contentType = content.get(0).getClass().getName();
        }

        return String.format("Page %s of %d containing %s instances", this.getNumber() + 1, this.getTotalPages(), contentType);
    }

    public boolean equals(@Nullable Object obj) {
        if (this == obj) {
            return true;
        } else if (!(obj instanceof PageImpl)) {
            return false;
        } else {
            PageImpl<?> that = (PageImpl)obj;
            return this.total == that.total && super.equals(obj);
        }
    }

    public int hashCode() {
        int result = 17;
        result += 31 * (int)(this.total ^ this.total >>> 32);
        result += 31 * super.hashCode();
        return result;
    }
}

3-1. Chunk<T> 추상 클래스.

package org.springframework.data.domain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.util.Assert;

abstract class Chunk<T> implements Slice<T>, Serializable {
    private static final long serialVersionUID = 867755909294344406L;
    private final List<T> content = new ArrayList();
    private final Pageable pageable;

    public Chunk(List<T> content, Pageable pageable) {
        Assert.notNull(content, "Content must not be null");
        Assert.notNull(pageable, "Pageable must not be null");
        this.content.addAll(content);
        this.pageable = pageable;
    }

    public int getNumber() {
        return this.pageable.isPaged() ? this.pageable.getPageNumber() : 0;
    }

    public int getSize() {
        return this.pageable.isPaged() ? this.pageable.getPageSize() : this.content.size();
    }

    public int getNumberOfElements() {
        return this.content.size();
    }

    public boolean hasPrevious() {
        return this.getNumber() > 0;
    }

    public boolean isFirst() {
        return !this.hasPrevious();
    }

    public boolean isLast() {
        return !this.hasNext();
    }

    public Pageable nextPageable() {
        return this.hasNext() ? this.pageable.next() : Pageable.unpaged();
    }

    public Pageable previousPageable() {
        return this.hasPrevious() ? this.pageable.previousOrFirst() : Pageable.unpaged();
    }

    public boolean hasContent() {
        return !this.content.isEmpty();
    }

    public List<T> getContent() {
        return Collections.unmodifiableList(this.content);
    }

    public Pageable getPageable() {
        return this.pageable;
    }

    public Sort getSort() {
        return this.pageable.getSort();
    }

    public Iterator<T> iterator() {
        return this.content.iterator();
    }

    protected <U> List<U> getConvertedContent(Function<? super T, ? extends U> converter) {
        Assert.notNull(converter, "Function must not be null");
        Stream var10000 = this.stream();
        Objects.requireNonNull(converter);
        return (List)var10000.map(converter::apply).collect(Collectors.toList());
    }

    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (!(obj instanceof Chunk)) {
            return false;
        } else {
            Chunk<?> that = (Chunk)obj;
            boolean contentEqual = this.content.equals(that.content);
            boolean pageableEqual = this.pageable.equals(that.pageable);
            return contentEqual && pageableEqual;
        }
    }

    public int hashCode() {
        int result = 17;
        result += 31 * this.pageable.hashCode();
        result += 31 * this.content.hashCode();
        return result;
    }
}

3-2. Page 인터페이스

  • 조회된 데이터와 페이징 관련 정보를 담는 객체.
    • 즉, 페이징 응답을 담당하는 인터페이스.

상속계층도는 아래와 같음.

Page 인터페이스
         
Slice<T> 인터페이스
         
Streamable<T> 인터페이스
         
Iterable<T>, Supplier<Stream<T>> 인터페이스

package org.springframework.data.domain;

import java.util.Collections;
import java.util.function.Function;

public interface Page<T> extends Slice<T> {
    static <T> Page<T> empty() {
        return empty(Pageable.unpaged());
    }

    static <T> Page<T> empty(Pageable pageable) {
        return new PageImpl(Collections.emptyList(), pageable, 0L);
    }

    int getTotalPages();

    long getTotalElements();

    <U> Page<U> map(Function<? super T, ? extends U> converter);
}

3-3. Slice<T> 인터페이스

package org.springframework.data.domain;

import java.util.List;
import java.util.function.Function;
import org.springframework.data.util.Streamable;

public interface Slice<T> extends Streamable<T> {
    int getNumber();

    int getSize();

    int getNumberOfElements();

    List<T> getContent();

    boolean hasContent();

    Sort getSort();

    boolean isFirst();

    boolean isLast();

    boolean hasNext();

    boolean hasPrevious();

    default Pageable getPageable() {
        return PageRequest.of(this.getNumber(), this.getSize(), this.getSort());
    }

    Pageable nextPageable();

    Pageable previousPageable();

    <U> Slice<U> map(Function<? super T, ? extends U> converter);

    default Pageable nextOrLastPageable() {
        return this.hasNext() ? this.nextPageable() : this.getPageable();
    }

    default Pageable previousOrFirstPageable() {
        return this.hasPrevious() ? this.previousPageable() : this.getPageable();
    }
}

3-4. Streamable<T> 인터페이스

package org.springframework.data.util;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.springframework.util.Assert;

@FunctionalInterface
public interface Streamable<T> extends Iterable<T>, Supplier<Stream<T>> {
    static <T> Streamable<T> empty() {
        return Collections::emptyIterator;
    }

    @SafeVarargs
    static <T> Streamable<T> of(T... t) {
        return () -> {
            return Arrays.asList(t).iterator();
        };
    }

    static <T> Streamable<T> of(Iterable<T> iterable) {
        Assert.notNull(iterable, "Iterable must not be null");
        Objects.requireNonNull(iterable);
        return iterable::iterator;
    }

    static <T> Streamable<T> of(Supplier<? extends Stream<T>> supplier) {
        return LazyStreamable.of(supplier);
    }

    default Stream<T> stream() {
        return StreamSupport.stream(this.spliterator(), false);
    }

    default <R> Streamable<R> map(Function<? super T, ? extends R> mapper) {
        Assert.notNull(mapper, "Mapping function must not be null");
        return of(() -> {
            return this.stream().map(mapper);
        });
    }

    default <R> Streamable<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) {
        Assert.notNull(mapper, "Mapping function must not be null");
        return of(() -> {
            return this.stream().flatMap(mapper);
        });
    }

    default Streamable<T> filter(Predicate<? super T> predicate) {
        Assert.notNull(predicate, "Filter predicate must not be null");
        return of(() -> {
            return this.stream().filter(predicate);
        });
    }

    default boolean isEmpty() {
        return !this.iterator().hasNext();
    }

    default Streamable<T> and(Supplier<? extends Stream<? extends T>> stream) {
        Assert.notNull(stream, "Stream must not be null");
        return of(() -> {
            return Stream.concat(this.stream(), (Stream)stream.get());
        });
    }

    default Streamable<T> and(T... others) {
        Assert.notNull(others, "Other values must not be null");
        return of(() -> {
            return Stream.concat(this.stream(), Arrays.stream(others));
        });
    }

    default Streamable<T> and(Iterable<? extends T> iterable) {
        Assert.notNull(iterable, "Iterable must not be null");
        return of(() -> {
            return Stream.concat(this.stream(), StreamSupport.stream(iterable.spliterator(), false));
        });
    }

    default Streamable<T> and(Streamable<? extends T> streamable) {
        return this.and((Supplier)streamable);
    }

    default List<T> toList() {
        return (List)this.stream().collect(StreamUtils.toUnmodifiableList());
    }

    default Set<T> toSet() {
        return (Set)this.stream().collect(StreamUtils.toUnmodifiableSet());
    }

    default Stream<T> get() {
        return this.stream();
    }

    static <S> Collector<S, ?, Streamable<S>> toStreamable() {
        return toStreamable(Collectors.toList());
    }

    static <S, T extends Iterable<S>> Collector<S, ?, Streamable<S>> toStreamable(Collector<S, ?, T> intermediate) {
        return Collector.of(intermediate.supplier(), intermediate.accumulator(), intermediate.combiner(), Streamable::of);
    }
}

3-5. Iterable<T> 인터페이스

package java.lang;

import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;

/**
 * Implementing this interface allows an object to be the target of the enhanced
 * {@code for} statement (sometimes called the "for-each loop" statement).
 *
 * @param <T> the type of elements returned by the iterator
 *
 * @since 1.5
 * @jls 14.14.2 The enhanced {@code for} statement
 */
public interface Iterable<T> {
    /**
     * Returns an iterator over elements of type {@code T}.
     *
     * @return an Iterator.
     */
    Iterator<T> iterator();

    /**
     * Performs the given action for each element of the {@code Iterable}
     * until all elements have been processed or the action throws an
     * exception.  Actions are performed in the order of iteration, if that
     * order is specified.  Exceptions thrown by the action are relayed to the
     * caller.
     * <p>
     * The behavior of this method is unspecified if the action performs
     * side-effects that modify the underlying source of elements, unless an
     * overriding class has specified a concurrent modification policy.
     *
     * @implSpec
     * <p>The default implementation behaves as if:
     * <pre>{@code
     *     for (T t : this)
     *         action.accept(t);
     * }</pre>
     *
     * @param action The action to be performed for each element
     * @throws NullPointerException if the specified action is null
     * @since 1.8
     */
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    /**
     * Creates a {@link Spliterator} over the elements described by this
     * {@code Iterable}.
     *
     * @implSpec
     * The default implementation creates an
     * <em><a href="../util/Spliterator.html#binding">early-binding</a></em>
     * spliterator from the iterable's {@code Iterator}.  The spliterator
     * inherits the <em>fail-fast</em> properties of the iterable's iterator.
     *
     * @implNote
     * The default implementation should usually be overridden.  The
     * spliterator returned by the default implementation has poor splitting
     * capabilities, is unsized, and does not report any spliterator
     * characteristics. Implementing classes can nearly always provide a
     * better implementation.
     *
     * @return a {@code Spliterator} over the elements described by this
     * {@code Iterable}.
     * @since 1.8
     */
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

3-6. Supplier<T> 인터페이스

package java.util.function;

/**
 * Represents a supplier of results.
 *
 * <p>There is no requirement that a new or distinct result be returned each
 * time the supplier is invoked.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #get()}.
 *
 * @param <T> the type of results supplied by this supplier
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

4. Pageable 인터페이스

  • 페이징 요청을 담당하는 인터페이스.
package org.springframework.data.domain;

import java.util.Optional;
import org.springframework.util.Assert;

public interface Pageable {
    static Pageable unpaged() {
        return unpaged(Sort.unsorted());
    }

    static Pageable unpaged(Sort sort) {
        return Unpaged.sorted(sort);
    }

    static Pageable ofSize(int pageSize) {
        return PageRequest.of(0, pageSize);
    }

    default boolean isPaged() {
        return true;
    }

    default boolean isUnpaged() {
        return !this.isPaged();
    }

    int getPageNumber();

    int getPageSize();

    long getOffset();

    Sort getSort();

    default Sort getSortOr(Sort sort) {
        Assert.notNull(sort, "Fallback Sort must not be null");
        return this.getSort().isSorted() ? this.getSort() : sort;
    }

    Pageable next();

    Pageable previousOrFirst();

    Pageable first();

    Pageable withPage(int pageNumber);

    boolean hasPrevious();

    default Optional<Pageable> toOptional() {
        return this.isUnpaged() ? Optional.empty() : Optional.of(this);
    }

    default Limit toLimit() {
        return this.isUnpaged() ? Limit.unlimited() : Limit.of(this.getPageSize());
    }

    default OffsetScrollPosition toScrollPosition() {
        if (this.isUnpaged()) {
            throw new IllegalStateException("Cannot create OffsetScrollPosition from an unpaged instance");
        } else {
            return this.getOffset() > 0L ? ScrollPosition.offset(this.getOffset() - 1L) : ScrollPosition.offset();
        }
    }
}

5. PageRequest 클래스

  • Pageable 인터페이스를 구현한 클래스.

상속계층도는 아래와 같음.

PageRequest 클래스.
         
AbstractPageRequest 추상클래스
         
Pageable, Serializable 인터페이스

package org.springframework.data.domain;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

public class PageRequest extends AbstractPageRequest {
    private static final long serialVersionUID = -4541509938956089562L;
    private final Sort sort;

    protected PageRequest(int pageNumber, int pageSize, Sort sort) {
        super(pageNumber, pageSize);
        Assert.notNull(sort, "Sort must not be null");
        this.sort = sort;
    }

    public static PageRequest of(int pageNumber, int pageSize) {
        return of(pageNumber, pageSize, Sort.unsorted());
    }

    public static PageRequest of(int pageNumber, int pageSize, Sort sort) {
        return new PageRequest(pageNumber, pageSize, sort);
    }

    public static PageRequest of(int pageNumber, int pageSize, Sort.Direction direction, String... properties) {
        return of(pageNumber, pageSize, Sort.by(direction, properties));
    }

    public static PageRequest ofSize(int pageSize) {
        return of(0, pageSize);
    }

    public Sort getSort() {
        return this.sort;
    }

    public PageRequest next() {
        return new PageRequest(this.getPageNumber() + 1, this.getPageSize(), this.getSort());
    }

    public PageRequest previous() {
        return this.getPageNumber() == 0 ? this : new PageRequest(this.getPageNumber() - 1, this.getPageSize(), this.getSort());
    }

    public PageRequest first() {
        return new PageRequest(0, this.getPageSize(), this.getSort());
    }

    public boolean equals(@Nullable Object obj) {
        if (this == obj) {
            return true;
        } else if (!(obj instanceof PageRequest)) {
            return false;
        } else {
            PageRequest that = (PageRequest)obj;
            return super.equals(that) && this.sort.equals(that.sort);
        }
    }

    public PageRequest withPage(int pageNumber) {
        return new PageRequest(pageNumber, this.getPageSize(), this.getSort());
    }

    public PageRequest withSort(Sort.Direction direction, String... properties) {
        return new PageRequest(this.getPageNumber(), this.getPageSize(), Sort.by(direction, properties));
    }

    public PageRequest withSort(Sort sort) {
        return new PageRequest(this.getPageNumber(), this.getPageSize(), sort);
    }

    public int hashCode() {
        return 31 * super.hashCode() + this.sort.hashCode();
    }

    public String toString() {
        return String.format("Page request [number: %d, size %d, sort: %s]", this.getPageNumber(), this.getPageSize(), this.sort);
    }
}

5-1. 추상클래스 AbstractPageRequest

package org.springframework.data.domain;

import java.io.Serializable;

public abstract class AbstractPageRequest implements Pageable, Serializable {
    private static final long serialVersionUID = 1232825578694716871L;
    private final int pageNumber;
    private final int pageSize;

    public AbstractPageRequest(int pageNumber, int pageSize) {
        if (pageNumber < 0) {
            throw new IllegalArgumentException("Page index must not be less than zero");
        } else if (pageSize < 1) {
            throw new IllegalArgumentException("Page size must not be less than one");
        } else {
            this.pageNumber = pageNumber;
            this.pageSize = pageSize;
        }
    }

    public int getPageSize() {
        return this.pageSize;
    }

    public int getPageNumber() {
        return this.pageNumber;
    }

    public long getOffset() {
        return (long)this.pageNumber * (long)this.pageSize;
    }

    public boolean hasPrevious() {
        return this.pageNumber > 0;
    }

    public Pageable previousOrFirst() {
        return this.hasPrevious() ? this.previous() : this.first();
    }

    public abstract Pageable next();

    public abstract Pageable previous();

    public abstract Pageable first();

    public int hashCode() {
        int prime = true;
        int result = 1;
        result = 31 * result + this.pageNumber;
        result = 31 * result + this.pageSize;
        return result;
    }

    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (obj != null && this.getClass() == obj.getClass()) {
            AbstractPageRequest other = (AbstractPageRequest)obj;
            return this.pageNumber == other.pageNumber && this.pageSize == other.pageSize;
        } else {
            return false;
        }
    }
}

5-2. Pageable 인터페이스

package org.springframework.data.domain;

import java.util.Optional;
import org.springframework.util.Assert;

public interface Pageable {
    static Pageable unpaged() {
        return unpaged(Sort.unsorted());
    }

    static Pageable unpaged(Sort sort) {
        return Unpaged.sorted(sort);
    }

    static Pageable ofSize(int pageSize) {
        return PageRequest.of(0, pageSize);
    }

    default boolean isPaged() {
        return true;
    }

    default boolean isUnpaged() {
        return !this.isPaged();
    }

    int getPageNumber();

    int getPageSize();

    long getOffset();

    Sort getSort();

    default Sort getSortOr(Sort sort) {
        Assert.notNull(sort, "Fallback Sort must not be null");
        return this.getSort().isSorted() ? this.getSort() : sort;
    }

    Pageable next();

    Pageable previousOrFirst();

    Pageable first();

    Pageable withPage(int pageNumber);

    boolean hasPrevious();

    default Optional<Pageable> toOptional() {
        return this.isUnpaged() ? Optional.empty() : Optional.of(this);
    }

    default Limit toLimit() {
        return this.isUnpaged() ? Limit.unlimited() : Limit.of(this.getPageSize());
    }

    default OffsetScrollPosition toScrollPosition() {
        if (this.isUnpaged()) {
            throw new IllegalStateException("Cannot create OffsetScrollPosition from an unpaged instance");
        } else {
            return this.getOffset() > 0L ? ScrollPosition.offset(this.getOffset() - 1L) : ScrollPosition.offset();
        }
    }
}

5-3. Serializable 인터페이스.

public interface Serializable {
}

6. Sort 클래스.

  • TypedSort<T>, Order 내부 클래스(Inner Class)가 존재.
  • Direction, NullHandling 열거형(Enum)이 존재.

상속계층도는 아래와 같음.

Sort 클래스
         
Streamable<T> 인터페이스, Serializable 인터페이스
         
Iterable<T> 인터페이스, Supplier<Stream<T>> 인터페이스

package org.springframework.data.domain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.data.util.MethodInvocationRecorder;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

public class Sort implements Streamable<Order>, Serializable {
    private static final long serialVersionUID = 5737186511678863905L;
    private static final Sort UNSORTED = by();
    public static final Direction DEFAULT_DIRECTION;
    private final List<Order> orders;

    protected Sort(List<Order> orders) {
        this.orders = orders;
    }

    private Sort(Direction direction, @Nullable List<String> properties) {
        if (properties != null && !properties.isEmpty()) {
            this.orders = (List)properties.stream().map((it) -> {
                return new Order(direction, it);
            }).collect(Collectors.toList());
        } else {
            throw new IllegalArgumentException("You have to provide at least one property to sort by");
        }
    }

    public static Sort by(String... properties) {
        Assert.notNull(properties, "Properties must not be null");
        return properties.length == 0 ? unsorted() : new Sort(DEFAULT_DIRECTION, Arrays.asList(properties));
    }

    public static Sort by(List<Order> orders) {
        Assert.notNull(orders, "Orders must not be null");
        return orders.isEmpty() ? unsorted() : new Sort(orders);
    }

    public static Sort by(Order... orders) {
        Assert.notNull(orders, "Orders must not be null");
        return new Sort(Arrays.asList(orders));
    }

    public static Sort by(Direction direction, String... properties) {
        Assert.notNull(direction, "Direction must not be null");
        Assert.notNull(properties, "Properties must not be null");
        Assert.isTrue(properties.length > 0, "At least one property must be given");
        return by((List)Arrays.stream(properties).map((it) -> {
            return new Order(direction, it);
        }).collect(Collectors.toList()));
    }

    public static <T> TypedSort<T> sort(Class<T> type) {
        return new TypedSort(type);
    }

    public static Sort unsorted() {
        return UNSORTED;
    }

    public Sort descending() {
        return this.withDirection(Sort.Direction.DESC);
    }

    public Sort ascending() {
        return this.withDirection(Sort.Direction.ASC);
    }

    public boolean isSorted() {
        return !this.isEmpty();
    }

    public boolean isEmpty() {
        return this.orders.isEmpty();
    }

    public boolean isUnsorted() {
        return !this.isSorted();
    }

    public Sort and(Sort sort) {
        Assert.notNull(sort, "Sort must not be null");
        List<Order> these = new ArrayList(this.toList());
        Iterator var3 = sort.iterator();

        while(var3.hasNext()) {
            Order order = (Order)var3.next();
            these.add(order);
        }

        return by((List)these);
    }

    public Sort reverse() {
        List<Order> reversed = this.doReverse();
        return by(reversed);
    }

    protected List<Order> doReverse() {
        List<Order> reversed = new ArrayList(this.orders.size());
        Iterator var2 = this.iterator();

        while(var2.hasNext()) {
            Order order = (Order)var2.next();
            reversed.add(order.reverse());
        }

        return reversed;
    }

    @Nullable
    public Order getOrderFor(String property) {
        Iterator var2 = this.iterator();

        Order order;
        do {
            if (!var2.hasNext()) {
                return null;
            }

            order = (Order)var2.next();
        } while(!order.getProperty().equals(property));

        return order;
    }

    public Iterator<Order> iterator() {
        return this.orders.iterator();
    }

    public boolean equals(@Nullable Object obj) {
        if (this == obj) {
            return true;
        } else if (obj instanceof Sort) {
            Sort that = (Sort)obj;
            return this.toList().equals(that.toList());
        } else {
            return false;
        }
    }

    public int hashCode() {
        int result = 17;
        result = 31 * result + this.orders.hashCode();
        return result;
    }

    public String toString() {
        return this.isEmpty() ? "UNSORTED" : StringUtils.collectionToCommaDelimitedString(this.orders);
    }

    private Sort withDirection(Direction direction) {
        List<Order> result = new ArrayList(this.orders.size());
        Iterator var3 = this.iterator();

        while(var3.hasNext()) {
            Order order = (Order)var3.next();
            result.add(order.with(direction));
        }

        return by((List)result);
    }

    static {
        DEFAULT_DIRECTION = Sort.Direction.ASC;
    }

    public static enum Direction {
        ASC,
        DESC;

        private Direction() {
        }

        public boolean isAscending() {
            return this.equals(ASC);
        }

        public boolean isDescending() {
            return this.equals(DESC);
        }

        public static Direction fromString(String value) {
            try {
                return valueOf(value.toUpperCase(Locale.US));
            } catch (Exception var2) {
                throw new IllegalArgumentException(String.format("Invalid value '%s' for orders given; Has to be either 'desc' or 'asc' (case insensitive)", value), var2);
            }
        }

        public static Optional<Direction> fromOptionalString(String value) {
            if (ObjectUtils.isEmpty(value)) {
                return Optional.empty();
            } else {
                try {
                    return Optional.of(fromString(value));
                } catch (IllegalArgumentException var2) {
                    return Optional.empty();
                }
            }
        }
    }

    public static class TypedSort<T> extends Sort {
        private static final long serialVersionUID = -3550403511206745880L;
        private final MethodInvocationRecorder.Recorded<T> recorded;

        private TypedSort(Class<T> type) {
            this(MethodInvocationRecorder.forProxyOf(type));
        }

        private TypedSort(MethodInvocationRecorder.Recorded<T> recorded) {
            super(Collections.emptyList());
            this.recorded = recorded;
        }

        public <S> TypedSort<S> by(Function<T, S> property) {
            return new TypedSort(this.recorded.record(property));
        }

        public <S> TypedSort<S> by(MethodInvocationRecorder.Recorded.ToCollectionConverter<T, S> collectionProperty) {
            return new TypedSort(this.recorded.record(collectionProperty));
        }

        public <S> TypedSort<S> by(MethodInvocationRecorder.Recorded.ToMapConverter<T, S> mapProperty) {
            return new TypedSort(this.recorded.record(mapProperty));
        }

        public Sort ascending() {
            return this.withDirection(Sort::ascending);
        }

        public Sort descending() {
            return this.withDirection(Sort::descending);
        }

        private Sort withDirection(Function<Sort, Sort> direction) {
            return (Sort)this.recorded.getPropertyPath().map((xva$0) -> {
                return Sort.by(xva$0);
            }).map(direction).orElseGet(Sort::unsorted);
        }

        public Iterator<Order> iterator() {
            return ((Set)this.recorded.getPropertyPath().map(Order::by).map(Collections::singleton).orElseGet(Collections::emptySet)).iterator();
        }

        public boolean isEmpty() {
            return this.recorded.getPropertyPath().isEmpty();
        }

        public String toString() {
            return ((Sort)this.recorded.getPropertyPath().map((xva$0) -> {
                return Sort.by(xva$0);
            }).orElseGet(Sort::unsorted)).toString();
        }
    }

    public static class Order implements Serializable {
        private static final long serialVersionUID = 1522511010900108987L;
        private static final boolean DEFAULT_IGNORE_CASE = false;
        private static final NullHandling DEFAULT_NULL_HANDLING;
        private final Direction direction;
        private final String property;
        private final boolean ignoreCase;
        private final NullHandling nullHandling;

        public Order(@Nullable Direction direction, String property) {
            this(direction, property, false, DEFAULT_NULL_HANDLING);
        }

        public Order(@Nullable Direction direction, String property, NullHandling nullHandlingHint) {
            this(direction, property, false, nullHandlingHint);
        }

        public Order(@Nullable Direction direction, String property, boolean ignoreCase, NullHandling nullHandling) {
            if (!StringUtils.hasText(property)) {
                throw new IllegalArgumentException("Property must not be null or empty");
            } else {
                this.direction = direction == null ? Sort.DEFAULT_DIRECTION : direction;
                this.property = property;
                this.ignoreCase = ignoreCase;
                this.nullHandling = nullHandling;
            }
        }

        public static Order by(String property) {
            return new Order(Sort.DEFAULT_DIRECTION, property);
        }

        public static Order asc(String property) {
            return new Order(Sort.Direction.ASC, property, DEFAULT_NULL_HANDLING);
        }

        public static Order desc(String property) {
            return new Order(Sort.Direction.DESC, property, DEFAULT_NULL_HANDLING);
        }

        public Direction getDirection() {
            return this.direction;
        }

        public String getProperty() {
            return this.property;
        }

        public boolean isAscending() {
            return this.direction.isAscending();
        }

        public boolean isDescending() {
            return this.direction.isDescending();
        }

        public boolean isIgnoreCase() {
            return this.ignoreCase;
        }

        public Order with(Direction direction) {
            return new Order(direction, this.property, this.ignoreCase, this.nullHandling);
        }

        public Order reverse() {
            return this.with(this.direction == Sort.Direction.ASC ? Sort.Direction.DESC : Sort.Direction.ASC);
        }

        public Order withProperty(String property) {
            return new Order(this.direction, property, this.ignoreCase, this.nullHandling);
        }

        public Sort withProperties(String... properties) {
            return Sort.by(this.direction, properties);
        }

        public Order ignoreCase() {
            return new Order(this.direction, this.property, true, this.nullHandling);
        }

        public Order with(NullHandling nullHandling) {
            return new Order(this.direction, this.property, this.ignoreCase, nullHandling);
        }

        public Order nullsFirst() {
            return this.with(Sort.NullHandling.NULLS_FIRST);
        }

        public Order nullsLast() {
            return this.with(Sort.NullHandling.NULLS_LAST);
        }

        public Order nullsNative() {
            return this.with(Sort.NullHandling.NATIVE);
        }

        public NullHandling getNullHandling() {
            return this.nullHandling;
        }

        public int hashCode() {
            int result = 17;
            result = 31 * result + this.direction.hashCode();
            result = 31 * result + this.property.hashCode();
            result = 31 * result + (this.ignoreCase ? 1 : 0);
            result = 31 * result + this.nullHandling.hashCode();
            return result;
        }

        public boolean equals(@Nullable Object obj) {
            if (this == obj) {
                return true;
            } else if (!(obj instanceof Order)) {
                return false;
            } else {
                Order that = (Order)obj;
                return this.direction.equals(that.direction) && this.property.equals(that.property) && this.ignoreCase == that.ignoreCase && this.nullHandling.equals(that.nullHandling);
            }
        }

        public String toString() {
            String result = String.format("%s: %s", this.property, this.direction);
            if (!Sort.NullHandling.NATIVE.equals(this.nullHandling)) {
                result = result + ", " + this.nullHandling;
            }

            if (this.ignoreCase) {
                result = result + ", ignoring case";
            }

            return result;
        }

        static {
            DEFAULT_NULL_HANDLING = Sort.NullHandling.NATIVE;
        }
    }

    public static enum NullHandling {
        NATIVE,
        NULLS_FIRST,
        NULLS_LAST;

        private NullHandling() {
        }
    }
}

7. 참고

profile
Every cloud has a silver lining.

0개의 댓글