질문, 피드백 등 모든 댓글 환영합니다.
JPA로 엔티티를 조회할 경우 일반적으로 id를 기준으로 조회하므로 asc(오름차순) 정렬이 기본적으로 사용됩니다.
보통 커뮤니티 게시글을 살펴보면 게시글 리스트는 주로 내림차순으로 조회하고 상세 게시글에 달린 댓글은 오름차순으로 조회합니다. (ex) 네이버 카페
)
저 또한 위의 방식대로 정렬해 주겠습니다.
스프링 데이터 JPA 를 사용한다면 정렬하는 방법은 크게 3가지가 있습니다.
@Query
어노테이션에서 직접 order by 작성만약 회원을 조회할 때 이름을 기준으로 내림차순 정렬을 하고 싶다면
@Query("select m from Member m order by m.name desc")
처럼 작성할 수 있습니다.
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 값을 가집니다.
- 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개의 반환 타입을 가질 수 있습니다.
List<T> : 엔티티만 포함
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)
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 라이브러리를 도입하여 동적 쿼리 기반 검색 기능을 구현하겠습니다.