[ClassFlix] EP 17. QueryDSL 도입 (페이징, 정렬)
Project/ClassFlix

[ClassFlix] EP 17. QueryDSL 도입 (페이징, 정렬)

Querydsl 도입 계획

  • build.gradle 작성, JPAQueryFactory등 Querydsl 도입
  • 기존에 있는 jpql을 querydsl로 바꾸기
  • 홈 화면에서 페이징, 정렬 기능 (대량 강의 데이터 추가, ddl 설정 변경)
  • 강의 검색기능추가 (페이징, 정렬기능)

 

 

 

Querydsl 도입

 

build.gradle, Q-type파일 생성

 

기존에 작성했던 QueryDSL 설정 포스팅을 참고하여 설정합니다.

 

2021.06.08 - [Web/QueryDSL] - [QueryDSL] EP1. QueryDSL 설정

 

[QueryDSL] EP1. QueryDSL 설정

QueryDSL 사용에 앞서 설정을 해주도록 하겠습니다. 기본적으로 QueryDSL은 start.io 에서 dependency를 제공하지 않기 때문에 사용하기 위해서는 직접 설정해주어야 합니다. build.gradle 설정 plugins //queryds..

ksabs.tistory.com

 

 

기존에 있는 jpql을 querydsl로 바꾸기

를 하려는데 바꿀게 없습니다;;

 

너무 기본적인 조회코드만 사용했던 것 같습니다.

 

그래서 바로 페이징, 정렬기능을 구현해보겠습니다.

 

 

 

홈화면 강의목록 페이징, 정렬

 

강의가 4개밖에 없기 때문에 현재 홈화면에서는 정상적으로 출력되는 것처럼 보입니다.

 

만약 강의가 100개가 넘어간다면 홈화면에서 이 강의들을 모두 다 보여줄 수는 없을 것입니다.

 

이 강의들을 최신순, 이름순 으로 선택 기준을 통해 정렬하고, 페이지 당 16개씩 페이징 할 계획입니다.

 

정렬 기준

  • 최신등록순
  • 이름순
  • 인기순 (클릭순) (추가예정)

 

페이징

  • 한 페이지당 16개씩

 

 

 

페이징은 설계상 파라미터로 전달받는 것이 아니라 16개로 고정입니다.

하지만 16개를 나중을 위해 하드코딩 하지않고 Pageable을 통해 파라미터를 전달받는 것처럼 구현하겠습니다.

 

정렬기준은 condition dto를 만들어서 받아야되나? pageable에서 해야되나? 고민하다가 Pageable에서 공식적으로 getSort()를 지원하는 것을 찾았습니다.

 

그래서 홈화면 강의목록 페이징과 정렬은 Pageable만 잘 사용해주면 구현가능해 보입니다.

 

 

Flow

  1. 홈화면 접속(default 정렬기준은 최신순)
  2. 정렬기준 두가지 버튼중 하나를 클릭시 해당 정렬기준과 페이징파라미터가 home controller에 전달
  3. controller에서는 해당 pageable 객체를 조회쿼리에 전달
  4. 전달된 pageble 기준으로 HomeLectureDto로 강의 리스트를 받아 controller에 다시 전달
  5. model attribute에 담아 view에 데이터 전달

 

 

 

페이징 구현

 

Spring의 Pageable을 이용해서 현재 페이지(offset), 페이지당 개수(limit), 정렬기준을 파라미터로 받아 페이징하는 Querydsl 메서드부터 짜보겠습니다.

 

Data JPA의 LectureRepository에서 사용자 정의 메서드를 구현하는 것이기 때문에 Cutrom Interface와 구현클래스를 만듭니다.

 

LectureRepositoryCustom

package dongho.classflix.repository;

import dongho.classflix.domain.Lecture;
import org.springframework.data.domain.Pageable;

import java.util.List;

public interface LectureRepositoryCustom {
    List<Lecture> findAllPageSort(Pageable pageable);
}

 

 

 

 

LectureRepositoryImpl

 

Pageable을 매개변수로 받아 offset, limit으로 페이징쿼리를 만들고,

조인과 같은 복잡한 쿼리가 아니기 때문에 OrderSpecifier를 이용해 정렬쿼리를 만들어 줍니다.

 

fetch()를 날리고 조회된 Lecture List를 반환합니다.

package dongho.classflix.repository;

import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import dongho.classflix.domain.Lecture;
import dongho.classflix.domain.QLecture;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import javax.persistence.EntityManager;
import java.util.List;

