SQL 동적 정렬이란?

하나의 API에서 정렬 조건을 동적으로 변경해, 정렬 혹은 정렬 + 페이징을 진행하는 것을 의미합니다. 예시를 보며, 필요한 상황이 언제이며, 어떻게 해결하는지 알아가 보겠습니다. 

 

어떤 경우에 필요할까?

우리는 Querydsl만을 통해서가 아닌 Springboot Data JPA를 통해서도 쉽게, 페이징과 정렬을 할 수 있습니다. 하지만 문제 되는 경우는 같은 API 호출임에도 불구하고, 내림 차순 or 오름 차순과 같이 동적으로 정렬이 바뀌는 경우가 존재합니다. 물론 API를 2개 만들어 호출하면 문제없습니다. 하지만 리소스, 동작이 동일한데 API를 분리하는 것은 복잡성을 증가시킬 뿐입니다. 2가지의 방법으로 해결할 수 있습니다. 

 

테스트 셋팅

 

Person이라는 Entity가 존재하고, name, age, region 3개의 칼럼을 가지고 있습니다.

정렬 조건 =  null -> name 오름차순

정렬 조건 = age -> age 오름차순

정렬 조건 = region -> region 오름차순

으로 진행하는 상황으로 가져가 보겠습니다. 

저는 더미 데이터를 삽입 후 진행했습니다. 

 

1. Entity

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Parent{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "parent_id")
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "age", nullable = false)
    private int age;

    @Column(name = "region")
    private String region;

    public static Parent createParent(String name, int age) {
        Parent parent = new Parent();
        parent.name = name;
        parent.age = age;
        return parent;
    }
}

 

2. PersionController

 

@RestController
@RequiredArgsConstructor
@RequestMapping("/persons")
public class PersonController {

    private final PersonService personService;


    @GetMapping
    public List<Person> getParent(OrderConditionRequest orderConditionRequest){
        return personService.findAllPersonOrderBy(orderConditionRequest.getOrderCondition());
    }

    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    public static class OrderConditionRequest{

        OrderCondition orderCondition;

    }
}

 

 

해결 방법

 

1.  SQL을 3개 작성하고, 분기 처리한다. 

 

가장 간단한 방법으로 아래와 같이 SQL을 3가지 작성하면 됩니다. Service계층에서 정렬 조건에 대해 null 검증을 진행한 후에 각 조건에 맞는 SQL을 실행하면 됩니다. 

 

1-2. PersonService

 

@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class PersonService {

    private final PersonRepository personRepository;

    public List<Person> findAllPersonOrderBy(OrderCondition orderCondition) {
        if (Objects.nonNull(orderCondition) && orderCondition.equals(OrderCondition.AGE)) {
            return personRepository.findAllByOrderByAgeDesc();
        } else if (Objects.nonNull(orderCondition) && orderCondition.equals(OrderCondition.REGION)) {
            return personRepository.findAllByOrderByRegionDesc();
        } else {
            return personRepository.findAll(Sort.by(Sort.Direction.DESC, "name"));
        }
    }

}

 

1-3. 실행 결과

1. 아무런 조건을 주지 않았을 때 name의 오름 차순 정렬

 

2. OrderCondition으로 age를 줬을 때

 

 

3. OrderCondition으로 Region를 줬을 때

 

 

 

이렇게 Service 로직에서 분기를 이용해서 쿼리를 선택해 동적 정렬을 진행할 수 있습니다. 하지만 문제점이 있습니다. 

 

- 정렬 조건이 많아진다면??

- 정렬 조건이 계속해서 변화한다면??

- 정렬뿐 아니라 페이징이 적용됐다면?

 

계속해서 Service 로직을 변경하고, 쿼리 또한 계속해서 변경해야 합니다. 무엇보다 가장 큰 문제는 페이징이 적용됐을 때 쿼리 단에서 해결할 수 있어야 합니다.

이러한 문제점을 해결할 수 있는 것은 Querydsl의 OrderSpecifier입니다. 

 

2. OrderSpecifier 적용

 

Querydsl 설정법은 넘어가고 바로 시작하겠습니다.

 

@RequiredArgsConstructor
@Repository
public class PersonQuerydslRepository {

    private final JPAQueryFactory jpaQueryFactory;


    public List<Person> findAll(OrderCondition orderCondition){
        OrderSpecifier[] orderSpecifiers = createOrderSpecifier(orderCondition);

        return jpaQueryFactory.selectFrom(person)
                .orderBy(orderSpecifiers)
                .fetch();
    }

    private OrderSpecifier[] createOrderSpecifier(OrderCondition orderCondition) {

        List<OrderSpecifier> orderSpecifiers = new ArrayList<>();

        if(Objects.isNull(orderCondition)){
            orderSpecifiers.add(new OrderSpecifier(Order.DESC, person.name));
        }else if(orderCondition.equals(OrderCondition.AGE)){
            orderSpecifiers.add(new OrderSpecifier(Order.DESC, person.age));
        }else{
            orderSpecifiers.add(new OrderSpecifier(Order.DESC, person.region));
        }
        return orderSpecifiers.toArray(new OrderSpecifier[orderSpecifiers.size()]);
    }
}

 

OrderSpecifier 객체를 만들고, Querydsl의 그래프 탐색을 통해서 손쉽게 정렬 조건을 추가할 수 있습니다. 여기서 

OrderSpecifier 리스트로 선언한 이유는 현재 한 개의 정렬 조건이지만, 여러 개의 정렬 조건이 추가될 수도 있기에 배열 선언하고 넣어줍니다. (페이징이 필요하시면 넣으시면 됩니다.)

 

테스트 Region 오름차순 

 

 

 

추가적으로 페이징 + 여러 개의 정렬 조건이 필요하다면 아래와 같이 작성하시면 됩니다. 

public List<Person> findAll(OrderCondition orderCondition) {
    OrderSpecifier[] orderSpecifiers = createOrderSpecifier(orderCondition);

    Pageable pageable = PageRequest.of(0, 10);

    return jpaQueryFactory.selectFrom(person)
            .orderBy(orderSpecifiers)
            .limit(pageable.getPageSize())
            .offset(pageable.getOffset())
            .fetch();
}

private OrderSpecifier[] createOrderSpecifier(OrderCondition orderCondition) {
    List<OrderSpecifier> orderSpecifiers = new ArrayList<>();

    orderSpecifiers.add(new OrderSpecifier(Order.DESC, person.name));
    orderSpecifiers.add(new OrderSpecifier(Order.DESC, person.region));

    return orderSpecifiers.toArray(new OrderSpecifier[orderSpecifiers.size()]);
}

 

정리

 

동적 정렬을 진행하는 방법에 대해서 알아봤습니다. 단순히 Service 계층에서 분기 처리 후 정렬하는 것에는 유지보수와 페이징이 더해졌을 때 진행이 어렵습니다. 따라서 OrderSpecifier를 사용해서 동적으로 정렬을 진행하고, 페이징까지 진행할 수 있는 것을 살펴볼 수 있었습니다.