[백엔드] QueryDSL 페이징

2026.02.03. 14:12

1. 스프링(백엔드)

1.1 컨트롤러

@GetMapping("/{slug}")
public ResponseEntity<?> getPostList(
       @PathVariable("slug") String slug
       , PostSearchCondition param
       , Pageable pageable

) {

    Long boardId = boardService.getIdBySlug(slug);
    Page<PostListItem> list = postService.getPostList(boardId, param, pageable);
    return ResponseEntity.ok(list);

}

1.2 서비스 

@Override
public Page<PostListItem> getPostList(
       Long boardId
       , PostSearchCondition param
       , Pageable pageable
){

    return postRepo.findPosts(boardId, param, pageable);

}

1.3 리포지토리 

public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {} // QueryDSL을 사용할 때는 커스텀에 위치
public interface PostRepositoryCustom {  // 쿼리DSL 로직은 커스텀에 위치
    Page<PostListItem> findPosts(Long boardId, PostSearchCondition param, Pageable pageable);
}

1.4 구현체

@RequiredArgsConstructor
public class PostRepositoryCustomImpl implements PostRepositoryCustom{

    //QueryDSL
    private final JPAQueryFactory query;


    @Override
    public Page<PostListItem> findPosts(Long boardId, PostSearchCondition param, Pageable pageable) {

        // 게시글 리스트 조회
        List<PostListItem> list =  query.select(new QPostListItem( //레코드와 순서 맞춰야함.
                post.id,
                post.boardId,
                post.title,
                post.writerId,
                post.createdAt,
                post.updatedAt,
                post.viewcnt
                )
        )
            .from(post)
            .where(
                    post.boardId.eq(boardId)
                    .and(post.title.containsIgnoreCase(param.keyword())) // 검색 조건식
                    .and(post.deleted.isFalse()) //삭제 처리 제외
            )
            .orderBy(getOrderSpecifiers(pageable))
            .offset(pageable.getOffset()) // skip first.  pageNumber × pageSize
            .limit(pageable.getPageSize()) // pagesize
            .fetch(); // 쿼리 실행

        // 게시글 개수 조회
        Long total = query
                .select(post.count())
                .from(post)
                .where(
                        post.boardId.eq(boardId)
                                .and(post.title.containsIgnoreCase(param.keyword())) // 검색 조건식
                                .and(post.deleted.isFalse()) //삭제 처리 제외
                )
                .fetchOne();



        return new PageImpl<>(list, pageable, total == null ? 0 : total);
    }

     // sort 옵션 조회
    private OrderSpecifier<?>[] getOrderSpecifiers(Pageable pageable){

        OrderSpecifier<?>[] orders = pageable.getSort().stream()
                .map(this::toOrderSpecifier)
                .toArray(OrderSpecifier[]::new);

        if(orders.length == 0){
            return  new OrderSpecifier[]{
                    post.createdAt.desc(),
                    post.id.desc()

            };

        }

        return  orders;
    }


    // sort 옵션 분류기
    private OrderSpecifier<?> toOrderSpecifier(Sort.Order order){
        boolean asc = order.isAscending();

        switch (order.getProperty()){
            case "createdAt":
                return asc? post.createdAt.asc() : post.createdAt.desc();
            default :
                return  post.createdAt.desc();
        }


    }


}

2. 프론트 

// 게시글 목록 조회
export const getPostList = async (
    slug:string,
    currentPage:number,
    pageSize:number,
    keyword:string
) => {
    const res = await jsonApi.get(`/api/board/${slug}`, {
        params : {
            page:currentPage,
            size: pageSize,
            sort:  'createdAt,desc',
            keyword: keyword
        },
    } );
    return res.data;
}