import static dongho.classflix.domain.QLecture.*;

public class LectureRepositoryImpl implements LectureRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public LectureRepositoryImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    @Override
    public List<Lecture> findAllPageSort(Pageable pageable) {
        JPAQuery<Lecture> query = queryFactory
                .selectFrom(lecture)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize());

        for (Sort.Order o : pageable.getSort()) {
            PathBuilder pathBuilder = new PathBuilder(lecture.getType(), lecture.getMetadata());
            query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC,
                    pathBuilder.get(o.getProperty())));
        }

        List<Lecture> result = query.fetch();
        return result;
    }
}

 

 

 

LectureRepository extends

 

LectureRepository에서 LectureRepositoryCustom을 extends해주는 것도 잊지 않고 해줍니다.

package dongho.classflix.repository;

import dongho.classflix.domain.Lecture;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface LectureRepository extends JpaRepository<Lecture, Long>, LectureRepositoryCustom {
    List<Lecture> findByLectureNameAndTeacherName(String lectureName, String teacherName);
}

 

 

 

테스트를 위한 application.yml 분리

 

Main application을 실행하면 InitDB에서 자동으로 데이터를 추가했습니다.

하지만 Test는 Main과 분리되어야 하기 때문에 application.yml을 분리하여 InitDB가 Main이 실행될때만 실행되도록 바꾸어주어야 합니다.

 

아래 포스팅을 통해 바꿔줍니다.

2021.07.15 - [Web/팁] - [Spring] main과 test에서 따로 동작하는 클래스 만들기

 

[Spring] main과 test에서 따로 동작하는 클래스 만들기

개발을 하다보면 main에서만 실행되고 test에서는 실행되길 기대하지 않는 메서드(나 클래스)가 있을 수 있습니다. main 과 test에서 따로 동작하는 클래스를 제작하는 방법에 대해 다뤄봅니다. 방법

ksabs.tistory.com

 

 

 

findAllPageSortTest 작성

 

테스트 강의는 4개를 넣도록 합니다.

 

페이징과 정렬기준

  • page : 1
  • pageSize : 2
  • sort : "createDate", DESC

 

createDate 의 내림차순 (즉 최신순)으로 lecture를 정렬하고, 페이지당 2개씩 반환해줄때 두번째 페이지(Pageable은 0부터 시작)에 해당하는 강의들의 목록을 조회하라

 

lecture1, 2, 3, 4 순으로 등록이 되기 때문에 최신순으로 정렬하면 4 3 2 1 이 순서입니다.

그 중에서 두번째 페이지는 lecture2, lecture1 입니다.

 

package dongho.classflix.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import dongho.classflix.domain.Lecture;
import org.assertj.core.api.Assertions;
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.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
class LectureRepositoryImplTest {

    @Autowired
    EntityManager em;

    @Autowired
    LectureRepository lectureRepository;


    @Test
    public void findAllPageSortTest() throws Exception {
        //given
        Lecture lecture1 = new Lecture("스프링입문", "김영한", "좋아요");
        Lecture lecture2 = new Lecture("스프링코어", "김영한", "나빠요");
        Lecture lecture3 = new Lecture("jpa기초", "김영한", "그냥그래요");
        Lecture lecture4 = new Lecture("jpa활용", "김영한", "좋아요");
        em.persist(lecture1);
        em.persist(lecture2);
        em.persist(lecture3);
        em.persist(lecture4);

        //when
        List<Lecture> results = lectureRepository.findAllPageSort(PageRequest.of(1, 2, Sort.Direction.DESC, "createdDate"));

        //then

        assertAll("page : 1, size : 2, sort : createDate&DESC",
                () -> assertEquals(results.get(0).getId(), lecture2.getId()),
                () -> assertEquals(results.get(1).getId(), lecture1.getId()));


        for (Lecture lecture : results) {
            System.out.println("lecture = " + lecture.getLectureName() + " time : " + lecture.getCreatedDate());
        }
    }

}

 

 

 

 

InitDB에 테스트 데이터 추가

 

페이징이 정상적으로 구현됐는지를 확인하기 위해 100개의 임의의 강의를 더 추가해주었습니다.

package dongho.classflix;

import dongho.classflix.domain.Gender;
import dongho.classflix.domain.Lecture;
import dongho.classflix.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.LocalDateTime;

@Profile("local")
@Component
@RequiredArgsConstructor
public class InitDB {

    private final InitService initService;

    @PostConstruct
    public void init() throws URISyntaxException {
        initService.dbInit1();
    }


