[TIL] 23.06.15 Querydsl을 활용한 복잡한 동적 쿼리( +네이버 쇼핑몰 검색 필터링 대해 ) + QueryDsl 의 정렬 기능(OrderSpecifier) (7)

hyewon jeong·2023년 6월 15일
0

TIL

목록 보기
133/138

김영한의 QueryDsl 강의를 다보고 난 후
네이버 쇼핑 처럼 다중 조건으로 검색 하는 방법에 대해 궁금해져서
네이버 쇼핑몰을 모델로 파헤쳐 보았다.

네이버 쇼핑몰

Where 조건에 해당하는 값들이 0 ~ N 개 까지 선택이 가능하다.

1. 필터의 조건을 걸때마다 get 방식으로 요청

네이버 쇼핑몰 같은 경우 필터 조건을 걸때마다 URL의 요청 파라미터가 바뀌는 것에서 알 수 있듯이 GET 방식으로 구현하고 있음을 알 수 있다.

https://search.shopping.naver.com/search/all?frm=NVSHATT&maker=194129%204772%203675%209883&origQuery=%ED%82%A4%EB%B3%B4%EB%93%9C&pagingIndex=1&pagingSize=40&productSet=total&query=%ED%82%A4%EB%B3%B4%EB%93%9C&sort=rel&spec=M10015647%7CM10785324&timestamp=&viewType=list

1-1. 공유기능으로 GET 과 POST 방식의 차이점

일반적으로 "공유하기" 기능은 URL을 통해 데이터를 전달하는 것을 의미한다.

1-1-1.GET 방식은

  1. URL에 파라미터를 포함하여 요청을 보내므로, 사용자가 해당 URL을 복사하고 공유하기만 하면 된다. 다른 사용자는 해당 URL을 방문하여 동일한 데이터를 얻을 수 있다.

  2. GET 방식의 장점은
    간편함과 공유의 용이성이다. URL을 공유함으로써 다른 사용자들이 동일한 데이터를 쉽게 얻을 수 있다. 또한, 캐싱이 가능하므로 동일한 요청에 대한 응답을 캐시하여 성능을 향상시킬 수 있다.

  3. GET 방식의 단점은
    URL에 데이터가 노출되는 것이다. URL은 브라우저 히스토리, 로그 파일, 북마크 등에 저장되므로 보안에 민감한 정보를 포함하면 안 된다ㅏ. 또한, URL에 전송할 수 있는 데이터의 크기에 제한이 있으며, 보안을 위해 암호화해야 할 수도 있다.

1-1-2. POST 방식은

  1. 데이터를 요청의 본문에 포함하여 전송합니다.

  2. POST 방식의 장점은
    더 많은 데이터를 전송할 수 있고, 데이터가 URL에 노출되지 않으므로 보안에 더 우수합니다. POST 방식은 민감한 정보를 전송해야 할 때, 데이터의 크기가 큰 경우, 데이터를 서버로 보내고 처리하는 것이 필요한 경우에 적합합니다.

  3. POST 방식의 단점은

  • 캐싱 불가능함으로 동일 요청시 get방식에 비해 성능저하

  • 즐겨찾기와 공유의 어려움: POST 요청은 URL에 데이터가 포함되지 않기 때문에 사용자가 특정 상태나 데이터를 즐겨찾기에 추가하거나 다른 사람과 공유하기가 어렵다. GET 요청은 URL에 요청 파라미터가 포함되어 쉽게 공유할 수 있는 반면, POST 요청은 별도의 링크를 생성하거나 데이터를 직접 전달해야 한다.

  • 보안 위험: POST 요청은 데이터가 요청 본문에 포함되기 때문에 보안적인 측면에서 GET 요청보다는 안전하지만, 그럼에도 불구하고 데이터를 암호화하거나 추가적인 보안 조치를 취해야 할 수도 있습니다.

