쌓고 쌓다

QueryDSL로 검색과 페이징 API 요청 처리하기 본문

프로그래밍/spring

QueryDSL로 검색과 페이징 API 요청 처리하기

승민아 2024. 2. 13. 17:32

구현하고자하는 기능은 다음과 같다.

 

1. 지도에 표시할 주위 장소들을 위한 요청 ( 페이징 X )

  • /locations?latitude=위도&longitude=경도 : 위도 경도로 주위의 정보를 페이징 처리하지 않고 한 페이지에 모두 출력

 

2. 게시글 목록을 위한 주위 장소들을 위한 요청 ( 정렬, 페이징 O )

  • /locations?latitude=위도&longitude=경도&search=내용
  • /locations?latitude=위도&longitude=경도&sort=like
  • /locations?latitude=위도&longitude=경도&search=내용&sort=like

정렬과 검색을 따로 또는 함께 요청이 가능하며 페이징 처리된 응답을 받길 원한다.

이때 page와 size는 기본값으로 설정된다.

 

3. 2번의 요청(위의 요청)에 페이지 디테일 포함

  • /locations?latitude=위도&longitude=경도&search=내용&page(또는 size)=1

page 또는 size만 지정하여 요청할 수 있으며 지정하지 않은 값은 기본값으로 설정된다.

 

4. 모든 조건 파라미터 포함

/locations
?latitude={위도값}
&longitude={경도값}
&size={페이지크기}
&page={페이지번호}
&sort={정렬방법}
&search={제목 또는 내용}

 

 

 

LocationConditionRequest

@Data
public class LocationConditionRequest {

    @Max(value = 30, message = "max size : 30")
    private Integer size;
    private Integer page;
    private String sort;
    private String search;

}

 

위와 같이 쿼리 파라미터로 들어온 요청을 매핑하기 위한 LocationConditionRequest를 만든다.

이것을 이용해 페이징 처리를 진행한다.

 

LocationRepositoryImpl

@Override
public Page<LocationResponse> searchLocations(Double latitude,
                                              Double longitude,
                                              LocationConditionRequest conditionRequest) {

    int defaultSize = 10;
    int defaultPage = 1;

    QLocation location = QLocation.location;
    QLocationLike locationLike = QLocationLike.locationLike;

    StringPath likeCount = Expressions.stringPath("like_count");

    Pageable pageable = null;

    Integer page = conditionRequest.getPage();
    Integer size = conditionRequest.getSize();
    String sort = conditionRequest.getSort();
    String search = conditionRequest.getSearch();

    JPAQuery<LocationResponse> searchQuery = jpaQueryFactory
                .select(Projections.constructor(LocationResponse.class,
                        location.id,
                        location.latitude,
                        location.longitude,
                        location.title,
                        location.address,
                        location.description,
                        locationLike.count().as("like_count")))
                .from(location)
                .leftJoin(locationLike).on(locationLike.location.id.eq(location.id))
                .where(location.latitude.between(latitude - scale, latitude + scale)
                        .and(location.longitude.between(longitude - scale, longitude + scale)))
                .groupBy(location.id);

        JPAQuery<Long> countQuery = jpaQueryFactory
                .select(location.count())
                .from(location)
                .where(location.latitude.between(latitude - scale, latitude + scale)
                        .and(location.longitude.between(longitude - scale, longitude + scale)));

        if (page != null || size != null || sort != null || search != null) {

            if (page == null)
                page = defaultPage;
            if (size == null)
                size = defaultSize;

            pageable = PageRequest.of(page - 1, size);
            searchQuery
                    .limit(pageable.getPageSize())
                    .offset(pageable.getOffset());

        }

        if (sort == null)
            searchQuery.orderBy(location.id.desc());
        else if (sort.equals("like"))
            searchQuery.orderBy(likeCount.desc());
        else if(sort.equals("recent"))
            searchQuery.orderBy(location.id.desc());
        else
            searchQuery.orderBy(location.id.desc());

        if (search != null) {
            searchQuery
                    .where(location.title.contains(search)
                            .or(location.description.contains(search)));
            countQuery
                    .where(location.title.contains(search)
                            .or(location.description.contains(search)));
        }

        List<LocationResponse> locations = searchQuery.fetch();

        return pageable != null ?
                PageableExecutionUtils.getPage(locations, pageable, countQuery::fetchOne) :
                new PageImpl<>(locations);
}

전체 코드는 위와 같고 아래의 주요 로직을 보자.

 

conditionRequest를 다뤄 응답하는 방법에 대해 중점으로 설명할 예정이다.

 

Pageable을 우선 null로 선언한다.

Pageable pageable = null;

Pageable이 null이면 페이징 처리를 하지 않고 전체 데이터를 응답한다.

만약 null이 아니라면 페이징 처리한 데이터를 응답한다.

 

if (page != null || size != null || sort != null || search != null) {

    if (page == null)
        page = defaultPage;
    if (size == null)
        size = defaultSize;

    pageable = PageRequest.of(page - 1, size);
    searchQuery
        .limit(pageable.getPageSize())
        .offset(pageable.getOffset());

}

page, size, sort,  search가 null이 아니라면 쿼리 파라미터로 하나의 조건이라도 들어가 있는것이므로

한 페이지에 모든 데이터를 담으면 안되고 페이징 처리가 필요하다.

그래서 page, size가 null이라면 기본값으로 초기화 시켜준다. (null이 아니라면 요청한 조건이 있으니 요청 값 유지)

 

sort는 기본값은 최신순을 하기 위해 null이라면 recent로 초기화하도록 했다.

 

좋아요 정렬과 같은 없는 필드를 통해 정렬할 필요가 있기에 PageRequest.of로 페이지와 크기로만 생성한다.

searchQuery를 이어서 페이징을 위한 limit, offset을 추가해준다.

 

 

if (sort == null)
    searchQuery.orderBy(location.id.desc());
else if (sort.equals("like"))
    searchQuery.orderBy(likeCount.desc());
else if(sort.equals("recent"))
    searchQuery.orderBy(location.id.desc());
else
    searchQuery.orderBy(location.id.desc());

요청한 정렬을 맞춰 order By를 추가한다. sort에 이상한 값을 넣었다면 else문을 타도록 작성해봤다.

 

 

if (search != null) {
    searchQuery
        .where(location.title.contains(search)
                .or(location.description.contains(search)));
    countQuery
        .where(location.title.contains(search)
                .or(location.description.contains(search)));
}

검색 파라미터가 존재하면 제목과 내용에 포함되어 있는 LIKE '%search%' 조건을 건다.

 

List<LocationResponse> locations = searchQuery.fetch();

fetch를 통해 리스트로 반환 받는다.

 

이제 젤 고민했던 부분이다. 페이징 조건이 없는 요청이라면 한 페이지에 다 넣고 응답하고싶고

페이징 조건이 있다면 맞춰 응답하는데

둘 다 Page로 반환하고 싶었다. 그럼 어떻게 return 할까?

 

다음과 같다.

return pageable != null ?
    PageableExecutionUtils.getPage(locations, pageable, countQuery::fetchOne) :
    new PageImpl<>(locations);

pageable이 있다면 PageableExcutionUtils.getPage로 return하고

없다면

new PageImpl<>()에 content만 넣어 생성한다.

 

생성자로 Pageable.unpaged() 반환값을 넣는데

 

결국 Pageable을 반환해준다.

 

 

조건이 하나 없이 요청을 했을 때 limit 없이 쿼리를 날려 모든 데이터를 뽑아온다.

 

search 조건만 넣었을때

limit를 함께 날리는걸 볼 수 있다.

Comments