Spring Example: Community #7 정렬, 페이징

함형주·2023년 1월 10일
0

질문, 피드백 등 모든 댓글 환영합니다.

JPA로 엔티티를 조회할 경우 일반적으로 id를 기준으로 조회하므로 asc(오름차순) 정렬이 기본적으로 사용됩니다.

보통 커뮤니티 게시글을 살펴보면 게시글 리스트는 주로 내림차순으로 조회하고 상세 게시글에 달린 댓글은 오름차순으로 조회합니다. (ex) 네이버 카페)

저 또한 위의 방식대로 정렬해 주겠습니다.

정렬

스프링 데이터 JPA 를 사용한다면 정렬하는 방법은 크게 3가지가 있습니다.

  1. @Query 어노테이션에서 직접 order by 작성

만약 회원을 조회할 때 이름을 기준으로 내림차순 정렬을 하고 싶다면
@Query("select m from Member m order by m.name desc") 처럼 작성할 수 있습니다.

  1. 쿼리 메서드 파라미터로 Sort 사용

JPARepository에서 리스트 형식으로 엔티티를 조회하는 경우 인자로 Sort, Pageable을 사용할 수 있습니다. (Pageable은 내부에 Sort 포함)

Sort 객체는 클래스 내부의 static 메서드인 Sort.by()를 이용하여 생성할 수 있습니다.
Sort.by()는 아래와 같이 구현되어 있습니다.

	Sort by(String... properties)
	Sort by(List<Order> orders)
	Sort by(Order... orders)
	Sort by(Direction direction, String... properties)

propertis로 정렬의 기준이 될 테이블 컬럼을 지정할 수 있습니다.

Direction은 Sort 내부에 정의 되어있는 ENUM 으로 ASC, DESC 값을 가집니다.

  1. Pageable 사용

Pageable(Interface) 은 위에서 언급했듯이 내부에 Sort를 포함하고 있고 자체적으로 페이징 기능도 가지고 있습니다. 스프링이 제공하는 페이징, 정렬 기능을 표준화한 인터페이스로 스프링과 결합하여 사용할 수 있도록 유용한 기능을 제공합니다.

Repository에서 조회할 때에는 Sort와 마찬가지로 파라미터로 Pageable을 포함하면 페이징과 정렬을 적용하여 엔티티를 조회할 수 있습니다.

Pageable이 유용한 이유는 ArgumentResolver가 값을 생성할 수 있기 때문에 컨트롤러에서 파라미터로 사용할 수 있다는 점입니다. 이 때 Pageable 구현체인 PageRequest가 생성됩니다.

PageRequest는 Sort와 int page, int size를 필드로 가지고 있고 요청 파라미터로부터 아래의 값을 전달받으면 ArgumentResolver가 이 값을 자동으로 매핑해주어 편리하게 사용할 수 있습니다. 참고로 page는 0부터 시작합니다.

또한 @PageableDefault를 사용하여 해당 값을 자유롭게 설정할 수 있습니다.

	int value() default 10;
	int size() default 10;
	int page() default 0;
	String[] sort() default {};
	Direction direction() default Direction.ASC;

