본문 바로가기

내일배움캠프

querydsl로 동적 정렬 구현

querydsl로 기존 프로젝트를 리팩토링하는 도중 파라미터의 값에 따라 다른 방법으로 정렬을 하는 동적 정렬을 구현하고 싶어졌다.

 

처음엔 jpaqueryfactory의 orderby메소드에 pageable의 sort 멤버를 넘겨주면 간단히 해결될 줄 알았다.

에러 메세지를 보니 Sort 타입의 파라미터는 받을 수 없다고 한다.

 

그래서 한번 orderBy메소드의 정의를 자세히 보니 OrderSpecfier<?>라는 파라미터를 받도록 되어있었다.

orderBy(board.createdAt.desc())
public OrderSpecifier<T> desc() {
    if (this.desc == null) {
        this.desc = new OrderSpecifier(Order.DESC, this.mixin);
    }

    return this.desc;
}

정적인 정렬기능을 구현할 때 썼던 desc()나 asc()는 OrderSpecfier를 반환하기 때문에 문제없이 구현이 가능했던 것이다.

pageable의 sort는 Sort라는 형태의 아무런 연관도 없는 객체여서 OrderSpecfier로 간단히 변환하는것은 불가능했던 것이다.

처음엔 날짜별 정렬 메소드 따로, 이름별 정렬 메소드 따로, 이런식으로 모든 조건에 맞는 쿼리 메소드를 구현해야 하나 생각했었다. 

하지만 그렇게 모든 컬럼별로 정렬하는 메소드를 일일이 구현하려면 끝도 없을 거 같아서 그 방법은 더 이상 아이디어가 안떠오를 때 최후의 방법으로 사용하기로 하고 다른 방법을 생각해 봤다.

동적 정렬을 구현할 때 핵심은 저 OrderSpecfier를 직접 생성하게 하는 것 같았다. 메소드의 파라미터 별로 다른 종류의OrderSpecfier를 직접 생성하는 것 말이다.

 

일단 OrderSpecifier객체를 직접 생성하기 위해 생성자의 구조를 분석해 봤다.

public OrderSpecifier(Order order, Expression<T> target, NullHandling nullhandling) {
    this.order = order;
    this.target = target;
    this.nullHandling = nullhandling;
}

public OrderSpecifier(Order order, Expression<T> target) {
    this(order, target, OrderSpecifier.NullHandling.Default);
}

NullHandling은 무엇인지 잘 모르겠지만 nullhandling 변수를 주지 않아도 만들 수 있는거 같아서 넘어가고 일단 필요한건 Order객체와 Expression객체였다.

Sort객체에 Order가 리스트로 저장되어 있으니 저걸 활용하면 될 것 같고, 문제는 Expression이었다. Expression이 무슨 역할을 하는건지 몰라서 한번 조사해보기로 했다.

https://uchupura.tistory.com/7

 

[JPA] QueryDsl에서 Pageable 객체를 이용한 Sort 방법

우선 Order, Path, fieldName을 전달하면 OrderSpecifier 객체를 리턴하는 Util 클래스를 작성해서 Sort시 마다 사용할 수 있도록 한다. Path 파라미터는 compileQuerydsl 빌드를 통해서 생성된 Q타입 클래스의 객체

uchupura.tistory.com

위의 블로그에서 참고한 내용에 따르면 Expression 타입의 파라미터는 정렬하는 기준이 되는 엔티티와 컬럼, 정렬 방향이 포함된 Path객체를 넣어야 하는 것 같았다. 

 

그래서 저 블로그에서 나온 정보를 종합해서 OrderSpecfier를 생성하는 메소드를 만들었다.

public class QueryDslUtil {
    public static OrderSpecifier<?> getSortedColumn(Order order, Path<?> parent, String fieldName) {
        Path<Object> fieldPath = Expressions.path(Object.class, parent, fieldName);
        return new OrderSpecifier(order, fieldPath);
    }
}

 

 

