[Spring] How - 페이징에 정렬 기능 추가하기.

하쮸·2025년 10월 20일

Error, Why, What, How

목록 보기
46/68

1. 컬럼.

@Entity
@Getter
public class Article {
    
    
    				.....
    
	@ManyToMany
    private Set<SiteUser> voter;

    @Column(columnDefinition = "integer default 0", nullable = false)
    private int countView;

    @Column(columnDefinition = "integer default 0", nullable = false)
    private int voteCount;
}
  • 정렬을 하기 위해서는 엔터티 내부에 정렬에 사용할 컬럼이 필요함.
    • JPA의 정렬(Sort)은 엔터티 내부 컬렉션의 크기(voter.size()))를 기준으로 정렬할 수 없음.
    • 또한 컬럼이 없다면 레파지토리에서 JOINCOUNT()를 사용해서 값을 가져와야 하는데 컬럼을 추가하는 것이 사용성&성능면에서 더 낫다고 판단했음.

2. Service 메서드 변경.

public Page<Article> getArticleList(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.findAll(pageable);
    if (articlePage.getContent().isEmpty()) {
        log.info("size : {}, page : {}", size, page);
        throw new InvalidPageException(ERROR_PAGE_OUT_OF_ARTICLE_RANGE);
    }
    return articlePage;
}

↑ 기존 코드

  • 현재는 정렬에 createDate(작성일)만 담아서 정렬하고 있음.
    • 이 경우 JPA를 통해 자동적으로 ORDER BY createDate DESC가 적용됨.

↓ 수정한 코드

public Page<Article> getArticleList(int page, int size, String sortType) {
    List<Sort.Order> orderList = new ArrayList<>();
    switch (sortType) {
        case "vote" -> orderList.add(Sort.Order.desc("voteCount"));
        case "view" -> orderList.add(Sort.Order.desc("viewCount"));
        default -> orderList.add(Sort.Order.desc("createDate"));
    }
    // 추천수 or 조회수가 같은 경우를 대비해서 두 번째 정렬 조건을 추가.
    if (!("date".equals(sortType))) {
        orderList.add(Sort.Order.desc("createDate"));
    }
    Pageable pageable = PageRequest.of(page, size, Sort.by(orderList));
    Page<Article> articlePage = articleRepository.findAll(pageable);
    if (articlePage.getContent().isEmpty()) {
        log.warn("size : {}, page : {}", size, page);
        throw new InvalidPageException(ERROR_PAGE_OUT_OF_ARTICLE_RANGE);
    }
    return articlePage;
}
  • page, size, sortType이 유효한 값인지는 Controller에서 검증.
  • 정렬 기준을 담을 List<Sort.Order>타입의 orderList를 만들고
    switch문을 통해 sortType의 값을 체크해서 orderList에 추가함.
    • 이때 추천수 or 조회수가 같을 경우를 대비해서 두 번째 정렬 조건으로 최신순을 넣음.

2-1. 컨트롤러.

  • 컨트롤러 매개변수에 코드 한 줄만 추가하면 됨.
@RequestParam(value = "sortType", defaultValue = "date") String sortType
  • 필요하다면 sortType의 값이 유효한지도 검사.
  • 또한 뷰 템플릿에서 변수의 값이 필요할시 Model에 값을 담아줘야됨.
    • model.addAttribute("sortType", sortType);

2-2. 디버깅으로 확인.

  • 조회순으로 정렬을 요청할 시 정상적으로 sortType에는 view로 되어 있음.

  • switch문을 지나오면 orderList에 값이 하나 들어오게 되고
    countView,DESC(내림차순)이 들어있는 것을 확인.

  • 다음으로 두 번째 정렬 조건을 추가해주기 위한 if문을 지나면
    createDate, DESC(내림차순)역시 들어와 있는 것을 확인할 수 있음.

  • Sort.by()로 인해 Sort클래스의 by()메서드가 호출되었고 매개변수인 orders에 값이 들어있고
    orders를 이용해서 Sort생성자호출.
    • new Sort(orders) 이렇게 객체를 생성하게 됨.
      • List<Order> orders의 경우 Sort클래스의 멤버변수(필드)인 것을 확인.

  • pageable 객체 내부에 sort, pageNumber, pageSize가 있는 것을 확인.
    • 최종적으로 repository의 findAll() 메서드의 인자로 pageable객체를 넣음.

  • Hibernate 쿼리가 정상적으로 나온 것을 확인.
    • ORDER BY에 2개의 정렬이 들어가 있음.

3. 프론트 작업.

<!-- 페이지당 글 수 선택 -->
<div class="d-flex justify-content-start mb-2">
  <form th:action="@{/article/list}" method="get" class="d-flex align-items-center">
    <!-- 페이지당 글 수 선택시 항상 첫 페이지(0) -->
    <input type="hidden" name="page" value="0" />
    <select id="sizeSelect" name="size" class="form-select w-auto" onchange="this.form.submit()">
      <option th:value="10" th:selected="${size == 10}">10개씩</option>
      <option th:value="20" th:selected="${size == 20}">20개씩</option>
      <option th:value="40" th:selected="${size == 40}">40개씩</option>
      <option th:value="50" th:selected="${size == 50}">50개씩</option>
    </select>
  </form>
</div>

↓ 변경

<form th:action="@{/article/list}" method="get" class="d-flex align-items-center gap-2 mb-3">
    <input type="hidden" name="page" value="0" />

    <label for="sizeSelect" class="fw-bold">페이지당:</label>
    <select id="sizeSelect" name="size" class="form-select w-auto" onchange="this.form.submit()">
        <option th:value="10" th:selected="${size == 10}">10개</option>
        <option th:value="20" th:selected="${size == 20}">20개</option>
        <option th:value="40" th:selected="${size == 40}">40개</option>
        <option th:value="50" th:selected="${size == 50}">50개</option>
    </select>

    <label for="sortSelect" class="fw-bold">정렬:</label>
    <select id="sortSelect" name="sortType" class="form-select w-auto" onchange="this.form.submit()">
        <option th:value="date" th:selected="${sortType == 'date'}">최신순</option>
        <option th:value="vote" th:selected="${sortType == 'vote'}">추천순</option>
        <option th:value="view" th:selected="${sortType == 'view'}">조회순</option>
    </select>
</form>
  • 부트스트랩 select를 사용했음.

  • <input type="hidden" name="page" value="0" />

    • 사용자가 페이지당 개수(size)나 정렬 기준(sortType)을 변경하면 자동으로 첫 페이지로 이동.
  • name속성의 값은 쿼리스트링(QueryString)으로 서버로 전송될 값임.

    • 즉, 백엔드 쪽에서는 name속성에 맞게 @RequestParam에노테이션을 이용해서 이름을 맞춰줘야됨.
      @RequestParam(value = "sortType", defaultValue = "date") String sortType
  • onchange="this.form.submit()

    • 사용자가 드롭다운 목록에서 값을 선택하면 해당 폼을 즉시 서버로 요청하여 목록을 자동으로 새로고침함.
  • th:value

    • 각 옵션의 실제 값 (쿼리스트링으로 전달되는 문자열)
  • th:selected="${sortType == 'date'}"

    • 현재 선택된 정렬 기준이 date이면 해당 옵션이 선택 상태로 표시됨.

Ex

  • 현재 URL이 아래와 같을 때.
/article/list?page=5&size=10&sortType=vote
  • 서버에서는 sortType="vote"가 모델에 담겨서 반환될 경우
<option value="vote" selected>추천순</option>
  • 위와 같이 렌더링됨.
profile
Every cloud has a silver lining.

0개의 댓글