저는 이후 페이징 기능도 구현할 것이므로 3번의 Pageable을 사용하여 정렬하겠습니다.
(정렬 기능은 간단하니 페이징 기능을 구현할 때 한 번에 적용하겠습니다.

페이징

스프링 데이터 JPA를 사용할 때에는 대부분 Pageable을 사용하여 페이징 기능을 구현합니다.

Pageable을 사용하면 쿼리 메서드의 결과로 3개의 반환 타입을 가질 수 있습니다.

  1. List<T> : 엔티티만 포함

  2. Slice<T> : 엔티티와 페이징과 관한 정보 포함, count 쿼리 발생 x

	int getNumber(); //현재 페이지
	int getSize(); //페이지 크기
	int getNumberOfElements(); //현재 페이지에 나올 데이터 수
	List<T> getContent(); //조회된 데이터
	boolean hasContent(); //조회된 데이터 존재 여부
	Sort getSort(); //정렬 정보
	boolean isFirst(); //현재 페이지가 첫 페이지 인지 여부
	boolean isLast(); //현재 페이지가 마지막 페이지 인지 여부
	boolean hasNext(); //다음 페이지 여부
	boolean hasPrevious(); //이전 페이지 여부
	Pageable getPageable(); //페이지 요청 정보
	Pageable nextPageable(); //다음 페이지 객체
	Pageable previousPageable();//이전 페이지 객체
	<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기

Slice의 경우 실제로 조회해야 할 size 보다 하나 더 조회하여 hasPrevious() 값을 생성합니다. (하나 더 있으면 true)

  1. Page<T> : Slice를 상속 받은 클래스로, count 쿼리 발생 및 관련 정보 포함
	int getTotalPages(); //전체 페이지 수
	long getTotalElements(); //전체 데이터 수
	<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기

Page의 가장 큰 특징은 count 쿼리가 발생한다는 점입니다. 여기서 조심해야할 점이 count 쿼리는 모든 데이터를 조회해야 하므로 성능이 떨어지므로 특히 다수의 join이 발생하는 경우 성능 이슈가 발생할 수 있습니다.

이럴 경우 count 쿼리를 분리해서 생성하는 것이 유리합니다. @Query를 사용하면 쉽게 count 쿼리를 분리할 수 있습니다.

@Query(value = “select m from Member m”,
countQuery = “select count(m.username) from Member m”)

적용

화면에 관한 로직(Html, Thymeleaf)는 다음 블로그에서 기술할 검색 기능을 구현할 때 한 번에 적용하겠습니다.

PostController

public class PostController {

    @GetMapping("/post")
    public String postList(Model model,
                           @PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
        Page<PostListDto> postList = postService.findList(pageable);
        model.addAttribute("postListDto", postList.getContent());

        return "post/list";
    }
}

기존의 postList()를 수정해줍니다. Pageable에 매핑될 값은 HTTP 쿼리 파라미터를 통해 Get으로 받아주겠습니다. 메서드 파라미터로 Pageable을 등록하고 postService.findList()를 호출할 때 인자로 넘겨줍니다.

저는 한 번에 10개의 게시글을 조회하길 원하기에 size=10 (default)로 사용하겠습니다. 또한 id를 기준으로 정렬한 이유는 post.createdTime 과 post.id의 순번이 동일하기 때문에 간단하게 id를 기준으로 정렬했습니다.

PostService

public class PostService {

    public Page<PostListDto> findList(Pageable pageable) {
        Page<Post> find = postRepository.findPostList(pageable);
        return find.map(PostListDto::new);
    }
}

Page는 내부의 엔티티를 DTO로 변경할 수 있는 map() 메서드를 제공합니다. 이를 쉽게 만들어 줄 수 있도록 PostListDto 클래스에 Post를 파라미터로 하는 생성자를 만들어주겠습니다.

@Getter @Setter
@AllArgsConstructor
public class PostListDto {

    private Long id;
    private String title;
    private String membername;
    private int HeartNum;
    private int commentNum;
    private LocalDateTime createdDate;

    public PostListDto(Post post) {
        this.id = post.getId();
        this.title = post.getTitle();
        this.membername = post.getMember().getName();
        this.HeartNum = post.getHearts().size();
        this.commentNum = post.getComments().size();
        this.createdDate = post.getCreatedDate();
    }
}

PostRepository

public interface PostRepository extends JpaRepository<Post, Long> {

    @Query(value = "select p from Post p left join fetch p.member",
    countQuery = "select count (p) from Post p")
    Page<Post> findPostList(Pageable pageable);
}

기존 findPostList()에 Pageable 파라미터를 추가하고 @Query에선 count 쿼리를 분리시켜 주었습니다.

확인

"/post" 접속 시 발생하는 쿼리 로그 :

(아래에 지연로딩으로 Comment, Heart가 조회되지만 이 둘은 이전과 달라지는 부분이 없어 사진에 포함하지 않았습니다.)

조회 쿼리와 count 쿼리가 분리되는 것을 볼 수 있고 조회 쿼리에 order by, limit가 적용된 것을 볼 수 있습니다.


"/post" 접속 시 화면 :

제목이 1~12 까지 존재하는데 12~3 까지 내림차순으로 10개가 조회된 것을 볼 수 있습니다.


"/post?page=1" 접속 시 화면 :

(page는 0부터 시작) 다음 페이지 요청 시 뒤에 남은 2~1 게시글을 조회할 수 있습니다.

다음으로

정렬, 페이징 기능을 구현했고 여기에 더해 검색 기능까지 구현해보겠습니다.
스프링 데이터 JPA만을 사용하여 검색 기능을 개발하는 것이 쉽지 않습니다.(수많은 if문의 향연...) 때문에 QueryDsl 라이브러리를 도입하여 동적 쿼리 기반 검색 기능을 구현하겠습니다.

github , 배포 URL (첫 접속 시 로딩이 걸릴 수 있습니다.)

profile
평범한 대학생의 공부 일기?

0개의 댓글