웹 개발 Spring Day6 게시글 내용으로 검색, 페이징 처리 (PaigingModel Class를 통한)

김지원·2022년 8월 3일
0

WebDevelop2

목록 보기
25/34

  • 고객이라고 검색하면 '고객'이라고 DB에 들어갈텐데 '?\n'처럼 홑따옴표를 붙이지 않아도 그냥 해도 쿼리가 잘 돌아가는 이유는 PreparedStatement를 썼기 때문에 문자열이 들어가게 되면 홑따옴표를 알아서 붙여준다. (Statement와의 다른점이다.) 대신 이렇게 사용하면 절대로 홑따옴표를 처리해주면 안된다.

홑따옴표를 자동으로 붙여주는 것은 SQL Injection를 방어하기 위해서다.

SQL Injection

로그인하는 쿼리를 짠다고 해보자.

SELECT * 
FROM `member` .`user`
WHERE `id` = ?
AND `password` = ?
LIMIT 1

참고) LIMIT 1 은 하나만 찾으면 그만두라는 것 의미이고 상당히 빠르게 진행이 된다는 장점이 있다.

위의 쿼리를 문자열로써 처리한다고 하면 아래와 같이 적힐 것이다.

String query = "SELECT * FROM `member`.`user` WHERE `id` = '" + id + "' AND `password` = '"+password+"' LIMIT 1";

adimin이라는 id와 아래와 같은 비밀번호로 로그인을 시도한다고 했을 때

String id = "admin";
String password = "abc' OR 1 = 1 --"

위의 아이디와 비밀번호로 로그인을 하게 되면 이렇게 들어가게 된다.

String query = "SELECT * FROM `member`.`user` WHERE `id` = 'admin' AND `password` = '"abc' OR 1 = 1 --"' LIMIT 1";
  • 비밀번호가 abc 이거나 1 = 1 (true) 이다 라고 되버리고 -- 뒤에 것들은 주석처리가 되어 없는 것이 됨으로 관리자(admin)으로 로그인이 되버릴 것이다.

이러한 공격기법이 SQL Injection이다. 옛날에 막혀버린 공격기법이다.
따라서 문자열을 넣는게 아닌 set을 통해서 넣는게 안전하다.


내용으로 검색

LIKE '%...%'

: %는 아무거나

WHERE `content` LIKE '%마포%'  : 마포를 포함하고 있는 content

%마포 : 마포로 끝난다.
마포% : 마포로 시작

  • 여기서 문제는 마포자체가 이미 문자열이기 때문에 Dao의 ? 안에 집어넣을 수 없다. 마포를 포함해서 WHERE문을 적어버리면 검색을 할 때 마포라는 단어를 포함하지 않는다면 검색이 되지 않기 때문이다.
  • 문자열 합치기(CONCAT)를 사용한다.

→ InquiryService

  • searchByContent 메서드 추가

→ InquiryDao

→ InquiryService

→ HomeController

  • '되유'라고 검색했을 때 해당 검색어에 알맞는 문의만 나온다.

중복된 코드 메서드로 빼자 -> parseInquiry

  • 지금까지 만들었던 Dao의 메서드들 모두에 위와 같은 중복된 코드가 들어가있기 때문에 메서드로 빼서 작성하자.
  • 같다 붙였더니 resultSet에 빨간줄이 막들어왔다.
  • 매개변수로 ResultSet을 빼자.
  • result.add(InquiryDao.parseInquiry(resultSet)); 한 줄로 딱 줄여서 사용가능해졌다.

HttpServletRequest

: 요청에 대한 정보를 담고 있다.

#request

: 요청에 대한 모든 정보를 담고 있는 HttpServletRequest 객체이다.

getParameter(x)

: URL에 존재하는 변수 중 전달 인자 x의 이름을 가진 값을 반환한다.

setAttribute(n,v)

: 요청 객체에 n이라는 이름으로 v라는 값을 추가한다.

getAttribute(n)

: 요청 객체로부터 n이라는 이름으로 값을 가져온다.

주소의 매개변수

본 주소가 끝난 뒤 물음표(?)를 적으면 그 자리 부터 매개변수 자리이고 매개 변수는 이름=값 쌍 혹은 이름 형태로 작성한다. 쌍간의 구분은 앰퍼샌드(&)로 한다. 가령 /...?n1=v1&n2=v2&n3=v3...

