코딩항해기
[JSP] JDBC - 필터 검색 (Model 파트/HashMap 사용) 본문
기존 방식에서는 검색 쿼리를 실행시키기 위해 C에서 컨디션값을 받아와 해당 고정된 쿼리를 불러와 값을 넣어 필요한 데이터를 반환했다. 이 방식의 단점은 단일 검색이 아니라 다양한 조건이 중첩되는 검색을 할 때 두드러진다.
예를 들어, 특정 카테고리, 특정 검색어, 기간 7일, 15일, 30일을 설정해 검색할 수 있는 기능이 있다고 가정한다.
기존의 방식대로 했을 경우 검색어는 특별히 컨디션을 나누지 않는다고 하더라도, 카테고리 유무에 따라 2가지, 기간에 따라 (7일, 15일, 30일) 4가지를 곱해 모든 경우의 수인 8개의 컨디션 값이 나와야한다. 다른 검색 조건이 추가된다면 더욱 컨디션의 수가 늘어날 수 밖에 없는 구조이다. 특히 같은 쿼리문이 미세한 부분만 다르게 반복되므로, 효율이 떨어진다.
쿼리 분할
이러한 문제를 해결하기 위한 방법으로는 쿼리를 분할하는 방법이 있다. 검색 조건에 맞춰 조건문을 추가하고 빼는 방식으로 조합하는 것이다. 기존에는 한 덩어리의 고정된 쿼리였기 때문에, 일부 조건을 빼고자할 때도 별도의 쿼리문을 전부 작성해 필드값으로 선언해야했지만, 쿼리를 분할해 조합해서 사용하면 검색 조건을 원하는대로 빼고 넣는 것이 가능해진다.
(그림과는 다른 예시 - 필드로 선언한 부분)
private final String SELECTONE_CNT = "SELECT COUNT(*) AS CNT FROM BB_PRODUCT bp LEFT JOIN BB_BOARD bb ON bp.BOARD_NUM = bb.BOARD_NUM WHERE 1=1 ";
//쿼리파츠
//검색조건에 따라 해당하는 조건문만 추가한다.
private final String SELECT_PART_CATEGORY = "AND PRODUCT_CATEGORY_NUM = ?";
private final String SELECT_PART_NAME = "AND PRODUCT_NAME LIKE '%'||?||'%'";
private final String SELECT_PART_TITLE = "AND BOARD_TITLE LIKE '%'||?||'%'";
private final String SELECT_PART_PRICE_MIN = "AND PRODUCT_PRICE >= ?";
private final String SELECT_PART_PRICE_MAX = "AND PRODUCT_PRICE <= ?";
하나의 기능에 하나의 쿼리문만 있었던 기존 방식과 달리 조건문을 가진 쿼리문이 전부 분리되어 있어 조합해 사용할 수 있도록 되어있다.
이제 여러 개의 컨디션으로 분리할 필요없이 검색이라면 하나의 컨디션 분기 내에서 처리할 수 있게 되었으나, Controller로부터 어떤 조건들을 사용하는지 결정지어줄 값을 받아오는 과정이 필요해졌다. 컨디션 분기 내부에 별도의 컨디션값이 있다고 이해하면 쉬울 것 같다.
이 값을 받아와 처리하는 방식은 다양하게 있지만, 이번에는 컬렉션프레임워크 중 하나인 HashMap을 사용해 정리할 예정이다.
컬렉션 프레임워크(HashMap) 사용
먼저 컬렉션 프레임워크를 사용한 이유는 응집도를 높이기 위해서다. 모든 경우의 수를 String 필드로 선언해 사용하는 것도 가능하지만, 하나의 컬렉션 프레임워크, 배열 등으로 묶으면 응집도가 훨씬 올라가고 다양한 메서드를 사용할 수 있다.
그 중 HashMap으로 선정한 이유는 Map이 키와 밸류 구조로 이루어져있기 때문이다, 이번 필터 검색 기능에서는 모든 조건에 동적인 값이 세트로 묶여있는 구조이기 때문에 잘 어울린다고 생각했다. 그리고 정렬 조건은 고정되어있어 따로 순서가 필요하지 않은 점도 선정 이유에 한 몫했다.
DTO에 컬렉션 프레임워크 필드 추가
public class ProductDTO {
private int productNum; //상품번호
private String productName; //이름
private int boardNum; //게시글번호
private int productPrice; //가격
private String productProfileWay; //썸네일 이미지
private int productCateNum; //카테고리 번호
//타 테이블
//게시글 (board)
private String boardTitle; //제목
private String boardContent; //내용
//상품 카테고리
private String productCateName; //카테고리명
//개발용
private String condition; //컨디션
private HashMap<String, String> filterList; //필터검색용 <검색구분용, 검색값>
private int startNum; //페이지네이션 시작번호
private int endNum; //페이지네이션 끝번호
private int cnt; //페이지네이션 개수 반환용
만약 ArrayList를 사용할 경우 ArrayList에는 구분할 값을 담고, 키워드는 별도의 String이나 int 필드로 받으면 된다.
상황이나 설계에 더 알맞는, Controller와 합의된 컬렉션 프레임워크, 방식을 사용하면 된다.
쿼리 재조합
쿼리를 분할하고, C로부터 어떤 조건을 적용할지 약속하는 방식도 결정했으므로, 이제 쿼리를 원하는 방식으로 재조합 할 준비가 됐다. 어떤 조건이 필요한지 확인하는 부분은 PreparedStatement 기준으로 앞쪽과 뒤쪽에 있다. 먼저 쿼리문을 PreparedStatement에 보내기 전에 한 번, 보내고 나서 값을 넣어줄 때 한 번 이렇게 두 번 필요하다. 조건의 순서와, 값이 들어가는 순서가 동일해야한다는 점을 주의해야한다.
먼저, 쿼리를 완성하는 부분이다.
if(filters != null && !filters.isEmpty()) {
//만약 필터 검색을 한다면 (== C에게서 넘어온 filter 키워드가 있다면)
System.out.println("log: Product selectAll filter search");
for(String key : filters.keySet()) {
if(key.equals("A")) {
//카테고리 검색추가
System.out.println("log: Product selectAll filterList ProductCategory Search");
query.append(" "+SELECT_PART_CATEGORY);
}
else if(key.equals("B")) {
//이름 검색추가
System.out.println("log: Product selectAll filterList ProductName Search");
query.append(" "+SELECT_PART_NAME);
}
else if(key.equals("C")) {
//제목 검색추가
System.out.println("log: Product selectAll filterList ProductTitle Search");
query.append(" "+SELECT_PART_TITLE);
}
else if(key.equals("D")) {
//가격최소 검색추가
System.out.println("log: Product selectAll filterList Price Min Search");
query.append(" "+SELECT_PART_PRICE_MIN);
}
else if(key.equals("E")) {
//가격최대 검색추가
System.out.println("log: Product selectAll filterList Price Max Search");
query.append(" "+SELECT_PART_PRICE_MAX);
}
}
}//필터검색여부 확인 if문 종료
*필터 검색 구분값 ABC는 임의의 값으로 추후 Controller에게서 받아와 변동될 예정이다.
조건이 아예 붙지 않는 경우도 존재하므로, 조건을 추가하기를 요청하는지를 if문으로 확인한다.
값이 있다는 것은 추가할 조건이 있다는 것을 의미하므로, 해당 if문 내부의 반복문을 실행한다.
현재는 쿼리를 완성하기 위해서 Key값만 필요하기 때문에, keyset으로 key값을 전부 불러와 빠른 for문을 돌려 컬렉션 프레임워크에 저장되어있는 임의의 순서대로 쿼리문을 누적하고 있다. 이 때 String을 사용하여 누적 연결을 해줄 수도 있지만, String은 불변이므로 메모리 낭비가 심해 StringBuilder를 사용했다. StringBuilder에 대해서는 나중에 따로 다룰 예정이다.
append가 += 와 동일하다고 생각하면 된다. 이 때 주의할 점은 쿼리가 서로 붙는 일이 없도록 주의해야한다.
String a = "SELECT * FROM";
String b = "BOARD";
a += b;
syso(a); //SELECT * FROMBOARD (쿼리 실행시 에러발생)
//서로 붙어 정상적인 쿼리문이 아니게 되어버렸다. 누적할 때 스페이스바를 한 번 넣어주어 이러한 경우를 방지해야한다.
(+추가)
완성한 쿼리는 PreparedStatement에 보내주면된다. 이 때 주의할 점은 StringBuilder를 사용했으므로 다시 String으로 바꿔줘야한다.
pstmt = conn.prepareStatement(query.append(" "+SELECTALL_ENDPART).toString());
값 넣기
이제 동적 데이터를 넣어주기 위해 플레이스홀더(물음표 ?)를 사용했던 부분을 채워줘야한다. 하지만 이때 어떤 조건이 몇 개나 어떤 순서로 올지 알 수 없기 때문에 기존의 방식으로는 값을 넣을 수 없어, int 변수와 빠른 for문(for each)문을 다시 사용해야한다.
이때 검색어로 사용되는 밸류 값을 불러와야하므로 이때는 Entry 값으로 불러왔다.
pstmt = conn.prepareStatement(query.append(" "+SELECTALL_ENDPART).toString());
int placeholderNum = 1; //필터검색 선택한 것만 검색어를 넣기 위한 카운트
if(filters != null && !filters.isEmpty()) {
//만약 필터 검색을 한다면 (== C에게서 넘어온 filter 키워드가 있다면)
for(Entry<String, String> keyword : filters.entrySet()) {
if(keyword.getKey().equals("A")) {
//카테고리 검색어
pstmt.setInt(placeholderNum++, Integer.parseInt(keyword.getValue()));
//넘어온 값 확인 로그
System.out.println("log: parameter ProductCategory Search : "+keyword.getValue());
}
else if(keyword.getKey().equals("B")) {
//이름 검색어
pstmt.setString(placeholderNum++, keyword.getValue());
//넘어온 값 확인 로그
System.out.println("log: parameter ProductName Search : "+keyword.getValue());
}
else if(keyword.getKey().equals("C")) {
//제목 검색어
pstmt.setString(placeholderNum++, keyword.getValue());
//넘어온 값 확인 로그
System.out.println("log: parameter ProductTitle Search : "+keyword.getValue());
}
else if(keyword.getKey().equals("D")) {
//가격 검색어
pstmt.setInt(placeholderNum++, Integer.parseInt(keyword.getValue()));
//넘어온 값 확인 로그
System.out.println("log: parameter Price Min Search : "+keyword.getValue());
}
else if(keyword.getKey().equals("E")) {
//가격 검색어
pstmt.setInt(placeholderNum++, Integer.parseInt(keyword.getValue()));
//넘어온 값 확인 로그
System.out.println("log: parameter Price Max Search : "+keyword.getValue());
}
}
}//필터검색여부 확인 if문 종료
pstmt.setInt(placeholderNum++, productDTO.getStartNum()); //페이지네이션 용 시작번호
pstmt.setInt(placeholderNum++, productDTO.getEndNum()); //페이지네이션 용 끝번호
//넘어온 값 확인 로그
System.out.println("log: parameter getStartNum : "+productDTO.getStartNum());
System.out.println("log: parameter getEndNum : "+productDTO.getEndNum());
ResultSet rs = pstmt.executeQuery();
*필터 검색 구분값 ABC는 임의의 값으로 추후 Controller에게서 받아와 변동될 예정이다.
이때 주의할 점은 고정된 값과 유동적인 값이 들어가는 위치 조절과 형변환이다. 만약 int타입(NUMBER타입)의 값이 들어가는 위치라면 String 타입으로 값을 받아왔기 때문에 형변환을 반드시 해주어야 쿼리가 정상 작동한다.
DAO에서 이러한 부분이 반복해 사용된다면 별도의 메서드로 빼서 정리하는 것도 좋은 방식이 될 것 같다.
(selectOne 메서드 부분)
//상품리스트 (+필터검색)
//필터검색 추가
HashMap<String, String> filters = productDTO.getFilterList();//넘어온 MAP filter키워드
pstmt = conn.prepareStatement(filterSearch(SELECTALL,filters).append(" "+SELECTALL_ENDPART).toString());
int placeholderNum = 1; //필터검색 선택한 것만 검색어를 넣기 위한 카운트
placeholderNum = filterKeywordSetter(pstmt,filters,placeholderNum); //필터 검색 검색어
if(placeholderNum < 0) {
//만약 filterKeywordSetter 메서드에서 오류가 발생했다면 SQL예외처리
throw new SQLException();
}
pstmt.setInt(placeholderNum++, productDTO.getStartNum()); //페이지네이션 용 시작번호
pstmt.setInt(placeholderNum++, productDTO.getEndNum()); //페이지네이션 용 끝번호
//넘어온 값 확인 로그
System.out.println("log: parameter getStartNum : "+productDTO.getStartNum());
System.out.println("log: parameter getEndNum : "+productDTO.getEndNum());
ResultSet rs = pstmt.executeQuery();
while(rs.next()) {
ProductDTO data = new ProductDTO();
data.setProductNum(rs.getInt("BOARD_NUM")); //상품번호
//반환된 객체 리스트에 추가
datas.add(data);
System.out.print(" | result "+data.getProductNum());
}
rs.close();
System.out.println("end");
(메서드 부분)
//필터검색 메서드
private StringBuilder filterSearch(String startQuery, HashMap<String, String> filters) {
System.out.println("log: Product selectOne filterSearch start");
StringBuilder query = new StringBuilder(startQuery); //StringBuilder 객체 생성
if(filters != null && !filters.isEmpty()) {
//만약 필터 검색을 한다면 (== C에게서 넘어온 filter 키워드가 있다면)
System.out.println("log: filterSearch filter search");
for(String key : filters.keySet()) {
if(key.equals("A")) {
//기간 검색추가
System.out.println("log: filterSearch ProductCategory Search");
query.append(" "+SELECT_PART_CATEGORY);
}
else if(key.equals("B")) {
//제목 검색추가
System.out.println("log: filterSearch ProductName Search");
query.append(" "+SELECT_PART_NAME);
}
else if(key.equals("C")) {
//제목 검색추가
System.out.println("log: filterSearch ProductTitle Search");
query.append(" "+SELECT_PART_TITLE);
}
else if(key.equals("D")) {
//가격최소 검색추가
System.out.println("log: filterSearch Price Min Search");
query.append(" "+SELECT_PART_PRICE_MIN);
}
else if(key.equals("E")) {
//가격최대 검색추가
System.out.println("log: filterSearch Price Max Search");
query.append(" "+SELECT_PART_PRICE_MAX);
}
}
}//필터검색여부 확인 if문 종료
System.out.println("log: filterSearch end");
return query;
}
private int filterKeywordSetter(PreparedStatement pstmt, HashMap<String, String> filters, int placeholderNum) {
System.out.println("log: filterKeyword start");
if(filters != null && !filters.isEmpty()) {
System.out.println("log: filterKeyword is not null");
//만약 필터 검색을 한다면 (== C에게서 넘어온 filter 키워드가 있다면)
for(Entry<String, String> keyword : filters.entrySet()) {
try {
if(keyword.getKey().equals("A")) {
//카테고리 검색어
pstmt.setInt(placeholderNum++, Integer.parseInt(keyword.getValue()));
//넘어온 값 확인 로그
System.out.println("log: parameter ProductCategory Search : "+keyword.getValue());
}
else if(keyword.getKey().equals("B")) {
//이름 검색어
pstmt.setString(placeholderNum++, keyword.getValue());
//넘어온 값 확인 로그
System.out.println("log: parameter ProductName Search : "+keyword.getValue());
}
else if(keyword.getKey().equals("C")) {
//제목 검색어
pstmt.setString(placeholderNum++, keyword.getValue());
//넘어온 값 확인 로그
System.out.println("log: parameter ProductTitle Search : "+keyword.getValue());
}
else if(keyword.getKey().equals("D")) {
//가격 검색어
pstmt.setInt(placeholderNum++, Integer.parseInt(keyword.getValue()));
//넘어온 값 확인 로그
System.out.println("log: parameter Price Min Search : "+keyword.getValue());
}
else if(keyword.getKey().equals("E")) {
//가격 검색어
pstmt.setInt(placeholderNum++, Integer.parseInt(keyword.getValue()));
//넘어온 값 확인 로그
System.out.println("log: parameter Price Max Search : "+keyword.getValue());
}
} catch (NumberFormatException e) {
System.err.println("log: filterKeyword NumberFormatException");
return -1;
} catch (SQLException e) {
System.err.println("log: filterKeyword SQLException");
return -1;
}
}
}//필터검색여부 확인 if문 종료
System.out.println("log: filterKeyword end");
return placeholderNum;
}
'JSP' 카테고리의 다른 글
[JSP] JDBC - 컨디션 하드코딩 방지, 컨디션 값 변동이 예상될 때 (1) | 2024.09.15 |
---|---|
[JSP] 서버 시작할 때 크롤링한 샘플데이터 추가하기 (2) | 2024.09.12 |
[JSP] Servlet - 필터 Filter (0) | 2024.09.05 |
[JSP] alert창 띄우고 페이지 전환하기 (0) | 2024.09.04 |
[JSP] Servlet - 리스너 Listener (0) | 2024.09.02 |