Spring

스프링 - Part 4 - REST 방식과 Ajax를 이용하는 댓글 처리 02 (Ajax 댓글 처리 - 05)

록's 2023. 3. 30. 18:19
728x90
반응형

https://rogi221.tistory.com/161

 

스프링 - Part 4 - REST 방식과 Ajax를 이용하는 댓글 처리 02 (Ajax 댓글 처리 - 04)

https://rogi221.tistory.com/160 스프링 - Part 4 - REST 방식과 Ajax를 이용하는 댓글 처리 02 (Ajax 댓글 처리 - 03) https://rogi221.tistory.com/159 스프링 - Part 4 - REST 방식과 Ajax를 이용하는 댓글 처리 02 (Ajax 댓글 처

rogi221.tistory.com

 


Ajax 댓글 처리 - 05

 

 

댓글의 페이징 처리

현재까지 작성된 예제는 해당 게시물의 전체 댓글을 가져와서 화면에 출력한다.

문제는 댓글의 숫자가 엄청나게 많을 경우이다. 댓글의 숫자가 많다면 데이터베이스에서 많은 양의 데이터를 가져와야하고, 이는 성능상의 문제를 가져올 수 있다. 일반적으로는 이런 문제를 페이징 처리를 이용해서 처리한다.

 

 

데이터베이스의 인덱스 설계

 

댓글에 대해서 우선적으로 고려해야하는 일은 tbl_reply 테이블을 접근할 때 댓글의 번호(rno)가 중심이 아니라 게시물의 번호(bno)가 중심이 된다는 점이다.

댓글을 조회할 때에는 해당 게시물의 댓글을 가져오기 때문에 tbl_reply where bno = 200 order by rno asc와 같은 방식으로 접근한다.

 

tbl_reply 테이블의 PK는 rno 이므로 위와 같은 방식으로 쿼리를 실행하면 테이블을 접근하는 방식은 다음과 같다.

 

bno 값이 100번인 게시물의 댓글을 보면 PK_REPLY를 이용해서 검색하다보니 중간에 다른 게시물 번호들을 건너 뛰어 특정 게시물의 댓글을 찾아야한다. 데이터가 많아진다면 성능에 문제가 생길 수 있다.

 

효율을 높이려면 번호에 맞게 댓글들을 모아서 빠르게 찾을 수 있는 구조로 만드는 것이 좋다.

 

아래 그림을 보면 bno 별로 댓글들을 모아두었으므로 특정 게시물의 댓글을 찾을 때 모여 있는 부분만을 찾을 수 있게 되어있는 것을 볼 수 있다.

위와 같은 구조를 이용하게 되면 bno=200 order by rno asc와 같은 쿼리를 실행 할 때 왼쪽 구조에서 200에 해당하는 범위만 찾아서 사용하게 된다.

이러한 구조를 생성하는 것을 인덱스 생성한다고 표현한다.

 

인덱스를 작성하는 SQL 

create index idx_reply on tbl_reply (bno desc, rno asc);

 

 

 

인덱스를 이용한 페이징 쿼리

 

인덱스를 이용하는 이유 중 하나는 정렬을 피할 수 있기 때문이다. 특정한 게시물의 rno의 순번대로 데이터를 조회하려면 다음과 같은 쿼리를 작성하게 된다.

 

select /*+INDEX(tbl_reply idx_reply) */
	rownum rn, bno, rno, reply, replyer, replyDate, updatedate
    from tbl_reply
    where bno = 3145745 --(게시물 번호)
    and rno > 0

 

SQL의 실행 계획은 다음과 같이 처리된다.

 

실행된 결과를 보면 IDX_REPLY를 이용해서 테이블에 접근하는 것을 볼 수 있다.

ROWNUM은 가장 낮은 rno 값을 가지는 데이터가 1번이 되게 된다.

 

 

 

ROWNUM이 원하는 순서대로 나오기 떄문에 페이징 처리는 이전에 게시물 페이징과 동일한 형태로 작성할 수 있다.

 

예) 10개씩 2페이지를 가져온다면 아래와 같은 쿼리를 작성하게 된다.

select rno, bno, reply, replyer, replydate, updatedate
from 
  (
    select /*+INDEX(tbl_reply idx_reply) */
      rownum rn, bno, rno, reply, replyer, replyDate, updatedate
    from tbl_reply
    where bno = 게시물번호
          and rno > 0
          and rownum <= 20
  ) where rn > 10
;

 

 

 

 

ReplyMapper.xml에 위의 내용을 반영한다면 다음과 같이 정리된다,

 

 

ReplyMapper.xml

// ReplyMapper.xml

