Kim VamPa

[Spring][쇼핑몰 프로젝트][34] 검색 필터링 기능 - 1 본문

스프링 프레임워크/쇼핑몰 프로젝트

[Spring][쇼핑몰 프로젝트][34] 검색 필터링 기능 - 1

Kim VamPa 2021. 9. 30. 14:42
728x90
반응형
프로젝트 Github : https://github.com/sjinjin7/Blog_Project
프로젝트 포스팅 색인(index) : https://kimvampa.tistory.com/188

 

목표

검색 필터링 기능 구현

 

 현시점(2021-09-28)의 교보문고 키워드를 통해 검색을 하여 '상품 목록' 페이지로 이동을 하게 되면, 상품 목록 페이지 상단에 검색된 결과물들을 다시 필터링할 수 있는 인터페이스를 볼 수 있습니다. 인터페이스에는 검색 키워드를 포함하는 상품이 속한 '카테고리'가 출력되고 그 카테고리에 속하면서 검색 키워드를 가진 '상품의 개수'가 표시됩니다. 카테고리 이름을 클릭하면 키워드 검색 결과물 중에서 클릭한 카테고리로 필터링된 상품 목록 페이지로 이동을 합니다. 이번 포스팅에선 검색 결과물을 필터링해주는 기능을 구현하는 것이 목표입니다.

 

 

 

 

 

순서

 

1. 구현 방향

2. CateFilterVO.class 생성

3. Mapper 메서드

4. BookMapperTests.java

 

1. 구현 방향

전체적 방향

 

 먼저 어떠한 필터링 기능은 구현할지 나열해 보겠습니다.

 


- 이 기능은 오로지 사용자가 '책 제목 검색', '작가 이름 검색'을 하였을 때만 사용자가 이용 할 수 있음. (카테고리 네비를 통해 상품 목록 페이지 이동시 보이지 않음)

- 사용자가 검색한 키워드를 가진 상품이 속한 카테고리 이름들이 출력됨.

- 카테고리 이름 옆에 그 카테고리에 속한 상품 수가 출력됨.

- 필터를 눌렀을때 기존 필터링 데이터들은 그대로 존재. ( 카테고리 이름을 눌러서 기존 필터링된 페이지를 이동하더라도 기존에 떴던 필터링 할 수 있는 카테고리 이름 목록들은 없어지지 않습니다. 사용자가 새로운 키워드로 검색했을 때 변하게 됩니다.)

- 카테고리 이름을 클릭을 하면 "/search" url을 타고 type은 "TC" 혹은 "AC", cateCode는 해당 '카테고리 이름', keyword는 '사용자가 작성한 키워드'가 파라미터로 서버로 전송되어 필터링된 상품 목록 페이지로 이동됨.

 

 검색 필터링 구현에서 크게 두 가지가 측면에서 작업이 이루어져야 합니다. 하나는 사용자가 어떠한 필터링을 할수 있는지에 대한 정보를 뷰에서 만들어서 뷰에 전달하여 사용자가 보기 편하도록 인터페이스를 추가해주는 일이고, 다른 하나는 사용자가 그 인터페이스를 눌렀을 때 필터링된 검색을 수행할 수 있는 서버 측 메서드를 추가해주는 일입니다.

 

 다행인 점은 검색 필터링을 수행하는 서버측 메서드를 새로 만들 필요는 없다는 점입니다. 우리가 작성한 "/search"가 검색뿐만 아니라 검색 필터링 또한 수행 가능하도록 작성했기 때문입니다. 이전 포스팅에서 "/search" url 매핑 메서드를 작업한 포스팅에서 Mapper 메서드를 작성한 후 Junit 테스트에서  '책 제목', '작가 검색', '카테고리 검색'과 더불어서 '책 제목 + 카테고리 검색', '작가 + 카테고리 검색'을 진행했습니다. 이는 이번 포스팅에서 진행할 필터링 기능을 위한 거였습니다.

 

 따라서 우리가 진행할 주작업은 사용자가 자신이 검색한 상품 목록이 속한 카테고리 리스트를 볼 수 있도록 서버에서 데이터를 만들어내고 뷰에 전달하고, 뷰에선 사용자가 이 데이터를 필터링 페이지로 이동할 수 있도록 인터페이스를 추가하는 것입니다.

 

 작업의 결과물이 사용자가 필터링을 사용 할 수 있도록 '상품 목록 페이지(search.jsp)'에 인터페이스를 추가해야 하는 것인데,  그럼 이 인터페이스 에는 어떠한 데이터가 있어야 하는지 고민해보겠습니다. 먼저 '카테고리 이름(cateName)'과 '카테고리 코드 번호(cateCode)'일 것입니다. '카테고리 이름'은 사용자가 보이는 부분을 위해 서이고, '카테고리 코드 번호'는 "/search" url의 cateCode 파라미터 값으로 보내기 위해서입니다. 

 

 그 다음 필요로 한 정보는 사용자가 검색한 키워드를 상품이 속한 카테고리에 몇 개의 상품 있는지에 대한 '개수'입니다. (이하 이 데이터를 '개수'라고 말하겠습니다.) 사용자가 직관적으로 이 카테고리에는 자신이 검색한 상품이 몇 개 인지를 알 수 있도록 하기 위함입니다.

 

 정리를 하면 '카테고리 이름', '카테고리 코드', '갯수'에 대한 데이터가 필요로 합니다. 서버가 어떠한 데이터를 만들어 주어야 하는지를 알았습니다. 이 데이터는 사용자가 상품을 검색하여 '상품 목록 페이지(search.jsp)'로 이동했을 때 떠야 하기 때문에, 우리는 우리가 원하는 형태의 필터링 데이터를 만들어내는 Service 메서드를 새로 작성을하고 기존 "/search" url 매핑 메서드가  새로 작성한 Service 메서드를 호출하여 반환받은 결과를 뷰로 전송하도록  수정해 줄 것입니다.

 

 

