중간 회고 & 카테고리별 상품 조회

jihan kong·2023년 8월 9일
1
post-thumbnail

1. 중간 회고

올해 여름은 불반도가 분명하다. 🥵 정말 무더운 나날들이 계속되고 있고
푹푹찌는 더위에 집보다는 도서관에서 살고 있다.
언제쯤 시원한 가을이 올런지...

여튼 저번 포스팅 이후, 여러가지로 바빴지만 틈나는대로 쇼핑몰을 계속 뚝딱거리면서 여러가지를 개선했다.
가장 크게는 쇼핑몰의 외관을 완전히 바꾸었다. 전체적으로 투박하고 개성이 없어 보이는 모습이었는데 메인화면의 글자 애니메이션과 전체적인 폰트, 버튼스타일, 드롭다운 메뉴, 입고된 신상품을 보여주는 화면 등을 추가 혹은 변경하면서 조금 더 세련되게 쇼핑몰을 구성했다. 이로 인해 조금 더 쇼핑몰 다운 쇼핑몰이 된 느낌!

(본의 아니게 CSS 전문가가 된 것 같은...)

그리고 프론트 보다는 백 쪽에서 어떤 기능들을 더 추가할 수 있을지 많이 고민해보았다. 지금으로써 가장 개선이 필요한 부분은 결제 & 주문 기능이다. 현재는 장바구니에 담고 주문하기 버튼을 누르면 별다른 과정 없이 바로 주문이 가능하지만 결제 API를 활용하여 결제금액을 충전하여 주문하는 식의 결제 기능 혹은 결제 후 관리자 측에서 배송정보와 배송상황을 볼 수 있게끔 하는 등의 주문 관리 시스템을 구현하고 싶었다.

그렇게 야심차게 결제 기능을 구현해보았으나.. 여러가지 난관에 봉착하며 결제와 주문관리 기능은 생각보다 시간이 많이 소요된다는 것을 알게 되었다. 😥좌절하였지만 일단 구현은 다음으로 미뤄두고 쉬운 것부터 차근차근해보자는 생각으로 다음 기능을 생각해보았다.


2. 카테고리별 상품 조회

메인 메뉴 구성을 보았을 때, 메뉴에 만들어 놓은 카테고리에 따라 상품을 조회하는 기능이 있으면 좋겠다는 생각을 했다.


위와 같이 상품(item)은 "의류", "강아지 용품", "장난감", "사료(껌)", "기타" 로 카테고리화 되어있다. 사실 하위 카테고리도 생각해보았는데 이는 계층 구조 등으로 상당히 복잡한 로직을 가지고 있어 현재는 다섯 가지의 카테고리로 구분하기로 했다.


3. 구현 방식

구현하는 방식으로는 메인메뉴 아이템 카테고리마다 Controller 단에서 파라미터 매핑을 하고 이를 통해 카테고리별로 조회하는 그림이 가장 이상적이지만 조금 더 쉽게 조회를 하게끔 하고 싶었다.

먼저 구현해놓은 쇼핑몰 기능으로 검색 페이징 기능이 있다. 현재는 메인화면에서 상품 이름으로만 검색이 가능하다. 이를 수정하여 상품 카테고리로도 검색을 할 수 있게끔 하고, 검색된 페이지를 메뉴에서 href 태그를 통해 이동시킬 수 있다면 메뉴별로 조회되는 것이 아닐까라는 생각을 했다.

4. 구현 과정

4-1. ItemRepositoryCustom

상품을 조건에 따라 조회하는 것은 JPA Query를 통해 수행할 수 있고, 이를 위해서는 쿼리 로직이 담겨있는 ItemRepository 를 만져야 한다.

다만, 전에 조금 더 많은 기능을 구현하기 위해 ItemRepository 를 커스텀화해서 ItemRepositoryCustom 사용자 정의 인터페이스를 생성했었다.

ItemRepositoryCustom.java

package com.shop.repository;