결론적으로, "공유하기" 기능은 주로 GET 방식을 사용한다. 이런 이유로 네이버 쇼핑몰도 GET방식을 사용하지 않을까 싶다.

  • GET 방식은 간단하고 빠르게 데이터를 공유할 수 있으며, 캐싱을 활용하여 성능을 향상시킬 수 있다.
  • 그러나 보안이 중요하거나 큰 데이터를 전송해야 하는 경우에는 POST 방식을 고려해야 합니다. 상황에 맞게 적합한 방식을 선택하여 구현하시면 된다.

2-1. 왜 스페이스를 '%20'으로 표현할까 🙄 ???


위의 스냅샷을 보면 네이버 쇼핑 사이트의 필터값 선택에 따른 url을 보면 브랜드 : Apple,아이폰, LG전자 와 같이 중복선택이 가능한 경우 space(%20) 띄어쓰기를 통해 요청 파라미터를 전달받고 이를 split을 통해 DTO로 전달받고 있음을 추측할 수 있다.

space(%20)은 URL에서 공백을 나타내는 특수 문자입니다.

URL 은 일반적으로 공백 등 특정 문자들을 사용할 수 없기 때문에 URL 인코딩을 통해 특수문자가 필요한 부분에서 안전하게 전송하여 사용한다.

2-1-1. URL 인코딩은

  1. 특정한 문자를 URL에서 안전하게 전송하기 위해 사용되는 메커니즘이다.
    URL에 사용되는 일부 문자는 특별한 의미를 가지거나, URL의 구문을 해석하는 데 사용되는 문자로 예약되어 있다. 이러한 문자들은 URL에서 직접 사용할 수 없으며, 대신 인코딩되어 전송되어야 한다.

  2. URL 인코딩에서는 인코딩된 문자를 표현하기 위해 16진수(HEX) 값으로 표현합니다. 각 문자는 ASCII 코드 값으로 표현되며, 해당 값을 16진수로 변환하여 사용합니다. 이를 통해 인코딩된 문자를 URL에서 안전하게 전송할 수 있습니다.

예 ) 공백 문자의 ASCII 코드 값은 32이며, 이를 16진수로 표현하면 20입니다. 따라서 공백을 나타내는 특수 문자로 %20을 사용하는 것입니다. 이는 URL에서 공백을 안전하게 표현하기 위한 표준 방식입니다.


그러므로
URL은 일반적으로 공백을 허용하지 않는 대신에 공백을 나타내는 문자로 %20을 사용합니다. %20은 URL 인코딩(Percent-Encoding)에 따라 공백을 나타내는 특수 문자로 인식됩니다.** 20은 공백 문자의 ASCII 코드 값인 32를 16진수로 표현한 값입니다.


https://namu.wiki/w/%EC%95%84%EC%8A%A4%ED%82%A4%20%EC%BD%94%EB%93%9C

따라서

예시 URL에서 brand 파라미터 값인 16505%20238622%20108602는 실제로는 16505 238622 108602로 해석되며, 각각의 숫자는 공백으로 구분된 여러 제조사의 ID를 나타내는 것으로 추측할 수 있다.

2-1-2. Spring 에서 URL 인코딩의 예

  • Spring에서 URL 인코딩 설정은 주로 UriComponentsBuilder를 사용하여 수행됩니다.

  • UriComponentsBuilder는 URL을 구성하는 데 사용되며, 인코딩, 쿼리 파라미터 추가, 경로 변수 설정 등을 지원합니다.

아래는 Spring에서 URL 인코딩을 설정하는 방법의 예시이다.

import org.springframework.web.util.UriComponentsBuilder;

public class URLExample {
    public static void main(String[] args) {
        String baseUrl = "https://example.com/search";
        String queryParam = "한글";

        // URL 인코딩 적용하여 URL 생성
        String encodedUrl = UriComponentsBuilder.fromHttpUrl(baseUrl)
                .queryParam("q", queryParam)
                .encode()
                .toUriString();

        System.out.println(encodedUrl);
    }
}