    @Component
    @Transactional
    @RequiredArgsConstructor
    static class InitService {
        private final EntityManager em;

        public void dbInit1() throws URISyntaxException {
            URI uri = new URI("https://www.inflearn.com/");

            Lecture lecture1 = new Lecture("스프링입문", "김영한", "좋아요", "인프런", uri);
            Lecture lecture2 = new Lecture("스프링코어", "김영한", "나빠요", "클래스101", uri);
            Lecture lecture3 = new Lecture("jpa기초", "김영한", "그냥그래요", "패스트캠퍼스", uri);
            Lecture lecture4 = new Lecture("jpa활용", "김영한", "좋아요", "Class Flix", uri);
            em.persist(lecture1);
            em.persist(lecture2);
            em.persist(lecture3);
            em.persist(lecture4);

            for (int i = 0; i < 100; i++) {
                em.persist(new Lecture("강의"+i, "김영한", "샘플데이터", "사이트"+i, uri));
            }


            Member member = new Member("김동호", 25, Gender.FEMALE, "학생");
            em.persist(member);

        }
    }
}

 

 

 

HomeController

홈컨트롤러에서 원래는 LectureService의 findAll을 호출했지만, 이번에 페이징과 정렬을 구현한 LectureRepository의 findAllPageSort를 호출하도록 했습니다.

 

Service계층에서 그냥 컨트롤러로 전달만 하는 로직은 과감히 그냥 컨트롤러에서 바로 불러올 수 있게 했습니다.

 

또한, 처음 홈 화면은 페이징과 정렬의 파라미터가 들어가지 않기 때문에 getDefaultPageRequest()를 만들어줘서 맨 처음 홈화면을 호출할때는 최신순으로, 페이지 당 16개씩 보여주도록 했습니다.

 

하지만 이 방법은 하드코딩이기때문에 새로고침이나, 홈화면으로 가는 배너등을 눌렀을때 기존의 정렬과 페이지번호가 없어지고 getDefaultPageRequest()에서 설정해준대로 강의목록을 보여줄 것입니다.

그래서 이 기능은 추후에 현재의 설정을 반영하도록 꼭 수정해주어야 합니다.

 

 

 

 

 

View

이제 View에서 totalCount를 받아 페이지를 표시하고, 페이지나 정렬기준을 눌렀을 때 해당 기능이 동작하도록 구현해야 합니다.

 

 

totalCount를 findAllPageSort에서 같이 반환하도록 수정했습니다.

 

컨트롤러에서 lecture를 HomeLectureDto로 변환해주는 로직을 빼고 Querydsl의 Dto로 반환해주는 방법을 findAllPageSort를 아예 HomeLectureDto로 받아왔습니다.

 

변화된 사항

  • 반환타입 List<Lecture> 에서 Page<HomeLectureDto>
  • totalCount를 구하기 위해 fetch() 에서 fetchResults() 로 바꿈
  • 반환값으로 content, pageable, total 넘겨줌

 

    @Override
    public Page<HomeLectureDto> findAllPageSort(Pageable pageable) {
        JPAQuery<HomeLectureDto> query = queryFactory
                .select(new QHomeLectureDto(
                        lecture.id.as("lectureId"),
                        lecture.representImagePath,
                        lecture.lectureName.as("lectureName"),
                        lecture.averageRating
                ))
                .from(lecture)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize());

        for (Sort.Order o : pageable.getSort()) {
            PathBuilder pathBuilder = new PathBuilder(lecture.getType(), lecture.getMetadata());
            query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC,
                    pathBuilder.get(o.getProperty())));
        }

        QueryResults<HomeLectureDto> results = query.fetchResults();
        List<HomeLectureDto> content = results.getResults();
        long total = results.getTotal();

        return new PageImpl(content, pageable, total);
    }

 

테스트도 수정해주었습니다.

    @Test
    public void findAllPageSortTest() throws Exception {
        //given
        Lecture lecture1 = new Lecture("스프링입문", "김영한", "좋아요");
        Lecture lecture2 = new Lecture("스프링코어", "김영한", "나빠요");
        Lecture lecture3 = new Lecture("jpa기초", "김영한", "그냥그래요");
        Lecture lecture4 = new Lecture("jpa활용", "김영한", "좋아요");
        em.persist(lecture1);
        em.persist(lecture2);
        em.persist(lecture3);
        em.persist(lecture4);

        //when
        PageRequest createdDate = PageRequest.of(1, 2, Sort.Direction.DESC, "createdDate");

        Page<HomeLectureDto> results = lectureRepository.findAllPageSort(createdDate);

        //then
        List<HomeLectureDto> content = results.getContent();
        assertAll("page : 1, size : 2, sort : createDate&DESC",
                () -> assertEquals(content.get(0).getId(), lecture2.getId()),
                () -> assertEquals(content.get(1).getId(), lecture1.getId()));


        for (HomeLectureDto lecture : content) {
            System.out.println("lecture = " + lecture.getLectureName());
        }
    }

 

 

 

 

 

 

 

