본문 바로가기
내일배움캠프

내일 배움 캠프 TIL -QueryDSL을 사용해서 수정일 순으로 정렬해서 가져오자

by Kiwimel0n 2024. 3. 11.

개인과제로 만들었었던 프로젝트로 기본적인 스프링 부트 crud 구현에서 조회 부분에 querydsl을 적용해보려고 한다.

 

QueryDsL 이란

정적 타입을 이용하여, SQL과 같은 쿼리를 코드형태로 생성할 수 있도록 해주는 오픈소스 빌더 API이다.
JPQL처럼 query문을 문자열로 작성하는 하드 코딩이 아닌 코드형태로 작성을하여 컴파일 시간에 에러가 식별되어 리스크를 줄일 수가 있다.


JPQL을 사용한 quey문

public interface PersonRepository extends JpaRepository<Person, Long>{

	/*	변수 바인딩 시, ?시퀀스 사용하는 경우 */
   @Query("select p from Person p where p.firstName = ?1 and p.lastName = ?2")
   Person findPerson(String firstName, String lastName);

   /*	변수 바인딩 시, :이름 사용하는 경우 */
   @Query("select p from Person p where p.firstName = :firstName and p.lastName = :lastName")
   Person findPerson2(@Param("firstName") String firstName, @Param("lastName") String lastName);

}

 

QueryDSL을 사용한 query문

 

@PersistenceContext
EntityManager em;