... 생략 ...

	<select id="getListWithPaging"
		resultType="org.codehows.domain.ReplyVO">
	<![CDATA[
		select rno, bno, reply, replyer, replyDate, updatedate
		from
			(
			select /*+INDEX(tbl_reply idx_reply) */
				rownum rn, rno, bno, reply, replyer, replyDate, updatedate
		from tbl_reply
		where bno = #{bno}
		and rno > 0
		and rownum <= #{cri.pageNum} * #{cri.amount}
		) where rn > (#{cri.pageNum} -1) * #{cri.amount}
		
	]]>
	</select>
    
... 생략 ...

 

ReplyMapper.xml의 페이징 처리는 아래와 같은 테스트 코드를 통해서 최종적인 결과를 확인한다.

 

 

ReplyMapperTests.java

// ReplyMapperTests.java

... 생략 ...

	
	@Test
	public void testList2() {
		Criteria cri = new Criteria(2, 10);
		List<ReplyVO> replies = mapper.getListWithPaging(cri, 194L);
		
		replies.forEach(reply -> log.info(reply));
	}
}

 

 

 

댓글의 숫자 파악

 

댓글들을 페이징 처리하기 위해서는 해당 게시물의 전체 댓글의 숫자를 파악해서 화면에 보여줄 필요가 있다.

ReplyMapper 인터페이스에는 getCountByBno()를 추가한다.

 

ReplyMapper.java

// ReplyMapper.java

... 생략 ...

	public List<ReplyVO> getListWithPaging(
			@Param("cri") Criteria cri,
			@Param("bno") Long bno);
	
	public int getCountByBno(Long bno);
}

 

ReplyMapper.xml 에서는 id 속성값이 getCountByBno인 <select>를 추가한다.

 

// ReplyMapper.xml

... 생략 ...

	</select>
	
	<select id="getCountByBno" resultType="int">
	<![CDATA[
		select count(rno) from tbl_reply where bno = #{bno}
	]]>
	
	</select>
	
... 생략 ...

 

 

 

ReplyServiceImpl에서 댓글과 댓글 수 처리

 

ReplyService 인터페이스와 구현 클래스인 ReplyServiceImpl 클래스는 List<ReplyVO>와 댓글의 수를 같이 전달할 수 있는 구조로 변경한다.

 

우선 org.codehows.domain 패키지에 두 가지 정보를 담을 수 있는 ReplyPageDTO 클래스를 정의한다.

 

 

ReplyPageDTO 클래스

// ReplyPageDTO

package org.codehows.domain;

import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;

@Data
@AllArgsConstructor
@Getter
public class ReplyPageDTO {
	
	private int replyCnt;
	private List<ReplyVO> list;
}

 

ReplyPageDTO는 객체 생성 시에 편리하도록 @AllArgsConstructor를 이용해서 replyCnt와 list를 생성자의 파라미터로 처리한다.

ReplyService 인터페이스와 ReolyServiceImpl 클래스에는 ReplyPageDTO를 반환하는 메서드를 추가한다.

 

ReplyService 인터페이스

// ReplyService.java

package org.codehows.service;

import java.util.List;

import org.codehows.domain.Criteria;
import org.codehows.domain.ReplyPageDTO;
import org.codehows.domain.ReplyVO;

public interface ReplyService {

	... 생략 ...
	
	public ReplyPageDTO getListPage(Criteria cri, Long bno);
}

 

ReplyServiceImpl 클래스

// ReplyServiceImpl.java

package org.codehows.service;

import java.util.List;

import org.codehows.domain.Criteria;
import org.codehows.domain.ReplyPageDTO;
import org.codehows.domain.ReplyVO;
import org.codehows.mapper.ReplyMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@Service
@Log4j
public class ReplyServiceImpl implements ReplyService {

	@Setter(onMethod_ = @Autowired)
	private ReplyMapper mapper;

... 생략 ...

	@Override
	public ReplyPageDTO getListPage(Criteria cri, Long bno) {
		return new ReplyPageDTO(
				mapper.getCountByBno(bno),
				mapper.getListWithPaging(cri, bno));
	}
}

 

 

 

ReplyController 수정

 

ReplyController에서는 ReplyService 에 새롭게 추가된 getListPage()를 호출하고 데이터를 전송하는 형태로 수정한다.

 

ReplyController.java

getList 부분 수정

// ReplyController.java

... 생략 ...

   
   @GetMapping(value = "/pages/{bno}/{page}",
		   produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE })
   public ResponseEntity<ReplyPageDTO> getList(@PathVariable("page") int page, @PathVariable("bno") Long bno) {
	   Criteria cri = new Criteria(page, 10);
	   log.info("get Reply List bno: " + bno);
	   log.info("cri : " + cri);
	   
	   return new ResponseEntity<>(service.getListPage(cri, bno),HttpStatus.OK);
   }
   