위의 예시에서는 UriComponentsBuilder를 사용하여 URL을 구성하고, encode() 메서드를 호출하여 URL 인코딩을 적용한다. 이후 toUriString()을 호출하여 최종적으로 인코딩된 URL을 얻을 수 있다.

❄️실행 결과는 다음

https://example.com/search?q=%ED%95%9C%EA%B8%80
# 한국어가 encoding 되어 %ED와 같이 변환되었다.

위의 예시에서는 쿼리 파라미터인 "q"에 값으로 "한글"을 전달하고, 해당 값이 URL 인코딩되어 "%ED%95%9C%EA%B8%80"으로 표현되었다.

UriComponentsBuilder를 사용하면 URL 인코딩 외에도 다양한 URL 구성 작업을 수행할 수 있으며, REST API에서 URL을 동적으로 구성해야 할 때 유용하다.

3. Querydsl의 정렬(OrderSpecifier)

스프링 데이터 JPA는 자신의 정렬(Sort)을 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는 기능을 제공한다.

정렬( Sort )은 조건이 조금만 복잡해져도 Pageable 의 Sort 기능을 사용하기 어렵다. 루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 Sort 를 사용하기 보다는 파 라미터를 받아서 직접 처리하는 것을 권장한다.

3-1. OrderSpecifier는

  • Querydsl 라이브러리에서 제공하는 클래스로, 정렬을 표현하기 위해 사용됩니다. 이 클래스는 정렬 방향(Order)과 정렬할 속성 또는 경로(Expression)의 조합으로 생성됩니다.

  • 일반적으로 orderBy() 절에서 사용된다.
    예를 들어, new OrderSpecifier(Order.ASC, member.name)는 member 엔티티의 name 속성을 오름차순으로 정렬하는 조건을 나타냅니다.

스프링 데이터의 정렬을 Querydsl의 정렬로 직접 전환하는 방법은 다음 코드를 참고하자.

3-2. 스프링 데이터 Sort를 Querydsl의 OrderSpecifier로 변환

    JPAQuery<Member> query = queryFactory
            .selectFrom(member);
    for (Sort.Order o : pageable.getSort()) {
        PathBuilder pathBuilder = new PathBuilder(member.getType(),
    member.getMetadata());
        query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC,
                pathBuilder.get(o.getProperty())));
    List<Member> result = query.fetch();

주어진 코드는 Spring Data의 Pageable을 사용하여 JPAQueryFactory를 통해 동적 정렬을 수행하는 부분이다.

  • JPAQuery query = queryFactory.selectFrom(member);: queryFactory를 사용하여 member 엔티티를 대상으로 JPAQuery를 생성한다.

  • for (Sort.Order o : pageable.getSort()) { ... }: pageable 객체에서 정렬 정보를 가져온다. pageable.getSort()를 통해 Sort.Order 객체들의 리스트를 얻는다.

  • PathBuilder pathBuilder = new PathBuilder(member.getType(), member.getMetadata());: PathBuilder를 사용하여 정렬할 경로(path)를 생성한다. member.getType()은 엔티티의 타입 정보를, member.getMetadata()는 엔티티의 메타데이터 정보를 가져온다.

  • query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC, pathBuilder.get(o.getProperty())));: 가져온 정렬 정보를 바탕으로 query에 정렬 조건을 추가한다. OrderSpecifier를 생성하여 정렬 방향과 경로를 지정한다. o.isAscending()이 true이면 오름차순(ASC)으로 정렬하고, false이면 내림차순(DESC)으로 정렬한다.

  • pathBuilder.get(o.getProperty())를 통해 정렬할 속성의 경로를 가져온다.

"정렬할 경로"는 정렬의 기준이 되는 속성 또는 엔티티의 경로를 의미한다. 예를 들어, pathBuilder.get("propertyName")은 propertyName에 해당하는 속성의 경로를 가져오는 메서드이다.

