230330 TIL #46 JPA 검색 기능

김춘복·2023년 3월 30일
0

TIL : Today I Learned

목록 보기
46/543
post-custom-banner

230330 Today I Learned

클론코딩 마지막날. 오늘은 프론트 페이지에서는 구현을 못했지만 백엔드 서버에서 검색기능을 구현했다.


JPA 검색기능

  • Spring으로 상품 검색기능을 넣어봤다.

  • keyword에 대해 검색하면 product 테이블의 title과 desc에서 keyword를 검색해 결과를 Page<ProductListResponseDto>로 반환

  • 프론트에서는 page에 현재 페이지, size에 한 페이지에 보여줄 상품 수, sortBy에 게시글 정렬 기준, isAsc에 오름차순이면 true / 내림차순이면 false, cateCode에 카테고리 코드를 보내줘야 한다.

  • 카테고리와 상관없이 검색하려면 cateCode에 0을 보내주고, 특정 카테고리 내의 상품만 검색하려면 그에 맞는 cateCode를 보내주면 된다.

  • 최신순은 sortBy에 id(혹은 createdAt), isAsc에 false를 담아 보내주면 된다.
    저가순은 sordBy에 price, isAsc에 true를 담아 보내주면 된다.
    고가순은 sortBy에 price, isAsc에 false를 담아 보내주면 된다.
    정확도순 검색은 일단 구현 중지.

  • Controller
@GetMapping
    public Page<ProductListResponseDto> searchProducts(@RequestParam(value = "keyword") String keyword,
                                                       @RequestParam(value = "page") int page,
                                                       @RequestParam(value = "size") int size,
                                                       @RequestParam(value = "sortBy") String sortBy,
                                                       @RequestParam(value = "isAsc") boolean isAsc,
                                                       @RequestParam(value = "cateCode") int cateCode) {
        if (keyword.isBlank()) throw new IllegalArgumentException("검색어를 입력해 주세요.");
        return searchService.searchProducts(keyword, page-1, size, sortBy, isAsc, cateCode);
    }

쿼리스트링으로 변수를 다 받는다. 번거롭다면 나중에 DTO로 바꿔서 받으면 된다.
페이징 기능 때문에 service에 넘길 때 page는 -1을 해준다.

  • service
    @Transactional(readOnly = true)
    public Page<ProductListResponseDto> searchProducts(String keyword, int page, int size, String sortBy, boolean isAsc, int cateCode) {
        Sort.Direction direction = isAsc? Sort.Direction.ASC : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);
        return productRepository.findByTitleOrDescContainingAndCateCode(keyword, cateCode, pageable)
                .map(ProductListResponseDto::new);
    }

pageable에 필요한 걸 넣어주고 stream으로 dto를 변환해서 반환한다.

  • repository
    @Query("SELECT p FROM Product p WHERE (p.title LIKE %:keyword% OR p.desc LIKE %:keyword%) AND (:cateCode = 0 OR p.cateCode = :cateCode)")
    Page<Product> findByTitleOrDescContainingAndCateCode(@Param("keyword") String keyword, @Param("cateCode") int cateCode, Pageable pageable);

jpa쿼리는 길어지거나 중간에 다른 변수를 넣으면 에러가 자주 떠서 직접 SQL쿼리를 만들어서 하는 것이 오히려 편했다.
keyword를 title과 desc(설명)에 검색해서 cateCode가 0이면 전체검색, cateCode에 맞는 int 값이 들어오면 거기에 맞는 카테고리만 검색하는 식으로 쿼리를 만들었다.