... 생략 ...

 

기존과 동일하게 JSON 데이터를 전송하지만 ReplyPageDTO 객체를 JSON으로 전송하게 되므로, 특정 게시물의 댓글 목록을 조회하려면 아래와 같이 replyCnt와 list라는 이름의 속성을 가지는 JSON 문자열이 전송된다.

 

 

 

 

댓글 페이지의 화면 처리

 

댓글의 화면처리

  • 게시물을 조회하는 페이지에 들어오면 기본적으로 가장 오래된 댓글들을 가져와서 1페이지에 보여준다.
  • 1페이지의 게시물을 가져올 때 해당 게시물의 댓글의 숫자를 파악해서 댓글의 페이지 번호를 출력한다.
  • 댓글이 추가되면 댓글의 숫자만을 가져와서 최종 페이지를 찾아서 이동한다.
  • 댓그르이 수정과 삭제 후에는 다시 동일 페이지를 호출한다.

 

 

 

댓글 페이지 계산과 출력

 

Ajax로 가져오는 데이터가 replyCnt와 list 라는 데이터로 구성되므로 이를 처리하는 reply.js 의 내용 역시 이를 처리하는 구조로 수정한다.

 

 

reply.js // getList 부분 수정

// reply.js

... 생략 ...

   function getList(param, callback, error) {
      var bno = param.bno;
      var page = param.page || 1;
      
      $.getJSON("/replies/pages/" + bno + "/" + page + ".json",
         function(data) {
            if(callback) {
               // callback(data); // 댓글 목록만 가져오는 경우
               callback(data.replyCnt, data.list); // 댓글 숫자와 목록을 가져오는 경우
            }
         }).fail(function(xhr, status, err) {
        if(error) {
           error();
        }
     });
   }
   
... 생략 ...

 

기존에 비해서 변경되는 부분은 callback 함수에 해당 게시물의 댓글 수(replyCnt)와 페이지에 해당하는 댓글 데이터를 전달하도록 하는 부분이다.

 

 

 

reply.js를 이용해서 댓글의 페이지를 호출하는 부분은 showList 함수이므로 페이지 번호를 출력하도록 수정한다

 

get.jsp의 일부

// get.jsp

... 생략 ...

      function showList(page) {
    	  
    	 console.log("show list " + page);
    	  
         replyService.getList({bno:bnoValue, page:page||1}, function(replyCnt, list) {
            
        	 console.log("replyCnt: " + replyCnt);
        	 console.log("list: " + list);
        	 console.log(list);
        	 
        	 if(page == -1){
        		 pageNum = Math.ceil(replyCnt/10.0);
        		 showList(pageNum);
        		 return;
        	 }
        	 
        	 var str = "";
             if(list == null || list.length == 0) {
             	replyUL.html("");  
             	return;
            }
             
            for (var i = 0, len = list.length || 0; i<len; i++) {
		 		str +="<li class='left clearfix' data-rno='"+list[i].rno+"'>";
		 		str +="	 <div><div class='header'><strong class='primary-font'>["+list[i].rno+"] "+list[i].replyer+"</strong>";
		 		str +="  <small class='pull-right text-muted'>"+replyService.displayTime(list[i].replyDate)+"</small></div>";
		 		str +="  <p>"+list[i].reply+"</p></div></li>";
		 	}
            replyUL.html(str);
         });   // end function
      }   // end showList
      
... 생략 ...

showList() 함수는 파라미터로 전달되는 page 변수를 이용해서 원하는 댓글 페이지를 가져오게 된다.

이때 만일 page 번호가 -1로 전달되면 마지막 페이지를 찾아서 다시 호출하게된다.

사용자가 새로운 댓글을 추가하면 showList(-1); 을 호출하여 우선 전체 댓글의 숫자를 파악하게된다.

이 후에 다시 마지막 페이지를 호출해서 이동시키는 방식으로 동작시킨다.

 

이러한 방식은 여러번 서버를 호출해야하는 단점, 댓글의 등록 행위가 댓글 조회나 페이징에 비해서 적기 때문에 심각한 문제는 아니다.

// get.jsp

... 생략 ...

		modalRegisterBtn.on("click", function(e){
			
			var reply= {
					reply: modalInputReply.val(),
					replyer:modalInputReplyer.val(),
					bno:bnoValue
				};
			replyService.add(reply, function(result){
				alert(result);
				
				modal.find("input").val("");
				modal.modal("hide");
				
				// showList(1);
				showList(-1);
			});
		});
        