HttpServletResponse

: 응답에 대한 정보를 담고 있다.

#response

: 응답에 대한 모든 정보를 담고 있는 HttpServletResponse 객체이다.


검색을 할 때 문의 내용과 작성자가 고정이 되어있으면 좋겠다.
index.html로 가자.

th:value="값"

: input 요소 등의 값을 지정한다.

  • 검색을 하고 난 후 경로를 보면 'keyword'라는 이름으로 '백종원'이라는 값이 서버로 "요청 ! "을 보낸 걸 볼 수 있다.

keyword에 어떤 값이 있는가를 가져와서 html에서 사용을 할 것이다.

검색창 input 태그로 가자.

th:value="${#request.getParameter('keyword')}
  • URL에 존재하는 변수 중 keyword의 이름을 가진 값을 반환해주고 #request가 보여주게 된다.
  • input에 검색했던 내용이 사라지지 않고 남아있게 된다.

  • 개발자 도구 네트워크에 들어가서 localhost → Request Headers(요청 헤더)를 보면 User-Agent 가 있다. 이 값을 getIndex에서 찍어보고 싶다면.

User-Agent

: 사용자의 운영체제나 브라우저의 종류를 알려주는 것.

요청에 대한 모든 정보를 다 담고있는 객체의 타입은 HttpServletRequest 이다.

HttpServletRequest request    // 이렇게 받으면 알아서 객체화를 한다.
HttpServletResponse response

System.out.println(request.getHeader("User-Agent"));

th:selected="조건"

: 해당 조건이 참인 경우에만 selected 속성을 부여한다.

<option value="byContent" th:selected="${false}">문의 내용</option>
  • 거짓이라면 위 처럼 태그에 selected가 붙지않는다는 말이다.
  • selected = "selected" => selected랑 똑같은 말

th:with="이름 = 값"

: 일종의 변수 선언 같은 것.


th:with="criteria=${#request.getParameter('criteria')}, keyword=${#request.getParameter('keyword')}"
th:value="${keyword}

검색에 '백종원'을 치고 난 후 새로고침을 해도 계속 남아있는 이유는
주소창 keyword에 백종원이라고 되어있다. (keyword=백종원)
이 keyword라는 이름은 #request.getParameter('keyword') 가 가져온 것이고 그 가져온 것을 form태그의 keyword라는 변수에 들어가게 되고 그 변수에 담긴 것이 input의 keyword 랑 같기 때문에 input으로 이동한다.
request.getParameter가 이러한 역할을 하게 되는것이다.

<select class="input criteria" name="criteria">
	<option value="byContent" th:selected="${criteria == null || !criteria.equals('byWriter')}">문의 내용</option>
	<option value="byWriter" th:selected="${criteria != null && criteria.equals('byWriter')}">작성자</option>
</select>
  • criteria == null 처리를 해주는 이유는 localhost:8080 으로 들어갔을 때는 criteria가 null이기 때문에 NullPointException 이 뜨게 됨으로 처리해줘야한다.

메인페이지면 검색 초기화 비활성화 / 검색을 했다면 검색 초기화 활성화 시키자.

th:disabled="조건"

: 해당 조건이 참인 경우에만 disabled 속성을 부여한다.

  • 참이면 disabled가 생기게 된다.

  • localhost:8080 으로 들어왔거나 검색한 keyword가 없다면 disabled 속성을 가지게 된다. 즉, 비활성화가 된다는 것이다.
  • 검색 후에는 criteria가 null이 아니고 keyword도 null이 아니기 때문에 검색초기화 버튼이 살아나게 된다.

  • search-form의 reset-button (검색 초기화 버튼) 을 잡아서 click 이벤트를 주어 클릭을 했을 때 localhost:8080으로 가게 한다.

Paging

페이지라는 개념이 생기기위해서는 아래의 것들이 필요하다.

