ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] SpringBoot 프로젝트 : 리스트 페이징 처리(3) - DB에서 리스트 불러오기(Mapper.xml, Generics, ROWNUM)
    프로젝트/기능 정리 2023. 6. 17. 23:17

    목적

    • 게시판 형식 웹사이트 개발 시 DB에서 글 리스트 불러오기 기능 구현
    • 리스트 게시판 페이징 처리 받은 코드 리뷰

     

    지난 글에서는 요청 받은 컨트롤러가 DB 내 테이블에서 페이징을 위해 원하는 행의 개수를 가져오는 과정을 살펴봤다.

     

    이번에는 이어서 리스트를 불러오는 과정을 살펴보자.

     

    [데이터 흐름도]

    데이터 흐름도는 다음과 같다.

     

    게시판 리스트 데이터 흐름도

     

    컨트롤러의 코드를 다시 보자.

     

    [Controller - pointList.do]

    @RequestMapping("/mypage/pointList.do")
    public ModelAndView pointList(@RequestParam(value = "pageNum", defaultValue = "1") int currentPage, @RequestParam(value = "keyfield", defaultValue = "1") String keyfield, HttpSession session) {
        
        ModelAndView mav = new ModelAndView();
    
        MemberVO user = (MemberVO)session.getAttribute("user");
        
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("keyfield", keyfield);
        map.put("mem_num", user.getMem_num());
        int count = mypageService.selectPointListCountByMemNum(map);
    
        PagingUtil page = new PagingUtil(keyfield, null, currentPage, count, 5, 5, "pointList.do");
    
        List<PayVO> list = null;
    
        if(count > 0) {
            map.put("start", page.getStartRow());
            map.put("end", page.getEndRow());
            map.put("keyfield", keyfield);
    
            list = mypageService.selectPointListByMemNum(map);
        }
    
        mav.addObject("count", count); 
        mav.addObject("list", list);
        mav.addObject("page", page.getPage());
    
        mav.setViewName("pointList");
        return mav;
    }

    지난 글의 과정까지 따라왔다면, 현재 count에는 DB에서 가져온 회원의 총 결제 내역 행의 개수가 들어있음을 알 것이다.

     

    이어서 코드를 보자.

     

    먼저 PagingUtil 클래스의 생성자를 통해 page라는 객체를 생성했다.

     

    이후 글에서 PagingUtil 클래스에 대해 다루겠지만 우선 생성할 때 넘겨주는 인자를 보자.

     

    총 7개의 인자를 넘겨주는데 순서대로 보면 다음과 같다.

     

    1. keyfield: 리스트 정렬 옵션 값(ex. 날짜순 - 1, 가격순 - 2, 사용내역 - 3, 적립내역 - 4 등)
    2. keyword: 검색 기능 구현 시 사용자가 입력한 검색 키워드
    3. currentPage: 현재 사용자가 요청한 페이지
    4. count: DB에서 가져온 총 게시물 수
    5. rowCount: 한 페이지에서 보여줄 게시물 수
    6. pageCount: 한 페이지에서 보여줄 페이지 번호 수
    7. url: 페이지 번호에 넣어줄 호출 페이지URL

     

    하나씩 살펴보자.

     

    [keyfield]

     

    우선 keyfield는 jsp의 다음 태그들과 관련이 있다.

    <select name="keyfield" id="keyfield">
        <option value = "1"<c:if test="${param.keyfield == 1}">selected</c:if>>전체내역</option>
        <option value = "2" <c:if test="${param.keyfield == 2}">selected</c:if>>적립내역</option>
        <option value = "3" <c:if test="${param.keyfield == 3}">selected</c:if>>사용내역</option>
    </select>

    회원이 자신의 이용내역을 원하는 정렬 형식으로 보고자 할 때 select박스의 option값을 바꿀 수 있다.

     

    이때 아래 script를 통해 컨트롤러에 매핑된 주소를 호출해주면서 keyfield의 값을 선택된 option의 값으로 전달한다.

    <script type="text/javascript">
        $('#keyfield').change(function(){
            location.href = "/mypage/pointList.do?keyfield=" + $(this).val();
        });
    </script>

    적립내역을 선택하면 /mypage/pointList.do?keyfield=2 라는 주소로 요청이 전달되고, 컨트롤러는 요청에 해당하는 데이터를 DB에서 찾아 다시 리턴한다.

     

    페이지가 새로 불러와지면서 select박스의 option값이 제일 상단에 위치한 전체내역으로 바뀌는 것을 방지하기 위해 <c:if test="${param.keyfield == 2}">selected</c:if>를 넣어줬다(새로고침되도 적립내역이라는 텍스트가 유지됨).

     

    JSTL을 이용하면 EL표현식으로 ${param.변수명}을 했을 때, request.getParameter("변수명")와 같은 기능을 한다.

     

    여기서는 GET방식으로 keyfield라는 변수명에 값을 담아 전송했기 때문에 param.keyfield를 통해 request에 담겨 있는 값을 가져올 수 있다.

     

    아래 두 포스트를 참고하면 도움이 될 것 같다.

     

    참고 : https://blog.naver.com/dbxhvi/100033562897

     

    참고 : https://velog.io/@jungnoeun/request.getParameter는-GET-or-POST

     

    [keyword]

     

    다음으로 keyword는 검색기능을 구현할 때 사용한다.

     

    keyfield와 마찬가지의 방법으로 input 태그의 값을 GET방식으로 전달하는데, 이 keyword를 xml의 쿼리문에 적용해 원하는 keyword가 포함된 행들을 찾아내게 된다.

     

    여기선 이용하지 않아 null값을 할당했다.

     

    [currentPage]

     

    currentPage는 회원이 특정 페이지 번호를 눌렀을 때, 해당 페이지 번호를 알아내는데 목적이 있다.

     

    @RequestParam을 통해 클라이언트가 pageNum이라는 이름으로 값을 보내면, 그 값을 할당 받는다.

     

    pageNum은 다음 예시와 같이 페이지 수에 따라 생성될 때 이미 매핑되어 있다.

     

    페이지 번호 주소

     

    [count]

     

    count는 이전 글에서 설명했듯, DB에서 가져온 총 게시물의 수가 들어간다.

     

    [rowCount]

     

    rowCount는 한 페이지에서 보여줄 게시물의 수이다.

     

    rowCount 5인 경우

     

    예를 들어 위와 같이 한 화면에 보이는 게시물의 수가 5개로 설정하고 싶다면, 5를 넣어주면 된다.

     

    [pageCount]

     

    pageCount는 한 페이지에서 보여줄 페이지 번호의 수이다.

     

    pageCount 5인 경우

     

    pageCount를 5로 설정한 경우 번호가 5번까지 보이고 다음 버튼이 나오게 된다.

     

    [url]

     

    마지막으로 url은 버튼에 들어갈 링크의 주소가 된다.

     

    여기서는 포인트 조회 페이지를 호출해야 하므로 컨트롤러에 매핑된 주소인 pointList.do를 넣었다.

     

     

    PagingUtil 객체를 생성할 때 위 인자들을 설정해주면,

     

    현재 페이지의 첫 글 번호와 마지막 글 번호를 계산해주고 리스트 하단에 표시할 번호버튼 태그를 만들어 준다.

     

    자세한 PagingUtil 클래스 내부 코드는 다음 글에서 보도록 하자.

     

    [List]

     

    이제 PagingUtil 객체를 통해 얻은 현재 페이지의 첫 글 번호와 마지막 글 번호를 이용해 DB에서 리스트를 가져오는 과정이다.

     

    조건문을 통해 count가 0 이상이면 map 객체에 start(현재 페이지 첫 글 번호), end(현재 페이지 마지막 글 번호), keyfield를 담아준 뒤 주입받은 서비스를 호출해 map 객체를 넘겨줬다.

     

    [Mapper.xml]

    <select id="selectPointListByMemNum" parameterType="map" resultType="payVO">
        SELECT 
           *
        FROM (SELECT 
                 a.*,
                 rownum rnum
              FROM (SELECT
                       y.pay_price,
                       y.pay_plan,
                       y.pay_date,
                       y.pay_content,
                       p.point_point
                    FROM pay y JOIN point p
                    ON y.pay_num = p.pay_num WHERE y.mem_num = #{mem_num}
                    <if test="keyfield == 1">
    
                    </if>
                    <if test="keyfield == 2">
                    <![CDATA[
                        AND p.point_point > 0
                    ]]>
                    </if>
                    <if test="keyfield == 3">
                    <![CDATA[
                        AND p.point_point < 0
                    ]]>
                    </if>
                    ORDER BY y.pay_date DESC
                    )a)
        <![CDATA[
            WHERE rnum >= #{start} AND rnum <= #{end}
        ]]>
    </select>

    이어서 mapper.java는 다음과 같다.

    @Mapper
    public interface MypageMapper {
        public List<PayVO> selectPointListByMemNum(Map<String, Object> map);
    }

    우선 mapper.java를 보면 Map객체를 인자로 받고 List<PayVO>를 리턴타입으로 지정하고 있다.

     

    화살괄호로 표현한 부분은 제네릭이라고 한다.

     

    제네릭 표현식을 사용하면 List에 들어갈 데이터 타입을 지정해 줄 수 있다.

     

    제네릭 표현식 안에는 위처럼 직접 데이터를 담을 객체를 만들어 넣을 수도 있고, String, Integer 등의 객체도 담을 수 있다.

     

    깊게 들어가면 생각보다 어려워 간단히 이 리스트에 담을 데이터 타입은 <이것>이다 정도로 생각하면 될 것 같다.

     

    아래는 제네릭 관련 도움이 될만한 링크다.

     

    참고 : https://okky.kr/questions/514701

     

    참고 : https://opentutorials.org/module/516/6237

     

    참고 : https://wikidocs.net/207

     

    DB에서 검색된 각 행들을 payVO에 담아서 리스트에 저장할 것이므로, resultType은 payVO로 설정했다.

     

    [payVO]

    public class PayVO {
        private int pay_price;
        private int pay_plan;
        private Date pay_date;
        private String pay_content;
        private int point_point;
        
        //getter,setter생략
    }

    payVO는 결제관련 정보를 담아두기 위해 만든 클래스다.

     

    여러 데이터를 선언해두고 getter/setter를 만들어 두었지만 여기서는 DB에서 꺼내오는 정보만 보겠다.

     

    쿼리문을 보면 위 5개의 데이터를 select하고 있다. 따라서 해당 데이터를 담을 VO에는 반드시 위 5개의 변수는 선언이 되어있어야 한다.

     

    [약간 논외]

     

    당연히 setter가 있어야 DB에서 꺼내온 데이터를 VO에 담을 수 있으니까 필수라고 생각했다.

     

    근데 setter가 없어도 list를 가져오는데 이상이 없었다..(테스트 방식이 잘못된 것일 수 있음)

     

    setter가 필수라고 생각했던 이유는 Mybatis는 어찌됐든 JDBC 이용을 편하게 해주는(?) 프레임워크니까 JDBC를 사용할 때 필요한 코드들을 내부적으로 수행하고 있을거라 짐작했기 때문이다.

     

    Mybatis 없이 프로젝트를 할 때, DB에서 꺼내온 list 데이터는 다음과 같이 하나씩 꺼내서 담아줬다(아래는 예시).

    rs = pstmt.executeQuery();
    list = new ArrayList<EventVO>();
    while(rs.next()) {
        EventVO event = new EventVO();
        event.setE_num(rs.getInt("e_num"));
        event.setE_title(StringUtil.useNoHtml(rs.getString("e_title")));
        event.setE_date(rs.getDate("e_date"));
    
        list.add(event);
    }

    쿼리문을 작성해서 prepareStatement의 파라미터로 넣어주고, 쿼리문을 실행 시킨 결과를 rs에 담아 한 행씩 꺼내면서 list에 할당해주는 방식이다.

    Mybatis가 알아서 DB 테이블의 열이름과 VO에 선언된 프로퍼티명이 같은 것끼리 짝지어 데이터를 어떻게 처리해주는지 자세히 모르겠으나, 아직 의문이 남는다..

     

    뭔가 놓친 것일 수 있으니 아래 링크를 걸어두자..

     

    참고 : https://mybatis.org/mybatis-3/ko/sqlmap-xml.html#Result_Maps

     

    [쿼리문]

    이제 작성한 쿼리문을 보자.

     

    상당히 길어보이는데 서브쿼리(하나의 쿼리문에 또다른 쿼리문이 포함된 쿼리문)가 들어가서 그렇다.

     

    분리해서 보면 크게 감싸고 있는 아래 쿼리문과

    SELECT * FROM (SUBQUERY1) WHERE rnum >= #{start} AND rnum <= #{end}

    SUBQUERY1에 해당하는 아래 구문,

    SELECT a.*, rownum rnum FROM (SUBQUERY2)a

    SUBQUERY2에 해당하는 아래 구문

    SELECT vo에 넣을 데이터 꺼내는 쿼리문

    이렇게 세 부분이다.

     

    rownum을 사용하고 있는데 이는 추출된 데이터에 번호를 매겨주는 역할을 한다.

     

    SUBQUERY2를 통해 추출한 데이터에, a라는 alias를 붙여줬다.

     

    중요한 점은 원하는 데이터 정렬과정이 SUBQUERY2에서 이루어져야한다는 것이다.

     

    rownum은 추출된 데이터에 번호를 붙이거나 원하는 개수만큼 출력할 때 사용하게되는데, 데이터를 뽑은 다음 숫자를 붙여줘야 뽑힌 데이터 순서대로 번호가 매겨진다.

     

    좀 더 자세한 설명이 있는 포스트가 있어 첨부한다.

     

    참고 : https://jhnyang.tistory.com/entry/%EC%9C%88%EB%8F%84%EC%9A%B0

     

    이렇게 원하는 정렬대로 데이터를 추출한 후 rownum을 통해 번호를 부여하면, 정렬 기준이 같을 때 항상 특정 행은 특정 rownum을 갖는다.

     

    예를 들어, DB에 다음과 같은 테이블이 있다고 하자.

     

    물품번호 가격 수량
    3213 1000 3
    4421 1500 2
    2341 2400 6

    이를 수량 순 정렬을 하면 아래와 같을 것이다.

     

    물품번호 가격 수량
    2341 2400 6
    3213 1000 3
    4421 1500 2

    이 상태로 정렬한 후 rownum을 부여하면 아래와 같다.

     

    ROWNUM 물품번호 가격 수량
    1 2341 2400 6
    2 3213 1000 3
    3 4421 1500 2

    즉, 이 테이블에선 원래 데이터가 어떻게 되어 있든 수량 기준에 따라 정렬 후 rownum을 부여하면 물품번호 2341번 물품이 항상 rownum 1번이 된다는 뜻이다.

     

    그러므로 rownum의 범위를 지정해서 꺼내오면 1페이지에 rownum 1번부터 5번, 2페이지에 6번부터 10번, 3페이지에 11번부터 15번과 같이 페이징 처리를 할 수 있게 된다.

     

    그 몇 번부터 몇 번까지를 지정해주는 부분이 가장 외부를 감싸고 있는 쿼리문의 where절이다.

    WHERE rnum >= #{start} AND rnum <= #{end}

    start에 현재 페이지의 첫 글에 해당하는 rownum, end에 현재 페이지의 마지막 글에 해당하는 rownum이 들어가 DB에서 데이터를 꺼내오고 그 데이터들이 list에 담기게 된다.

     

    이후 ModelAndView 객체에 다음과 같이 담아주면 끝이다.

    mav.addObject("count", count); 
    mav.addObject("list", list);
    mav.addObject("page", page.getPage());
    
    mav.setViewName("pointList");
    return mav;

     

    서비스단은 코드만 적고 넘어가도록 하자.

     

    [Service]

    public interface MypageService {
        public List<PayVO> selectPointListByMemNum(Map<String, Object> map);
    }

    [ServiceImpl]

    @Service
    @Transactional
    public class MypageServiceImpl implements MypageService{
        @Autowired
        private MypageMapper mypageMapper;
        
        @Override
        public List<PayVO> selectPointListByMemNum(Map<String, Object> map) {
            return mypageMapper.selectPointListByMemNum(map);
        }
    }

    [Controller]

    @Controller
    public class MypageController {
        @Autowired
        private MypageService mypageService;
    }

     

    이렇게 해서 jsp로부터 값을 전달받은 컨트롤러가 DB에 데이터를 넘겨주고 값에 해당하는 데이터를 꺼내 다시 jsp에 응답하는 과정을 모두 정리했다.

     

     

    다음 글에서는 위 과정 중 설명하지 않고 넘어갔던 PagingUtil 클래스의 내부 코드에 대해 리뷰하도록 하겠다.

Designed by Tistory.