어떠한 Service 메서드?

 

 Service메서드를 작성하기전 먼저 우리는 어떠한 형태의 데이터를 반환하도록 만들지를 분명히 할 필요가 있는 거 같습니다.

 

 우리가 필요로한건 '카테고리 이름', '카테고리 코드번호', '개수'입니다. 이 데이터들을 주고받기 위해선 이 데이터들을 담을 객체가 필요로 합니다. 비슷한 객체로서 CateVO가 있긴 하지만 해당 객체엔 '개수'를 담을 수 있는 변수가 선언되어 있지 않습니다. 그렇기 때문에 새로운 CateFilterDTO라는 이름의 클래스를 작성해서 인스턴스화 하여 사용할 것입니다.

 

 반환받을 카테고리 정보가 하나라면 반환타입을 CateFilterDTO로 지정하면 됩니다. 하지만 사용자가 작성한 키워드가 책 제목 혹은 작가 이름으로 존재하는 카테고리 정보가 여러 개 일 수도 있습니다. 따라서 CateFilterDTO를 요소로 가지는 List 자료구조인 List <CateFilterDTO>를 반환 타입으로 지정해 줄 것입니다.

 

 어떠한 데이터가 필요로하고 반환 타입도 정하였으니 어떻게 DB에 데이터를 요청하고 가공하여 List <CateFilterDTO> 타입의 데이터를 반환할지 고민할 차례입니다.

 

 만약 '카테고리 이름', '카테고리 코드번호' 데이터만 필요로 하다면 카테고리 정보가 있는 테이블(vam_bcate)과 상품 테이블(vam_book )을 조인(JOIN)하여 정보를 만들어내면 되기 때문에 하나의 쿼리문만 있으면 됩니다. 문제는 '개수'에 대한 데이터를 만들어 내는 것입니다. 이 데이터를 각각의 카테고리에 따른 '상품의 개수'를 얻어야 하기 때문에 상품이 속한 카테고리 수만큼 쿼리문이 실행되어야 합니다.

 

 이러한 이유 때문에 큰 틀에서 두 가지 과정을 거치도록 로직(logic)을 짤 것입니다.

 

1. 상품 '검색 조건'에 속하는 '카테고리 코드 번호'를 반환하는 쿼리문 요청 하여 '카테고리 코드 번호' 리스트를 얻음

 