페이징 개수                  → paginationCount : 10
한 페이지에 표시할 게시글의 갯수  → rowCountPerPage : 10
전체 게시글 개수              → totalRowCount : ? (DB에서 받아온다.)
요청한 페이지 번호             → requestPage :(1) 
: 없으면 1페이지, 들어올 때 마다 바뀌기 때문에 값 지정해놓지 않는다.
첫 페이지                    → boundStartPage 
: (requestPage / paginationCount) * paginationCount + 1
끝 페이지                    → boundEndPage 
: (requestPage / paginationCount) * paginationCount + paginationCount
최대 페이지                  → maxPage : ? (최대 페이지는 갈 수 있는 최대한의 페이지다.)
: maxPage = totalRowCount / rowCountPerPage + (totalCount % rowCountPerPage == 0 ? 0 : 1)
최소 페이지                  → minPage : 1 (항상1)

1페이지부터 몇십페이지까지 다 표현할 수 없기 때문에 나눠야한다.
요청페이지 5라면 왼쪽으로 4개 오른쪽으로 4개가 배치가 되고 1 - '5' - 9 가 된다.

요청페이지가 7 이면   1 ~ 10 page
요청페이지가 14 이면  11 ~ 20 page
요청페이지가 22 이면  21 ~ 30 page
p			   (p/10) * 10
--------------------------
3  : 1  ~ 10      		 0
15 : 11 ~ 20      		10
22 : 21 ~ 30      		20
--------------------------

PagintModel Class

이 클래스의 목적은 객체화를 할 때 전체 행 갯수(==전체 게시글)와 현재 요청한 페이지의 번호만 넘겨주면 알아서 최소, 최대 페이지와 시작, 끝 페이지를 계산해주게 된다.

이런 페이징 클래스를 생성하면 모든 게시파 페이징에 적용이 가능해진다. 굉장히 편리하다.

package dev.jwkim.studydatabase.models;

public class PagingModel {
    public static final int DEFAULT_PAGINATION_COUNT = 10;
    public static final int DEFAULT_ROW_COUNT_PER_PAGE = 10;

    public final int  paginationCount;
    public final int rowCountPerPage;
    public final int totalRowCount;
    public final int requestPage;

    public final int maxPage;
    public final int minPage = 1;
    public final int boundStartPage;
    public final int boundEndPage;

    public PagingModel(int totalRowCount, int requestPage) {
        // super(); this 혹은 super 이여야하는데 super 은 이미 아래에 있다.
        this(PagingModel.DEFAULT_PAGINATION_COUNT, PagingModel.DEFAULT_ROW_COUNT_PER_PAGE, totalRowCount, requestPage);
    } // this : PagingModel

    public PagingModel(int paginationCount, int rowCountPerPage, int totalRowCount, int requestPage) {
        super();
        if(requestPage > this.minPage) {
            requestPage = this.minPage;
        }
        this.paginationCount = paginationCount;
        this.rowCountPerPage = rowCountPerPage;
        this.totalRowCount = totalRowCount;
        this.maxPage = this.totalRowCount / this.rowCountPerPage + (this.totalRowCount % this.rowCountPerPage == 0 ? 0 : 1);
        if(requestPage > this.maxPage) {
            requestPage = this.maxPage;
        }
        this.requestPage = requestPage;
        this.boundStartPage = (this.requestPage / this.paginationCount) * this.paginationCount + 1;
        this.boundEndPage = Math.min(maxPage, (this.requestPage / this.paginationCount) * this.paginationCount + this.paginationCount);
    }
}

  • 이 4개는 상수처럼 작동할 것이고 이 4개만 알면 페이징이 모두 해결될 수 있다.

생성자를 오버로딩한다.
생성자 오버로딩도 메서드 오버로딩과 조건이 같다. → 매개변수의 구조가 겹치면 안된다.

생성자에서 this를 호출하는 것은 오버로딩된 다른 생성자를 호출한다는 의미이다.
int타입의 매개변수를 4개 가지고 있다. 4개의 매개변수를 가진 PagingModel생성자를 command 클릭해보면 위의 PagingModel생성자 this로 가게 된다.
이런식으로 객체화가 되면 paginationCount과 rowCountPerPage 을 명시하지 않으면 기본 값을 사용한다는 의미이다.