home.html 설계

 

타임리프를 이용해 아래와 같이 화면에 보여주어야 합니다.

Prev 1 2 3 4 5 6 7 8 9 10 Next

 

  • Prev : 만약 현재 페이지의 전 페이지로 갈 수 있다면 활성화되고 클릭시 1페이지 전으로 이동
  • Next : 만약 현재 페이지의 다음 페이지로 갈 수 있다면 활성화되고 클릭시 1페이지 다음으로 이동
  • Pageable에서는 페이징이 0부터 시작이지만 view에서는 1부터 시작이어야 함
  • 만약 총 15페이지가 존재한다면, 10페이지에서 다음페이지를 누르면 11, 12 13, 14, 15페이지가 나와야 함

 

 

 

 

PageDto

 

설계

home.html에서 사용할 model에 넘겨줄 PageDto를 생성해야합니다.

 

view에서 필요한 정보

변수 역할 계산방법
pageSize 페이지당 몇 개의 데이터를 표시할 것인지 입력받음 (pageable에서 뽑음)
startPage 시작 페이지 번호 (현재 페이지 기준으로) endPage 일의 자리에서 올림 후 - 9
endPage 끝 페이지 번호 (현재 페이지 기준으로) ceil(전체 개수 / pageSize)
curPage 현재 페이지 번호
(view에서는 1부터
PageDto에서는 0부터)
입력받음 (pageable에서 뽑음)
Prev, Next 이전, 다음 페이지 존재 여부 Prev : view에서 현재 페이지가 1보다 크면 true
Next : view에서 현재 페이지가 마지막페이지보다 작으면 true

 

 

만약 total 강의의 수가 204개이고 pageSize : 16 이면 총 페이지가 13개가 나와야합니다. (204 나누기 16 = 12.75)

 

그런데,

현재 페이지가 7이라면 startPage : 1, endPage : 10이 나와야하고

현재 페이지가 12이라면 startPage : 11, endPage : 13이 나와야합니다.

 

 

바뀌는 값인 강의의 개수(total)와 pageable 객체를 생성자의 매개변수로 받고 view에서 사용할 데이터들을 계산해줍니다.

  • startPage
  • endPage
  • Prev
  • Next
package dongho.classflix.controller.dto;

import lombok.Data;
import org.springframework.data.domain.Pageable;

@Data
public class PageDto {
    private final int PAGENUM = 10; // 페이지 몇개로 구성할건지
    private int pageSize; // 페이지당 몇개 표시할건지
    private int startPage;
    private int endPage;
    private int curPage;
    private boolean prev, next;

    private long total;

    public PageDto() {
    }

    public PageDto(long total, Pageable pageable) {
        this.total = total;
        this.curPage = pageable.getPageNumber();
        this.pageSize = pageable.getPageSize();

        this.endPage = (int) (Math.ceil((curPage+1) / 10.0)) * 10; // 일단 endPage를 10단위로 세팅, view는 1부터 시작이므로 curPage+1
        this.startPage = this.endPage - (PAGENUM - 1); // 10단위 endPage에서 9를 빼면 시작페이지 구할 수 있음

        int realEnd = (int) (Math.ceil((total * 1.0) / pageSize));

        if (realEnd < this.endPage) { // 페이지가 10단위로 나누어 떨어지지 않을때 real endPage
            this.endPage = realEnd;
        }

        this.prev = (curPage+1) > 1; // view에서는 1부터 시작이므로
        this.next = (curPage+1) < realEnd; // view에서는 1부터 시작이므로
    }
}

 

 

HomeController

 

강의개수와 pageable을 넣어주어 PageDto를 생성하며 model에 attribute를 넘겨줍니다.

package dongho.classflix.controller;

import dongho.classflix.controller.dto.HomeLectureDto;
import dongho.classflix.controller.dto.PageDto;
import dongho.classflix.repository.LectureRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequiredArgsConstructor
@Slf4j
public class HomeController {
    private final LectureRepository lectureRepository;