2. '검색 조건', '카테고리 코드 번호' 를 조건으로 하는 쿼리문을 요청 후 반환받은 데이터를 List 자료구조 요서에 저장( '카테고리 코드 번호' 개수만큼 반복 실행하여 List 자료구조 요소에 저장)

 

이 큰틀에 맞춰서 구현을 시작해보겠습니다.

 

 

2. CateFilterVO.class 생성

 먼저 반환 데이터를 담을 그릇인 CateFilterDTO 클래스를 만들겠습니다. com.vam.model에 CateFilterDTO 클래스를 생성하고 아래와 같이 변수를 선언해줍니다.

 

	/* 카테고리 이름 */
	private String cateName;
	
	/* 카테고리 넘버 */
	private String cateCode;;
	
	/* 카테고리 상품 수 */
	private int cateCount;	
	
	/* 국내,국외 분류 */
	private String cateGroup;

	public String getCateName() {
		return cateName;
	}

	public void setCateName(String cateName) {
		this.cateName = cateName;
	}

	public String getCateCode() {
		return cateCode;
	}

	public void setCateCode(String cateCode) {
		this.cateCode = cateCode;
	}

	public int getCateCount() {
		return cateCount;
	}

	public void setCateCount(int cateCount) {
		this.cateCount = cateCount;
	}

	public String getCateGroup() {
		return cateGroup;
	}

	public void setCateGroup(String cateGroup) {
		this.cateGroup = cateGroup;
	}

	@Override
	public String toString() {
		return "CateFilterDTO [cateName=" + cateName + ", cateCode=" + cateCode + ", cateCount=" + cateCount
				+ ", cateGroup=" + cateGroup + "]";
	}

 

그림 3-1

 

 그리고 기존 계획했던 cateCode, cateCount, cateName에 더해 cateGroup 변수를 하나 더 추가해주었습니다. 이는 국내, 국외 카테고리 구분을 쉽게 하기 위하여 추가해준 변수입니다.

 

 cateGroup의 경우 cateCode변수에 값이 들어올때 값이 세팅이 도록 해주어야 합니다. cateGroup은 국내의 경우 1, 국외의 경우 2가 값이 되도록 할 것입니다. 이를 위해서 setCateCode() 메서드에 아래의 코드를 추가해줍니다.

 

this.cateGroup = cateCode.split("")[0];

 

그림 3-2

 

 cateCode가 가지는 값들은 '101001','201001'과 같은 형식인데 제일 첫 번째 숫자가 국내, 국외 구분을 할 수 있는 숫자입니다. 따라서 이 숫자를 추출하여 cateGroup 변수에 값으로 저장 해주록 해주었습니다.

 

 

3. Mapper 메서드

 앞서 언급한 Service 메서드가 수행할 로직에서 2개의 쿼리문이 필요로 합니다. 이 2개의 쿼리문을 수행할 Mapper 메서드를 먼저 작성해보겠습니다.

 

BookMapper.java

 

 BookMapper.java 인터페이스에 아래 두 가지의 메서드 선언부를 작성해줍니다.

 

	/* 검색 대상 카테고리 리스트 */
	public String[] getCateList(Criteria cri);
	
	/* 카테고리 정보(+검색대상 갯수) */
	public CateFilterDTO getCateInfo(Criteria cri);

 

그림 4-1

 

 첫 번째 메서드 getCateList()는 코드번호를 String 배열에 담아서 반환하게 됩니다. 기존 검색에 사용한 조건 데이터(Criteria)를 조건문에 사용하기 때문에 파라미터로 Criteria를 지정해주었습니다.

 

 두 번째 메서드 getCateInfo()는 우리가 최종적으로 얻고자 하는 '카테고리 이름', '카테고리 코드', '개수'정보가 담길 수 있는 CateFilterVO클래스를 반환 타입으로 지정하였습니다. 위와 동일하게 Criteria를 파라미터로 부여하였습니다.

 

BookMapper.xml

 

 먼저 getCateList()의 쿼리문입니다. 

 