정렬할 경로를 지정하여 해당 속성을 기준으로 정렬을 수행할 수 있다. 예를 들어, query.orderBy(new OrderSpecifier(Order.ASC, pathBuilder.get("name")));와 같이 name 속성을 기준으로 오름차순으로 정렬할 수 있다. 정렬할 경로를 명시함으로써 쿼리가 해당 경로를 기준으로 결과를 정렬하게 된다.

  • List result = query.fetch();: 최종적으로 정렬이 적용된 query를 실행하고 결과를 가져온다. 이 코드에서는 Member 엔티티의 리스트를 반환한다.

위의 과정을 요약하면, pageable에서 가져온 정렬 정보를 기반으로 query에 동적으로 정렬 조건을 추가하고, 최종적으로 정렬이 적용된 쿼리를 실행하여 결과를 가져오는 것이다.

3-3. JPA QueryDsl 정렬 사용하기 참고 자료

3-3-1.

[ Spring-Boot ] JPA QueryDsl 정렬 사용하기

3-3-2.

[JPA] QueryDsl에서 Custom Sorting 하는 법

import com.querydsl.core.types.Order; 
import com.querydsl.core.types.OrderSpecifier; 
import com.querydsl.core.types.Path; 
import com.querydsl.core.types.dsl.Expressions; 

public class QueryDslUtil { 
	public static OrderSpecifier<?> getSortedColumn(Order order, Path<?> parent, String fieldName) { 
    	Path<Object> fieldPath = Expressions.path(Object.class, parent, fieldName); 
  //  메서드 내부에서는 Expressions.path() 메서드를 사용하여 Path<Object> 객체를 생성합니다. 이 객체는 부모 경로와 필드 이름을 조합하여 특정 필드를 가리키는 경로를 나타냅니다.
        return new OrderSpecifier(order, fieldPath); 
    }
}
public Page<RoomStatistic> search(CsRoomStatisticFilter filter, Pageable pageable) {
    List<OrderSpecifier> ORDERS = getAllOrderSpecifiers(pageable); 
    JPAQueryFactory queryFactory = new JPAQueryFactory(em); 
    QueryResults<CsRoomStatistic> results = queryFactory 
        .select(Projections.constructor(
            RoomStatistic.class, 
            room.id, 
            room.createdDate, 
            room.updatedDate, 
            user.name, 
            user.jobInfo.name, 
            room.category, 
            type.name 
        )) 
        .from(room) 
        .join(user).on(room.createdUserId.eq(user.id)).fetchJoin() 
        .join(type).on(room.typeId.eq(type.id)).fetchJoin() 
        .where( room.createdDate.between(filter.getSearchStartDate(), filter.getSearchEndDate()) ) 
        .orderBy(ORDERS.stream().toArray(OrderSpecifier[]::new)) 
        .offset(pageable.getOffset()) 
        .limit(pageable.getPageSize()) 
        .fetchResults(); 
    return new PageImpl<>(results.getResults(), pageable, results.getTotal());
}

에서

 .orderBy(ORDERS.stream().toArray(OrderSpecifier[]::new))
  • 코드에서 배열로 변환하는 이유는 orderBy() 메서드의 인자로 배열을 전달하기 위해서입니다.

  • Querydsl의 orderBy() 메서드는 OrderSpecifier를 가변 인자로 받는 형태로 정의되어 있습니다. OrderSpecifier 객체들을 배열 형태로 전달하여 한 번에 여러 개의 정렬 조건을 지정할 수 있습니다.

  • ORDERS.stream().toArray(OrderSpecifier[]::new)는 ORDERS 리스트를 스트림으로 변환한 뒤 toArray() 메서드를 호출하여 배열로 변환하는 과정입니다. 이렇게 배열로 변환된 정렬 조건들을 orderBy() 메서드에 전달하여 쿼리 결과를 해당 조건에 맞게 정렬할 수 있습니다.

profile
개발자꿈나무

0개의 댓글