import com.shop.dto.ItemSearchDto;
import com.shop.dto.MainItemDto;
import com.shop.entity.Item;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface ItemRepositoryCustom {

    Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable);            
    
    // 상품 조회 조건을 담고 있는 itemSearchDto 객체와
    // 페이징 정보를 담고 있는 pageable 객체를 파라미터로 받는 getMainItemPage 메소드를 정의
    Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable);
}

위 코드의 getMainItemPage 메서드를 통해 알맞은 조건으로 페이징된 정보를 메인화면에 뿌려줄 수 있다는 것을 알게 되었다.


4-2. ItemRepositoryCustomImpl

커스텀화된 인터페이스가 ItemRepositoryCustom 라면 실제로 이를 구현하는 ItemRepositoryCustomImpl 클래스가 있다. 여기서 QueryDsl을 통해 복잡하고 동적인 쿼리들을 쉽게 다룰 수 있었다.


ItemRepositoryCustomImpl.java

package com.shop.repository;

// .. import 생략

public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{

    private JPAQueryFactory queryFactory;

    public ItemRepositoryCustomImpl(EntityManager em){
        this.queryFactory = new JPAQueryFactory(em);
    }
	
    // ...others class skip

	@Override
    public Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
        QItem item = QItem.item;
        QItemImg itemImg = QItemImg.itemImg;

        List<MainItemDto> content = queryFactory
                .select(
                        new QMainItemDto(
                                item.id,
                                item.itemNm,
                                item.itemCategory,
                                item.itemDetail,
                                itemImg.imgUrl,
                                item.price)
                )
                .from(itemImg)
                .join(itemImg.item, item)
                .where(itemImg.repimgYn.eq("Y"))
                .where(itemNmLike(itemSearchDto.getSearchQuery()))
                .orderBy(item.id.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        long total = queryFactory
                .select(Wildcard.count)
                .from(itemImg)
                .join(itemImg.item, item)
                .where(itemImg.repimgYn.eq("Y"))
                .where(itemNmLike(itemSearchDto.getSearchQuery()))
                .fetchOne()
                ;

        return new PageImpl<>(content, pageable, total);
    }

다른 클래스 살펴볼 것도 없이 getMainItemPage 메소드를 바로 들여다보자. queryFactory를 이용해서 쿼리를 생성하고, SQL처럼 select, from, join, where 등의 구문을 통해 쿼리 조건을 만들고 있다.

나는 조회를 위한 where 조건절이 수정되어야함을 느꼈다. where절에는 현재 ItemNmLike 즉, 상품 이름으로 검색할 수 있는 쿼리만 존재하기 때문에 ItemCategoryLike 와 같이 카테고리로 검색할 수 있는 BooleanExpression 을 반환하는 조건문을 만들고 이를 추가해주면 어떨까란 생각을 했다.

4-3. 조건문 생성 & 추가

 private BooleanExpression itemNmLike(String searchQuery){
        return StringUtils.isEmpty(searchQuery) ? null : QItem.item.itemNm.like("%" + searchQuery + "%");
    }

private BooleanExpression itemCategoryLike(String searchQuery){
        return StringUtils.isEmpty(searchQuery) ? null : QItem.item.itemCategory.like("%" + searchQuery + "%");
    }

위와 같이 itemNmLike 조건문처럼 itemCategoryLike 도 생성해주고, QueryDsl을 사용하게끔 QItemitemCategory 를 참조하게 하면 searchQuery를 통해 사용자가 검색한 내용을 받아 조건처리할 수 있을 것이다.

4-4. ItemRepositoryCustomImpl 변경

이제 where절을 수정할 차례이다. 다음과 같이 수정해보았다.

ItemRepositoryCustomImpl.java

List<MainItemDto> content = queryFactory
                .select(
                        new QMainItemDto(
                                item.id,
                                item.itemNm,
                                item.itemCategory,
                                item.itemDetail,
                                itemImg.imgUrl,
                                item.price)
                )
                .from(itemImg)
                .join(itemImg.item, item)
                .where(itemImg.repimgYn.eq("Y"))
                .where(itemNmLike(itemSearchDto.getSearchQuery())
					//추가된 부분
					.or(itemCategoryLike(itemSearchDto.getSearchQuery())))
                .orderBy(item.id.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        long total = queryFactory
                .select(Wildcard.count)
                .from(itemImg)
                .join(itemImg.item, item)
                .where(itemImg.repimgYn.eq("Y"))
                .where(itemNmLike(itemSearchDto.getSearchQuery())
                	//추가된 부분
                    .or(itemCategoryLike(itemSearchDto.getSearchQuery())))
                .fetchOne()
                ;
  • where 절에서는 , 로 연결하면 and 조건이 되어버린다.
  • 현재 나는 상품 이름으로 검색 또는 상품 카테고리로 검색. 즉, or 조건이 필요하므로 or 로 연결했다.

모든 준비가 끝나고 웹 애플리케이션을 작동해보았다.


5. 오류발생 ⚠️ & 해결

역시 한번에 작동된다면 오히려 서운할 뻔 했다. 다음과 같은 오류가 발생했다.

콘솔에서 NPE를 뿜어내고 있었다. 추가된 곳은 where절 밖에 없었기 때문에 확실히 where절로 인해 NULL이 발생했을 텐데... 갈피를 잡지 못했고 이를 해결하기 위해 구글에 'QueryDsl where절 null해결' 과 같은 키워드 검색을 시작했다.
(이제부터 시작한 열쇠찾기...😎)

그리고 정말 다행히! 빠른 시일내에 열쇠를 찾을 수 있었다.
https://www.inflearn.com/questions/38317/querydsl-where-%EC%A0%88-%EC%A1%B0%EA%B1%B4-%EA%B4%80%EB%A0%A8-%EC%98%A4%EB%A5%98-%EB%AC%B8%EC%9D%98%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4

인프런 사이트의 어떤 분이 문의한 내용과 김영한 쌤의 답변을 통해 문제를 해결할 수 있었다. 이 분도 or로 조건 검색을 하다가 null point exception 이 발생했고, 이에 대한 해결책으로 김영한 쌤은 다음과 같이 BooleanBuilder를 사용하라고 한다.


이제 알았으니 내 프로젝트에 바로 적용시켜보자.

ItemRepositoryCustomImpl.java

    private BooleanBuilder itemNmLike(String searchQuery){
        BooleanBuilder builder = new BooleanBuilder();

        builder.or(QItem.item.itemNm.like("%" + searchQuery + "%"));
        return builder;
    }

    private BooleanBuilder itemCategoryLike(String searchQuery){
        BooleanBuilder builder = new BooleanBuilder();

        builder.or(QItem.item.itemCategory.like("%" + searchQuery + "%"));
        return builder;
    }

6. 동작 화면 🕹️

BooleanBuilder로 바꿔주니 NPE가 해결되면서 잘 동작할 수 있었다.
(역시 갓영한쌤👍)

다음은 동작화면이다.

[의류] 카테고리를 가진 '강아지 하네스' 상품을 등록해놓았고, [의류] 라는 카테고리를 검색창에서 검색했을 때, 강아지 하네스 상품이 결과로 나와야 제대로 동작하는 것이 되겠다.

검색해보자.

위와 같이 상품 카테고리로 검색했을 때, 상품이 잘 검색되었다.

물론 상품 이름으로 검색해도 잘 뜬다.
이제 이 결과값을 메뉴 항목마다 연결해주면 끝. (또 다시 프론트작업...)

profile
학습하며 도전하는 것을 즐기는 개발자

2개의 댓글

comment-user-thumbnail
2023년 8월 9일

좋은 글 감사합니다. 자주 방문할게요 :)

답글 달기
comment-user-thumbnail
2023년 10월 13일

잘 읽어보았읍니다.

답글 달기