getSortedColumn()의 parent 파라미터에는 정렬 기준이 되는 엔티티의 Q클래스가 필요하다고 한다. fieldName은 엔티티의 필드 이름이 들어가고, order는 Order타입의 enum이 들어가면 된다. Order.ASC혹은 Order.DESC중 하나를 받으면 될 것 같다.

 

OrderSpecfier를 반환하는 메소드를 구현했으니 이제 Pageable객체를 OrderSpecfier객체로 반환하는 메소드를 구현할 필요가 있었다.

private List<OrderSpecifier> getOrderSpecifier(Pageable pageable) {
    List<OrderSpecifier> orders = new ArrayList<>();

    if (!isEmpty(pageable.getSort())) {
        for (Sort.Order order : pageable.getSort()) {
            Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC;
            switch (order.getProperty()) {
                case "createdAt":
                    OrderSpecifier<?> orderCreatedAt = QueryDslUtil.getSortedColumn(direction, board, "createdAt");
                    orders.add(orderCreatedAt);
                    break;
                case "user":
                    OrderSpecifier<?> orderUser = QueryDslUtil.getSortedColumn(direction, board, "generatedname");
                    orders.add(orderUser);
                    break;
            }
        }
    }
    return orders;
}

orderBy라는 메소드는 OrderSpecfier타입의 파라미터를 하나만 받을 수도 있고 여러개를 받을 수도 있었다. 정렬 기준이 여러개일 때를 대비한 것 같았다. 지금까지 구현한 코드에는 정렬기준을 여러개 넣지 않지만 정렬기준이 여러개일 것을 대비해 List로 반환하도록 메소드를 구현했다. 만약 정렬 기준인 Sort클래스 속 Order클래스의 property값이 createdAt이면 작성날짜를 기준으로 하고, user면 작성자 이름을 기준으로 하도록 했다.

public Page<Board> getAllBoardsByFollowedUsers(Long userId, Pageable pageable) {
    List<OrderSpecifier> ORDERS = getOrderSpecifier(pageable);
    List<Board> boardList = jpaQueryFactory.selectFrom(board)
            .where(
                    board.user.id.in(
                            jpaQueryFactory.select(follow.followedUser.id)
                                    .from(follow)
                                    .where(follow.user.id.eq(userId))
                                    .fetch())
            )
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(ORDERS.stream().toArray(OrderSpecifier[]::new))
            .fetch();

마지막으로 orderBy에는 여러개의 파라미터를 받을 수 있지만 List가 아닌 배열로만 받아야 한다. 그래서 stream을 활용해 Array로 변환했다.

 

public BoardListResponseDto getAllBoardsByFollowedUsers(User user, int page, int size, String orderby, String direction) {
    Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.valueOf(direction), orderby));
    Page<Board> boardPage = boardRepository.getAllBoardsByFollowedUsers(user.getId(), pageable);
    ...

서비스 클래스에도 정렬 기준이 될 필드이름과 정렬 방향을 받을 수 있도록 수정했다. 만약 orderby의 값이 ASC면 Order.ASC를 대입해서 pageable을 만들고 DESC면 Order.DESC를 대입해서 pageable을 만든다.

@GetMapping("/users/follows")
public ResponseEntity<BoardListResponseDto> getBoardListByFollowedUser(
        @RequestParam(defaultValue = "1") int page, //페이지 번호 파라미터
        @RequestParam(defaultValue = "5") int size,
        @RequestParam(defaultValue = "createdAt") String orderby,
        @RequestParam(defaultValue = "ASC") String direction,
        @AuthenticationPrincipal UserDetailsImpl userDetails) { //페이지당 갯수 파라미터
    BoardListResponseDto response = boardService.getAllBoardsByFollowedUsers(userDetails.getUser(),page -1, size, orderby, direction);
    return ResponseEntity.ok(response);
}

마지막으로 api uri에 정렬 기준 필드이름, 정렬 방향을 받는 queryparameter를 추가했다.

 

작성일자, 작성자이름(익명처리된 이름)기준으로 모두 잘 정렬된 채로 반환이 된다.