구현 결과

  • 위와같이 구현해서
    http://URI/api/products/search?keyword=실사용&page=1&size=4&sortBy=price&isAsc=false&cateCode=0
    처럼 쿼리스트링으로 request를 보내면
{
    "content": [
        {
            "id": 67,
            "img": "7fb9ca40-ad30-4ef7-a6b2-792df61d2199_금매입.24k.순금.18k.14k.돌반지.다이아몬드.18k반지 천만원.jpg",
            "title": "금매입 24k 순금 18k 14k 돌반지",
            "price": 10000000,
            "thunderPay": true,
            "timeInterval": "1달 전",
            "done": false
        },
        {
            "id": 110,
            "img": "e0300b43-7753-4104-9569-123b0cd2542e_써벨로 P5 판매 데스크탑킥보드 교신 700만.jpg",
            "title": "써벨로 P5 판매 데스크탑 킥보드",
            "price": 7000000,
            "thunderPay": true,
            "timeInterval": "1주 전",
            "done": false
        },
        {
            "id": 68,
            "img": "78bf92e7-bef0-4cbf-87ee-7072b3d56345_까르띠에앵끌루sm반지 350만.jpg",
            "title": "까르띠에앰끌루 sm반지",
            "price": 3500000,
            "thunderPay": true,
            "timeInterval": "1달 전",
            "done": false
        },
        {
            "id": 53,
            "img": "ca86aff2-8fa2-4dcd-b087-727d270749fd_루이비통 쁘띠뜨 팔레 m45900 토트백 겸 숄더백 겸 크로스 새상품급 290만.jpg",
            "title": "루이비통 쁘띠뜨 팔레 m45900 새상품급",
            "price": 2900000,
            "thunderPay": false,
            "timeInterval": "2달 전",
            "done": false
        }
    ],
    "pageable": {
        "sort": {
            "empty": false,
            "sorted": true,
            "unsorted": false
        },
        "offset": 0,
        "pageNumber": 0,
        "pageSize": 4,
        "paged": true,
        "unpaged": false
    },
    "totalPages": 30,
    "totalElements": 119,
    "last": false,
    "size": 4,
    "number": 0,
    "sort": {
        "empty": false,
        "sorted": true,
        "unsorted": false
    },
    "numberOfElements": 4,
    "first": true,
    "empty": false
}

Response가 이렇게 페이지네이션식으로 나온다.
페이지에 관한 정보는 TIL #34 참고

정확도순?

@Query("SELECT p FROM Product p WHERE (p.title LIKE %:keyword% OR p.desc LIKE %:keyword%) AND (:cateCode = 0 OR p.cateCode = :cateCode) ORDER BY CASE WHEN p.title = :keyword OR p.desc = :keyword THEN 0 WHEN p.title LIKE :keyword% OR p.desc LIKE :keyword% THEN 1 WHEN p.title LIKE %:keyword% OR p.desc LIKE %:keyword% THEN 2 WHEN p.title LIKE %:keyword OR p.desc LIKE %:keyword THEN 3 END")
Page<Product> findByTitleOrDescContainingAndCateCodeOrderByAccuracy(@Param("keyword") String keyword, @Param("cateCode") int cateCode, Pageable pageable);

SELECT p FROM Product p WHERE (p.title LIKE %:keyword% OR p.desc LIKE %:keyword%) AND (:cateCode = 0 OR p.cateCode = :cateCode)
ORDER BY CASE WHEN p.title = :keyword OR p.desc = :keyword THEN 0
WHEN p.title LIKE :keyword% OR p.desc LIKE :keyword% THEN 1
WHEN p.title LIKE %:keyword% OR p.desc LIKE %:keyword% THEN 2
WHEN p.title LIKE %:keyword OR p.desc LIKE %:keyword THEN 3 END

정확도순은 구글링해보니 위와 같이 keyword와 완전히 같은 case, 비슷한 케이스 등으로 나눠서 정렬한다고 하던데 막상 해보니 SQL 쿼리가 너무 길어서 그런지 에러가 발생했다.
시간관계상 정확도순 검색은 구현하지 못했는데 다음 기회에 한번 더 찾아봐야 겠다.

profile
Backend Dev / Data Engineer
post-custom-banner

0개의 댓글