    @RequestMapping("/")
    public String home(Model model, @PageableDefault(size = 16, sort = "createdDate",direction = Sort.Direction.DESC) Pageable pageable) {
        log.info("home controller");

        Page<HomeLectureDto> results = lectureRepository.findAllPageSort(pageable);
        model.addAttribute("lectures", results.getContent());
        model.addAttribute("page", new PageDto(results.getTotalElements(), pageable));

        return "home";
    }

}

 

 

 

 

 

 

Home.html

 

  • 타임리프의 if, unless를 이용해 Prev, Next의 표시를 해줍니다.
  • PageDto의 startPage, endPage를 이용해 반복문을 만들며 페이지 번호를 보여주고 링크도 만듭니다.
<div class="text-center">
  <nav aria-label="Page navigation">
    <ul class="pagination pagination-sm">
    <li th:if="${page.isPrev()}" class="page-item"><a th:href="@{/?page={page}(page = ${page.getCurPage()-1})}" class="page-link" href="#">Prev</a></li>
    <li th:unless="${page.isPrev()}" class="page-item disabled"><a class="page-link">Prev</a></li>
    <li class="page-item" th:each="num, index: ${#numbers.sequence(page.getStartPage(), page.getEndPage())}">
      <a th:href="@{/?page={page}(page = ${index.current-1})}" th:text="${num}" class="page-link" href="">1</a>
    </li>
    <li th:if="${page.isNext()}" class="page-item"><a th:href="@{/?page={page}(page = ${page.getCurPage()+1})}" class="page-link" href="#">Next</a></li>
    <li th:unless="${page.isNext()}" class="page-item disabled"><a class="page-link">Next</a></li>
    </ul>
  </nav>
</div>

 

반복자를 이용할때 타임리프에서 제공해주는 상태변수와 -1, +1을 적절히 이용하면 페이징 처리를 쉽게 할 수 있습니다.

2021.04.01 - [Web/MVC] - EP8. Thymeleaf 타임리프

 

EP8. Thymeleaf 타임리프

타임리프 사용선언 속성변경 대부분의 HTML 속성을 th:xxx 로 변경할 수 있다. 변수 표현식 10000 모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다. 프로퍼티 접근법을 사용한다.

ksabs.tistory.com

 

 

 

 

 

 

애플리케이션 테스트

 

샘플 강의 데이터 204개를 미리 넣고 테스트를 해보겠습니다. (총 13페이지)

package dongho.classflix;

import dongho.classflix.domain.Gender;
import dongho.classflix.domain.Lecture;
import dongho.classflix.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import java.net.URI;
import java.net.URISyntaxException;

@Profile("local")
@Component
@RequiredArgsConstructor
public class InitDB {

    private final InitService initService;

    @PostConstruct
    public void init() throws URISyntaxException {
        initService.dbInit1();
    }


    @Component
    @Transactional
    @RequiredArgsConstructor
    static class InitService {
        private final EntityManager em;

        public void dbInit1() throws URISyntaxException {
            URI uri = new URI("https://www.inflearn.com/");

            Lecture lecture1 = new Lecture("스프링입문", "김영한", "좋아요", "인프런", uri);
            Lecture lecture2 = new Lecture("스프링코어", "김영한", "나빠요", "클래스101", uri);
            Lecture lecture3 = new Lecture("jpa기초", "김영한", "그냥그래요", "패스트캠퍼스", uri);
            Lecture lecture4 = new Lecture("jpa활용", "김영한", "좋아요", "Class Flix", uri);
            em.persist(lecture1);
            em.persist(lecture2);
            em.persist(lecture3);
            em.persist(lecture4);

            for (int i = 0; i < 200; i++) {
                em.persist(new Lecture("강의"+i, "김영한", "샘플데이터", "사이트"+i, uri));
            }


            Member member = new Member("김동호", 25, Gender.FEMALE, "학생");
            em.persist(member);

        }
    }
}

 

 

 

1페이지에서 Prev 비활성화 확인

 

10페이지에서 Prev, Next활성화 확인. Next를 눌러보겠습니다.

 

 

 

Prev, Next여전히 활성화 잘 되어있고 13페이지까지 보이는 것을 확인했습니다.

13페이지를 한번 눌러보겠습니다.

 

 

Next가 비활성화 되어있고 마지막 강의까지 잘 보입니다.

 

 

 

 

중간의 아무강의를 누르고, 리뷰가 잘 등록되는 것도 확인되었습니다.

 