... 생략 ...

 

댓글은 화면상에서 댓글이 출력되는 영역의 아래쪽에 <div class='panel-footer'>를 하나 추가하고 <div>의 아래쪽에 추가.

 

get.jsp

// get.jsp

... 생략 ...
   
         <!-- /.panel-heading -->
         <div class="panel-body">
            <ul class="chat">
            
                ... 생략 ...
                
            </ul>
            <!-- /. end ul -->
         </div>
         <!-- /.panel .chat-panel -->
         <div class="panel-footer">
         
         </div>
      </div>
   </div>
   <!-- ./ end row -->
</div>

 

추가된 <div class='panel-footer'>에 댓글 페이지 번호를 출력하는 로직은 showReplyPage()는 아래와 같다.

 

// get.jsp

... 생략 ...

         });   // end function
      }   // end showList
      
      var pageNum = 1;
      var replyPageFooter = $(".panel-footer");
      
      function showReplyPage(replyCnt) {
    	  
    	  var endNum = Math.ceil(pageNum / 10.0) * 10;
    	  var startNum = endNum - 9;
    	  
    	  var prev = startNum != 1;
    	  var next = false;
    	  
    	  if(endNum * 10 >= replyCnt) {
    		  endNum = Math.ceil(replyCnt/10.0);
    	  }
    	  if(endNum * 10 < replyCnt){
    		  next = true;
    	  }
    	  var str = "<ul class='pagination pull-right'>";
    	  
    	  if(prev){
    		  str+= "<li class='page-item'><a class='page-link' href='"+(starNum -1)+"'>Previous</a></li>";
    	  }
    	  
    	  for(var i = startNum; i<= endNum; i++){
    		  var active = pageNum == i? "active":"";
    		  str+= "<li class='page-item "+active+" '><a class='page-link' href='"+i+"'>"+i"</a></li>";
    	  }
    	  
    	  if(next){
    		  str+="<li class='page-item'><a class='page-link' href='"+(endNum + 1)+"'>Next</a></li>";
    	  }
    	  str += "</ul></div>";
    	  
    	  console.log(str);
    	  replyPageFooter.html(str);    	  
      }

... 생략 ...

 

showReplyPage( ) 는 기존에 Java로 작성되는 PageMaker의 JavaScript 버전에 해당한다.

댓글 페이지를 문자열로 구성한 후 <div>의 innerHTML로 추가한다.

showList()의 마지막에 페이지를 출력하도록 수정한다.

 

 

// get.jsp

... 생략 ...

            replyUL.html(str);
            
            showReplyPage(replyCnt);
         });   // end function
      }   // end showList
      
... 생략 ...

 

실제로 화면상에는 CSS의 구성으로 인해서 아래쪽에 추가된다.

 

마지막 처리는 페이지의 번호를 클릭했을 때 새로운 댓글을 가져오도록 하는 부분이다.

 

get.jsp

// get.jsp

... 생략 ...

      replyPageFooter.on("click","li a", function(e){
          e.preventDefault();
          console.log("page click");
          
          var targetPageNum = $(this).attr("href");
          
          console.log("targetPageNum: " + targetPageNum);
          
          pageNum = targetPageNum;
          
          showList(pageNum);
        });
        
... 생략 ...

댓글의 페이지 번호는 <a> 태그 내에 존재하므로 이벤트 처리에서는 <a> 태그의 기본동작을 제한하고(preventDefault()) 댓글 페이지 번호를 변경한 후 해당 페이지의 댓글을 가져오도록 한다.

 

 

댓글의 수정과 삭제

 

댓글이 페이지 처리되면 댓글의 수정과 삭제 시에도 현재 댓글이 포함된 페이지로 이동하도록 수정한다.

 

get.jsp

// get.jsp

... 생략 ...

modalModBtn.on("click", function(e){
			var reply = {rno:modal.data("rno"), reply: modalInputReply.val()};
			
			replyService.update(reply, function(result){
				alert(result);
				modal.modal("hide");
				showList(pageNum);
			});
		});
		
		modalRemoveBtn.on("click", function (e) {
			var rno = modal.data("rno");
			replyService.remove(rno, function(result) {
				alert(result);
				modal.modal("hide");
				showList(pageNum);
			});
		}); 
        
... 생략 ...

기존과 달라진 점은 showList() 를 호출 할 때 현재 보고 있는 댓글 페이지의 번호를 호출 한다는 점이다.

브라우저에서 댓글의 등록, 수정, 삭제 작업은 모두 페이지 이동을 하게 된다,

 

728x90
반응형