<!-- Oracle -->
	<!-- 검색 대상 카테고리 리스트 -->
	<select id="getCateList" resultType="String">
	
		select DISTINCT cateCode from vam_book where 
		<foreach item="type" collection="typeArr">		
  				<choose>
  					<when test="type == 'A'.toString()">
  						<trim prefixOverrides="or">
		  					<foreach collection="authorArr" item="authorId">
		  						<trim prefix="or">
		  							authorId = #{authorId}
		  						</trim>
		  					</foreach>  						
  						</trim>
  					</when>
  					<when test="type == 'T'.toString()">
  						bookName like '%' || #{keyword} || '%'
  					</when>  					
  				</choose>
  		</foreach>
	
	</select>

 

그림 4-2

 

<!-- MySQL -->
	<!-- 검색 대상 카테고리 리스트 -->	
	<select id="getCateList" resultType="String">
	
		select distinct cateCode from vam_book where 
		<foreach item="type" collection="typeArr">		
  				<choose>
  					<when test="type == 'A'.toString()">
  						<trim prefixOverrides="or">
		  					<foreach collection="authorArr" item="authorId">
		  						<trim prefix="or">
		  							authorId = #{authorId}
		  						</trim>
		  					</foreach>  						
  						</trim>
  					</when>
  					<when test="type == 'T'.toString()">
  						bookName like concat ('%', #{keyword}, '%')
  					</when>  					
  				</choose>
  		</foreach>
	
	</select>

 

그림 4-3

 

 

 이 쿼리 문의 경우 '작가 검색'(type="A") 혹은 '책 제목 검색'(type="T") 일 경우에만 동작하기 때문에, 2개의 타입 경우에 따라 쿼리문이 작성되도록 작성하였습니다. 동적 쿼리문을 통해 생성될 수 있는 쿼리문은 아래 2개의 쿼리문입니다.

select distinct cateCode from vam_book where authorId = #{authorId} OR orauthorId = #{authorId} ...

select distinct cateCode from vam_book where select distinct cateCode from vam_book where

 

 이 쿼리문에서 이전 쿼리문들과 다르게 새로운 키워드를 사용하였는데 DISTINCT입니다. 이 키워드를 사용하게 되면 중복되는 데이터를 제외하고, 중복되지 않는 행들만 출력이 되게 됩니다.

 

 

 

 그다음 getCateInfo()입니다. 

 

<!-- Oracle -->
	<!-- 카테고리 정보(+검색대상 갯수) -->	
	
	<select id="getCateInfo" resultType="com.vam.model.CateFilterDTO">
	
		select DISTINCT count(*) cateCount, a.cateCode, b.cateName from vam_book a left join vam_bcate b on a.cateCode = b.cateCode 
		
		where 

		<foreach item="type" collection="typeArr">		
  				<choose>
  					<when test="type == 'A'.toString()">
  					
  						<trim prefix="(" suffix=")" prefixOverrides="or">
  						
		  					<foreach collection="authorArr" item="authorId">
		  					
		  						<trim prefix="or">
		  						
		  							authorId = #{authorId}
		  							
		  						</trim>
		  						
		  					</foreach>
		  					  						
  						</trim>
  						
  						and a.cateCode = #{cateCode}
  						
  					</when>
  					
  					<when test="type == 'T'.toString()">
  					
  						bookName like '%' || #{keyword} || '%' and a.cateCode = #{cateCode}
  						 
  					</when>
  					  					
  				</choose>
  		</foreach>
  		
  		group by a.cateCode, b.cateName
	
	</select>

 

그림 4-4

 

<!-- MySQL -->
	<!-- 카테고리 정보(+검색대상 갯수) -->		
	<select id="getCateInfo" resultType="com.vam.model.CateFilterDTO">
	
		select DISTINCT count(*) cateCount, a.cateCode,b.cateName from vam_book a left join vam_bcate b on a.cateCode = b.cateCode 
		
		where 

		<foreach item="type" collection="typeArr">		
  				<choose>
  					<when test="type == 'A'.toString()">
  					
  						<trim prefix="(" suffix=")" prefixOverrides="or">
  						
		  					<foreach collection="authorArr" item="authorId">
		  					
		  						<trim prefix="or">
		  						
		  							authorId = #{authorId}
		  							
		  						</trim>
		  						
		  					</foreach>
		  					  						
  						</trim>
  						
  						and a.cateCode = #{cateCode}
  						
  					</when>
  					
  					<when test="type == 'T'.toString()">
  					
  						bookName like concat ('%', #{keyword}, '%') and a.cateCode = #{cateCode}
  						 
  					</when>
  					  					
  				</choose>
  		</foreach>
	
	</select>

 

그림 4-5

 

 

 이 쿼리문에서는 '검색 조건( C or T)'과 '카테고리 코드번호' 두 조건을 충족하는 카테고리 정보가 출력되도록 작성하였습니다. 동적 쿼리문을 통해 생성될 수 있는 쿼리문은 아래 2개의 쿼리문입니다.

 

select DISTINCT count(*) cateCount, a.cateCode,b.cateName 
from vam_book a left join vam_bcate b on a.cateCode = b.cateCode
where (authorId = #{authorId or authorId = #{authorId...)
and a.cateCode = #{cateCode}

select DISTINCT count(*) cateCount, a.cateCode,b.cateName 
from vam_book a left join vam_bcate b on a.cateCode = b.cateCode
where bookName like '%' || #{keyword} || '%' 
and a.cateCode = #{cateCode}

 

 

 '개수'데이터를 얻기 위해서 "COUNT()"함수를 사용하였습니다. 주의할 점은 별칭을 반드시 주어야 한다는 점입니다. 별칭을 주지 않는다면 메서드 반환 객체인 CateFilterDTO의 cateCount 변수에 데이터가 저장되지 않은 채 반환되게 됩니다. MyBatis는 '자바 빈 규약'에 따라서 지정한 반환 타입에 데이터를 세팅하게 되는데, SELECT문에 지정한 컬럼(Column)이름에 따라 "set컬럼이름" 메서드를 호출하여 객체의 변수들에 데이터를 세팅을 하기 때문입니다.

 

※ Oracle의 경우 GORUP BY 키워드를 사용하지 않으면 오류가 뜹니다.

 

※ COUNT() 함수를 사용하여서 중복되지 않은 결과들이 뜨긴 하지만, 좀 더 명확히 하기 위해 DISTINCT를 같이 작성해주었습니다. DISTINCT를 쓰든 쓰지 않든 의도한 결과가 뜹니다. 어떠한 방식이 맞는지는 잘 모르겠습니다. 

 

 

4. BookMapperTests.java

 

 작성한 Mapper 메서드를 의도한 대로 동작하는지 확인하기 위해서 BookMapperTests.java 클래스에 코드를 작성하여 Junit 테스트를 진행합니다. getCateList(), getCateInfo() 메서드 둘 다 type이 "AC", "TC"인 상황을 주어서 테스트를 하였습니다.

 

 getCateList() 메서드는 아래의 코드를 통해서 테스트를 하였습니다.

 

	/* 카테고리 리스트 */
	@Test
	public void getCateListTest1() {
		
		Criteria cri = new Criteria();
		
		String type = "TC";
		String keyword = "test";
		//String type = "A";
		//String keyword = "유홍준";		

		cri.setType(type);
		cri.setKeyword(keyword);
		//cri.setAuthorArr(mapper.getAuthorIdList(keyword));		
		
		String[] cateList = mapper.getCateList(cri)		;
		for(String codeNum : cateList) {
			System.out.println("codeNum ::::: " + codeNum);
		}
		
		
	}

 

그림 5-1

 

그림 5-2

 

 "AC"인 상황에서 쿼리에 authorId값이 안 들어오는 경우 에러가 나는 것을 확인하였습니다. 따라서 Service 단계에서 이쿼리를 호출할 시에 authorId가 없을 경우 쿼리가 동작하지 않도록 해 줄 것입니다.

 

 getCateInfo() 메서드는 다음의 코드를 통해서 테스트를 하였습니다.

 

	/* 카테고리 정보 얻기 */	
	@Test
	public void getCateInfoTest1() {
		
		Criteria cri = new Criteria();
		
		String type = "TC";
		String keyword = "test";	
		String cateCode="104001";

		cri.setType(type);
		cri.setKeyword(keyword);
		cri.setCateCode(cateCode);
		
		mapper.getCateInfo(cri);
		
	}

 

 

 

 

 

REFERENCE

  • 코드로배우는 스프링 웹 프로젝트(남가람북스)

 

 

DATE

  • 2020.09.28

 

728x90
반응형
Comments