강의 목록에서도 별점이 잘 보입니다.

 

 

 

 

 

 

정렬기준추가

 

정렬기준은 2가지로 정했습니다.

  • 최신순
  • 이름순

 

설계

  1. 홈 화면의 강의 목록에서 정렬 기준을 선택하면 해당 정렬기준으로 강의가 나옵니다.
  2. 정렬 기준을 선택하면 현재의 페이지가 쿼리파라미터로 같이 날아갑니다.
  3. 페이지를 선택해도 마찬가지로 현재 정렬기준이 쿼리파라미터로 같이 날아갑니다.

 

 

 

PageDto 수정

 

  • sortParam : 현재의 정렬기준을 쿼리파라미터로 넘길 수 있도록 문자열로 저장합니다. (ex. "createdDate,DESC")
  • 생성자 : pageable에서 property와 ASC인지 DESC인지를 받아 문자열로 저장합니다.
public class PageDto {
    ...
    
    private String sortParam;

    public PageDto() {
    }

    public PageDto(long total, Pageable pageable) {
        
        ...
        
        for (Sort.Order o : pageable.getSort()) {
            this.sortParam = o.getProperty() + ",";
            if (o.isAscending()) {
                this.sortParam += "ASC";
            } else {
                this.sortParam += "DESC";
            }
            break;
        }
    }
}

 

솔직히 pageable.getSort()를 저렇게 반복문 형식으로 받아도 되는지 잘 모르겠습니다.

페이징 파라미터를 계속 유지하는 것은 MVC2 강의를 듣고 난 다음 다시 리팩토링 해야할 사항일 것 같습니다.

 

 

home.html

정렬기준을 누르면 현재 페이지와 누른 정렬기준에 맞는 파라미터로 링크를 만들어 이동합니다.

<div class="text-right">
	<a class="btn btn-default" role="button"
		th:href="@{/(page=${page.getCurPage()},sort='createdDate,DESC')}" href="">최신순</a>
	<a class="btn btn-default" role="button"
		th:href="@{/(page=${page.getCurPage()},sort='lectureName,ASC')}" href="">이름순</a>
</div>

 

 

페이지 번호 부분에서도 (Prev, Next도 포함) 페이지 번호를 누르면 해당 번호만 날아가는 것이 아니라 현재 정렬기준도 같이 파라미터로 날아갑니다.

<ul class="pagination pagination-sm">
    <li th:if="${page.isPrev()}" class="page-item"><a th:href="@{/?page={page}(page = ${page.getCurPage()-1}, sort = ${page.getSortParam()})}" class="page-link" href="#">Prev</a></li>
    <li th:unless="${page.isPrev()}" class="page-item disabled"><a class="page-link">Prev</a></li>
    <li class="page-item" th:each="num, index : ${#numbers.sequence(page.getStartPage(), page.getEndPage())}">
        <a th:href="@{/?page={page}(page = ${index.current-1}, sort = ${page.getSortParam()})}" th:text="${num}" class="page-link" href="">1</a>
    </li>
    <li th:if="${page.isNext()}" class="page-item"><a th:href="@{/?page={page}(page = ${page.getCurPage()+1}, sort = ${page.getSortParam()})}" class="page-link" href="#">Next</a></li>
    <li th:unless="${page.isNext()}" class="page-item disabled"><a class="page-link">Next</a></li>
</ul>

 

 

오른쪽 위에 최신순, 이름순 정렬기준 버튼이 생긴 모습.

 

 

이름순을 클릭하니 sort파라미터로 lectureName과 ASC가 같이 나갑니다.

 

6페이지를 클릭했을때에도 정렬기준이 바뀌지 않고 유지됩니다.

 

 

 

 

고민점

 

아래 두가지 고민점이 있습니다.

  • 주소창에 정렬기준이 계속 노출되어있음
  • 노출된 sort 파라미터가 실제 엔티티의 멤버 이름

현재 위험한 상황이지만 서버사이드렌더링이고 쿠키없이 구현해보고 있기 때문에 발생하는 문제라고 추측됩니다.

 

물론 노출되는 파라미터가 실제 엔티티의 멤버이름인 것은 지금도 노출되지 않도록 변경할 수 있지만, 쿠키나 세션을 학습하고 난 뒤에 해결될 수도 있는 문제이기 때문에 일단 지금은 넘어가고 쿠키학습뒤 다시 리팩토링 해보겠습니다.

 

 

 

 

 

 

새로운 기능을 추가했으니 All Test 초록불도 확인하고 페이징 처리를 마무리 합니다.