public List<Person> selectPersonByNm(String firstNm, String lastNm){
	JPAQueryFactory jqf = new JPAQueryFactory(em);
	QPerson person = QPerson.person;

	List<Person> personList = jpf
				.selectFrom(person)
				.where(person.firstName.eq(firstNm)
				.and(person.lastName.eq(lastNm))
				.fetch();

	return personList;
}

 

 

QueryDSL을 사용하기 앞서 QueryDSL을 연동을 해주어야하는데 의존성과 설정을 조금 추가해주어야한다.

 

참고 레퍼런스

https://velog.io/@juhyeon1114/Spring-QueryDsl-gradle-%EC%84%A4%EC%A0%95-Spring-boot-3.0-%EC%9D%B4%EC%83%81

 

[Spring] QueryDsl gradle 설정 (Spring boot 3.0 이상)

스프링 부트 3.0이상에서의 Querydsl 설정방법

velog.io

 

의존성과 설정후에는 gradle의 빌드를 통하여

QueryDSL에서 query를 위해 사용할 Qclass가 생성이된다.

 

Q객체란?

 

JPAAnnotationProcessor가 컴파일 시점에 @Entity와 @Embeddedable과 같은 어노테이션이 걸린 클래스에 대해, 미리 생성하는 쿼리 객체

해당 객체에 쿼리문을 메서드 형태로 호출하여 SQL문을 짜듯이 코드로 쿼리문 작성이 가능하다.

 

 

@Override
    @Transactional(readOnly = true)
    public List<TodoListResponseDto> getAllTodo() {
        return toDoRepository.findAll()
                .stream()
                .map(TodoListResponseDto::new)
                .sorted(Comparator.comparing(TodoListResponseDto::getModifiedAt, Comparator.nullsLast(Comparator.reverseOrder())))
                .toList();
    }

 

이것이 리팩토리 전 service단에서 사용될 모든 todo라는 게시글을 가져오는 메서드로 서비스단에서 정렬을 하게 되었다. 이 정렬을 레포지토리 단에서 정렬해서 가져오기 위하여 그리고 page 기능을 추가하여 페이지를 적용시켜 보겠다.

 

먼저 QueryDsL을 이용하여 repository에서 가져올 때 Todo를 modifiedAt순으로 가져오게 리팩토링을 하였다.

 

public interface TodoRepository extends JpaRepository<Todo, Long> , TodoRepositoryQuery{

}

//TodoRepositoryQuery를 todoRepository가 상속하게 만들고

public interface TodoRepositoryQuery {
    List<Todo> findAllTodo();

}

// 인터페이스를 구현해주었다.


// 구현체를 통하여 구현을 해주고
@Repository
@RequiredArgsConstructor
public class TodoRepositoryQueryImpl implements TodoRepositoryQuery{

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public List<Todo> findAllTodo(Pageable pageable){
        QTodo qTodo = todo;

        OrderSpecifier<?> orderSpecifier = new OrderSpecifier<>(Order.DESC, todo.modifiedAt);

       return jpaQueryFactory
               .select(qTodo)
               .from(qTodo)
               //.orderBy(qTodo.modifiedAt.desc().nullsLast())
               //처음 강의에서는 직접 넣어서 수정일 순으로 구현을 하였지만.
               //다른강의에서는 OrderSpecifier을 이용하여 수정일 내림차순을 구현하게되었다.
               .orderBy(orderSpecifier)
               .fetch();
    }
   
}

// 서비스에서 구현을 완료해주었다.
@Override
    @Transactional(readOnly = true)
    public List<TodoListResponseDto> getAllTodo() {
        return todoRepository.findAllTodo()
                .stream()
                .map(TodoListResponseDto::new)
                .collect(Collectors.toList());
    }

 

 

이렇게 OrderSpecifier을 이용하여 동적인 정렬을 사용하면 좋은 장점은
코파일럿 의 답변으로는

  1. 동적 정렬: OrderSpecifier를 활용하면 동적으로 정렬 기준을 설정할 수 있습니다. 즉, 사용자의 요청에 따라 다양한 칼럼을 기준으로 정렬할 수 있습니다. 예를 들어, 조회수 순, 최신순, 스크랩 순 등 다양한 정렬 조건을 동적으로 처리할 수 있습니다.
  2. 페이징과 함께 사용: OrderSpecifier를 통해 정렬 정보를 생성하면, 페이징 처리와 함께 사용할 수 있습니다. 사용자가 원하는 정렬 순서를 적용하여 페이징된 결과를 반환할 수 있습니다.
  3. Expression 객체 활용: OrderSpecifier는 정렬 기준이 되는 칼럼의 Path를 표현하는 Expression 객체를 필요로 합니다. 이를 통해 정렬할 엔티티의 칼럼과 그 칼럼이 속한 엔티티를 합쳐서 표현합니다. 예를 들어, postId를 정렬 기준으로 설정하면, Post 엔티티의 postId Path가 됩니다

 

그 이후 pageable을 구현하기 위하여 PageDTO를 만들어주고 리스트를 불러오는 부분에 pagable을 구현해주었다.

 

@RequiredArgsConstructor
@AllArgsConstructor
@Builder
public class PageDTO {

    private final Integer currentPage;
    private final Integer size;
    private String sortBy;

    public Pageable toPageable() {
        if (Objects.isNull(sortBy)) {
            return PageRequest.of(currentPage - 1, size);
        } else {
            return PageRequest.of(currentPage - 1, size, Sort.by(sortBy).descending());
        }
    }

    public Pageable toPageable(String sortBy) {
        return PageRequest.of(currentPage - 1, size, Sort.by(sortBy).descending());
    }
}

pageDTO에서 우리는 페이지가 1부터 시작하지만 시스템에서는 0부터 시작하기때문에 그것에 맞춰 구현해주기위해서 -1을 해주었다.

public interface TodoRepositoryQuery {
    List<Todo> findAllTodo(Pageable pageable);

}

//
@Repository
@RequiredArgsConstructor
public class TodoRepositoryQueryImpl implements TodoRepositoryQuery{

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public List<Todo> findAllTodo(Pageable pageable){
        QTodo qTodo = todo;

        OrderSpecifier<?> orderSpecifier = new OrderSpecifier<>(Order.DESC, todo.modifiedAt);

       return jpaQueryFactory
               .select(qTodo)
               .from(qTodo)
               //.orderBy(qTodo.modifiedAt.desc().nullsLast())
               .orderBy(orderSpecifier)
               .offset(pageable.getOffset())
               .limit(pageable.getPageSize())
               .fetch();

    }

}

//
@Override
@Transactional(readOnly = true)
public List<TodoListResponseDto> getAllTodo(Pageable pageable) {
    return todoRepository.findAllTodo(pageable)
            .stream()
            .map(TodoListResponseDto::new)
            .collect(Collectors.toList());
  }

API요청에서 pageDTO를 받아와 컨트롤러에서 pageDTO.topageable()로 가져와 offset과 limit로 조회할 데이터 시작위치와 한페이조회할 데이터수를 가져와서 pageable을 구현하였다.