생성자는 두개이다.
하나는 두개의 매개변수를 받고 다른 하나는 네개를 받고 있다.
( == PagintModel()을 하면 4개를 주거나 2개를 줄 수 있다.)
두개의 매개변수를 받고 있는 생성자는 this로 네개의 매개변수를 받는 메서드를 호출하고 있다.
즉, 매개변수를 2개 입력하고, 별도로 paginationCount과 rowCountPerPage의 값을 입력하지 않으면 정적 메서드인 DEFAULT 값 두 개 10, 10이 찍히게 되는 것.

❗️마지막으로 쉽게 말을 해보자면 PaginModel(int a, int b);이렇게 두개만 쓰고 paginationCount과 rowCountPerPage에 대한 것을 정해주지 않았는데 출력해보면 10, 10 기본 값으로 설정이 된다는 것이다.

매개변수 4개를 가진 PaginModel의 super을 눌러보면 object로 가게 된다.
생성자의 첫 부분은 (부모 호출) this이거나 super이며 둘 다 일수 없다.
부모생성자 호출없이는 절대로 객체화를 할 수 없다.

this.boundEndPage = Math.min(maxPage, (this.requestPage / this.paginationCount) * this.paginationCount + this.paginationCount);
  • boundEndPage는 원래의 계산식이랑 내가 가지고 있는 maxPage 중 작은 것을 선택하게 한다.
    boundEndPage가 maxPage가 넘어가는 현상을 막기위해서이다.

private로 막고 getter,setter을 생성해도 되지만 굳이 그럴필요없다. 정말 굳이..


이제 문의의 갯수가 몇개인지... 요청한 페이지가 몇 페이지인지... 등의 정보를 받아와서 처리를 해줘야한다.

→ Controller getIndex로 넘어가서 @RequestParam을 추가해주자.

@RequestParam(name = "page") Optional<Integer> optionalPage
  • 현재 몇 페이지를 요청했는가에 대해 추가

Optional<Integer> 을 사용했을까?
그냥 페이지(localhost:8080)를 들어가게 되면 주소에 page(name) 가 없다.
사용자가 localhost:8080?page=5 이런식으로 적고 들어올꺼란 기대는 없기 때문에 굳이 이렇게 사용하지 않아도 된다고 생각이 든다.

System.out.println(optionalPage);
  • sout을 찍어보니 Optional.empty가 출력이 되었다.

int page = optionalPage.orElse(1);
  • optionalPage가 empty이면 1을 쓰고 아니면 실제 가져온 값을 쓰겠다는 의미이다.
  • http://localhost:8080/?page=5 이렇게 해보니 5가 찍히게 된다.
    localhost:8080 으로 들어가게 되면 sout 1이 찍히게 된다.

int타입은 default가 없기 때문에 이렇게 사용한다. 즉, null일 수 없기 때문이다.

여기서 알 수 있는 점은 PagingModel의 new 할 때 totalRowCount, requestPage 이 두개를 전달해줘야한다.

PagingModel에서 this.requestPage = requestPage; maxPage 에 관한 것을 먼저 만들어 준다.

  • 만약 최대페이지가 12페이지 인데 요청한 페이지가 12페이지보다 클수도있기 때문에 먼저 적어줘야한다. if문을 걸어준다.
this.maxPage = this.totalRowCount / this.rowCountPerPage + (this.totalRowCount % this.rowCountPerPage == 0 ? 0 : 1);
    if(requestPage > this.maxPage) {
        requestPage = this.maxPage;
}  

만일 -1과 같은 페이지를 요구했을 때 필터링 없이 DB로 그냥 넘겨버리면 오류가 발생할 수 도 있기 때문에

if(requestPage > this.minPage) {
	requestPage = this.minPage;
}  
  • 이렇게 처리를 해준다.

requestPage에 관해 minPage if문 처리를 맨위에서 해줘도 되는 이유

  • 실제로 this.requestPage가 참조되는 곳. (위에 표시한 곳) 이러한 것들을 만나기 전에 조취를 취해주면 되기 때문이다. 얘네를 제외한 위에서는 requestPage를 사용하지 않기 때문에 값이 어떻게 되든 상관이 없다.

그렇다면 다른 PagingModel생성자에 대해서는 처리를 안해줘도 되는가?

  • 어차피 밑에서 해줄것이기 때문에 필요없다. 해주면 오류도 발생함으로 못한다.

  • 이렇게 연결이 되기 때문에
  • 기본값이 될 수 있다.
profile
Software Developer : -)

0개의 댓글