상세 컨텐츠

본문 제목

Spring 블로그 만들기 - 12. 댓글 리스트

개발/Spring 블로그 만들기

by 똘똘이박사 2019. 3. 13. 06:19

본문



이 포스팅의 샘플 게시판 개발 환경은 MAC OS, STS, OpenJDK11 입니다.


게시물의 댓글쓰기 대한 포스팅은 아래와 같은 순서로 진행합니다.

  1. AJAX

  2. 댓글 저장을 위한 테이블 생성하기

  3. DAO, Service 생성하기

  4. BoardController의 수정

  5. 비동기식 Controller

  6. 댓글 입력 폼 만들기 (boardContent.jsp 수정)


블로그 만들기 - 12. 댓글 쓰기


댓글 기능은 게시판의 본문 작성 기능과 마찬가지로 게시판의 중요 기능중에 한 가지 입니다. 이번 포스팅에서는 게시물에 댓글을 작성하는 기능을 추가해 보려고 합니다. 


AJAX

댓글 작업을 하기 전에 'AJAX'가 무엇인지 개념을 알아야 합니다. 그러기 위해서 우선 '비동기 통신(Async)'과 '동기 통신(Sync)'이 무엇이고 어떤 차이점을 가지고 있는지 살펴보겠습니다.

비동기 통신이란 어떤 요청을 서버로 보냈을때, 서버의 응답을 기다리지 않고, 바로 다음 작업을 하는 통신 방식을 말합니다.

반대로, 동기 통신이란 어떤 요청을 서버로 보냈을때 서버에서 응답이 오기까지 기다렸다가 다음 작업을 진행하는 방식을 말합니다.




간단히 말해 동기통신 방식은 어떤 요청을 서버로 보냈을때 서버가 그 요청을 처리하여 응답을 할때까지 다른 작업을 하지 않고 대기하게 됩니다. 그리고 서버가 응답을 한 후에야 응답에 대한 작업을 처리 하게 됩니다. 비동기통신은 서버에 응답을 기다리지 않고 바로 이후 작업을 처리합니다. 나중에 서버에서 응답이 오면 그때 응답에 대한 처리를 진행하게 됩니다. 


우리가 지금까지 해왔던 작업은 모두 동기식 방식입니다. 게시판 리스트 화면을 예로 들면, 게시판 리스트에 접근 할때 서버에서 리스트를 조회 후 데이터를 model에 담아 보내기 전까지 화면은 멈춰서 응답을 기다리고 있습니다. 그리고 서버에서 데이터를 보내고 나서야 화면에서는 서버가 보내준 데이터를 화면에 보여 줍니다. 따라서 우리가 리스트 화면에 접근하게 되면, 모든 데이터가 출력되는 화면을 보게 됩니다.

이 과정을 비동기식으로 처리 하게 된다면, 서버에서 데이터를 화면으로 보내주기 전에 브라우저는 화면에 리스트를 출력하게 됩니다. 따라서 우리가 화면에 접근 했을때 아주 잠깐 이겠지만 리스트가 비어있는 화면을 보게될 수 있습니다. 게시판 같은 경우는 서버에서 처리해야 할 내용이 그다지 복잡하고 시간이 오래 걸리는 작업이 아니기 때문에 아주 잠깐 비어있는 화면이 나올 수도 있겠지만, 데이터의 양이 방대하고, 처리 시 속도가 많이 걸린다면 그 만큼 비어있는 리스트를 보게 되는 시간은 길어 지게 됩니다. (물론 이런경우 '데이터를 불러오는 중입니다' 와 같은 메시지를 화면에 출력해 컴퓨터가 멈춘게 아니라 처리 중이니 기다려 달라고 사용자에게 알릴 수 있습니다.)

이렇게 보면 비동기 통신 방법이 동기 통신 방법에 비해 나을게 없어 보이지만, 꼭 그렇지는 않습니다.

대표적으로 비동기 방식을 많이 사용하는 부분이라면 검색창의 '자동완성기능'을 들 수 있는데, 데이터를 서버로 보내고 결과를 돌려 받을때 화면 전환이나 리플래쉬 없이 작업이 가능하다는 장점이 있습니다. 

AJAX(asynchronous JavaScript and XML)란 위에서 설명한 비동기 통신 방식을 쉽게 처리할 수 있는 방법중 하나입니다. 우리는 여기서 jQuery 를 사용해 Ajax 통신을 할 예정입니다.



댓글 저장을 위한 테이블 생성하기

가장 먼저 할 일은 앞의 게시판 만들기와 유사하게 댓글을 저장하기 위한 테이블을 생성하는 작업입니다.

아래의 SQL 문을 실행하여 테이블을 생성합니다.


댓글 저장 테이블 생성 SQL 문 (TBL_REPLY)

create table tbl_reply

(

      rid     int auto_increment comment '일련번호'  primary key,

bid     int         not null comment '게시물 일련번호',

content text        null comment '댓글내용',

reg_id  varchar(45) not null comment '작성자',

reg_dt  timestamp   not null comment '작성일',

edit_dt timestamp   not null comment '수정일',

constraint tbl_reply_tbl_board_bid_fk

foreign key (bid) references tbl_board (bid)

);


위 SQL 문에는 두 종류의 ID가 사용되었습니다. 하나는 댓글 고유의 일련번호(RID)이고, 또 하나는 댓글이 어느 게시물의 댓글인지 표시해야 할 게시물의 일련번호(BID) 입니다. 게시물의 일련번호(BID)가 없다면 해당 댓글의 소속을 알 수 없어 데이터를 출력 할 수 없습니다. '게시물의 일련번호(BID)' 는 게시판의 Primary Key와 관련이 있으므로 Foregin Key 로 지정을 합니다.


댓글 Mapper 작성하기

아래 그림과 같이 [src/main/resources > mappers] 에 아래 그림과 같이 replyMapper.xml을 생성합니다. 그리고 댓글의 저장/수정/삭제/조회를 위한 SQL문을 작성합니다.



replyMapper.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper

    PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN"

    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

    

<mapper namespace="com.freehoon.web.board.replyMapper">

<select id="getReplyList" resultType="com.freehoon.web.board.model.ReplyVO">

SELECT

RID

, BID

, CONTENT

, REG_ID

, REG_DT

, EDIT_DT

FROM

TBL_REPLY

WHERE

BID = #{bid}

ORDER BY REG_DT desc

</select>

<insert id="saveReply" parameterType="com.freehoon.web.board.model.ReplyVO">

INSERT INTO TBL_REPLY(BID, CONTENT, REG_ID, REG_DT, EDIT_DT)

VALUES (

#{bid}

, #{content}

, #{reg_id}

, now()

, now()

)

</insert>

<update id="updateReply" parameterType="com.freehoon.web.board.model.ReplyVO">

UPDATE TBL_REPLY SET

CONTENT = #{content}

, EDIT_DT = now()

WHERE

RID = #{rid}

</update>

<delete id="deleteReply" parameterType="int">

DELETE FROM TBL_REPLY

WHERE

RID = #{rid}

</delete>

</mapper>




DAO, Service 수정

boardDAO와 boardService에 댓글 저장/수정/삭제/조회 처리와 관련된 내용들을 아래와 같이 추가 합니다.


boardDAO.java 수정(내용 추가)

// 댓글 리스트

public List<ReplyVO> getReplyList(int bid) throws Exception;

public int saveReply(ReplyVO replyVO) throws Exception;

public int updateReply(ReplyVO replyVO) throws Exception;

public int deleteReply(int rid) throws Exception;


boardDAOImpl.java 수정(내용 추가)

  // 댓글 리스트

@Override

public List<ReplyVO> getReplyList(int bid) throws Exception {

return sqlSession.selectList("com.freehoon.web.board.replyMapper.getReplyList", bid);

}


@Override

public int saveReply(ReplyVO replyVO) throws Exception {

return sqlSession.insert("com.freehoon.web.board.replyMapper.saveReply", replyVO);

}


@Override

public int updateReply(ReplyVO replyVO) throws Exception {

return sqlSession.update("com.freehoon.web.board.replyMapper.updateReply", replyVO);

}


@Override

public int deleteReply(int rid) throws Exception {

return sqlSession.delete("com.freehoon.web.board.replyMapper.deleteReply", rid);

}


boardService.java 수정 (내용 추가)

  // 댓글 리스트

public List<ReplyVO> getReplyList(int bid) throws Exception;

public int saveReply(ReplyVO replyVO) throws Exception;

public int updateReply(ReplyVO replyVO) throws Exception;

public int deleteReply(int rid) throws Exception;


boardServiceImpl.java 수정 (내용 추가)

// 댓글 리스트

@Override

public List<ReplyVO> getReplyList(int bid) throws Exception {

return boardDAO.getReplyList(bid);

}


@Override

public int saveReply(ReplyVO replyVO) throws Exception {

return boardDAO.saveReply(replyVO);

}


@Override

public int updateReply(ReplyVO replyVO) throws Exception {

return boardDAO.updateReply(replyVO);

}


@Override

public int deleteReply(int rid) throws Exception {

return boardDAO.deleteReply(rid);

}



Controller 수정

게시글을 조회 시 댓글을 입력할 수 있는 폼이 생성되어야 합니다. 게시글 입력 폼을 만들때와 마찬가지로 게시글 조회를 할때 댓글 입력 폼에서 사용할 VO를 지정해 보내 주어야 합니다. 따라서 아래와 같이 getBoardContent 메소드를 수정해야 합니다.


BoardController.java 수정 (getBoardcontent 메소드 수정)

@RequestMapping(value = "/getBoardContent", method = RequestMethod.GET)

public String getBoardContent(Model model, @RequestParam("bid") int bid) throws Exception {

model.addAttribute("boardContent", boardService.getBoardContent(bid));

model.addAttribute("replyVO", new ReplyVO());

return "board/boardContent";

}


getBoardContent에는 조회 화면의 댓글 입력 폼에서 사용할 VO를 보내주기 위한 내용을 추가해 주었습니다.



비동기식 Controller

댓글 작업은 Ajax를 이용하여 비동기식 통신 작업으로 처리 하려고 합니다. Spring에서 비동기식 통신을 지원하는데 두 가지 방식이 있습니다. 하나는 스프링 3.x 이하에서만 지원되는 @ResponseBody 어노테이션을 사용하는 방법이고, 다른 하나는 4.x 이상 버전에서 지원하기 시작한 @RestController를 사용하는 방식입니다. 

여기서는 @RestController을 이용한 방식을 사용하지만, @ResponseBody를 사용하는 방식에 대해서도 간략하게 확인하고  넘어 가도록 하겠습니다.


@ResponseBody를 이용한 방식 (스프링 3.x 이하)

@ResponseBody는 기존의 controller을 그대로 이용하여 작업 할 수 있습니다. 즉 한 Controller 안에 동기 통신 방식과 비동기식 통신방식 모두를 구현해 넣을 수 있습니다. 방법은 간단합니다. 비동기식 통신을 할 서비스(메소드) 앞에 @ResponseBody 어노테이션을 붙여 주기만 하면 됩니다.


@ResponseBody 방식 예

@RequestMapping(value = "/getBoardList", method = RequestMethod.GET)

public string getBoardList(Model model) throws Exception {

// 동기식 방법 

// 생략

}


@ResponseBody

@RequestMapping(value = "/getReplyList", method = RequestMethod.POST)

public List<ReplyVO> getReplyList(int bid) throws Exception{

// 비동기식 방법

// 생략

}


위의 샘플 코드에서 보는 것과 같이 지금까지 써오던 Controller 방식에 @ResponseBody 어노테이션만 추가 하였을 뿐입니다.


@RestController를 이용한 방식 (스프링 4.x 이상)

우리가 사용할 방식은 RestController를 이용한 방식입니다.

이 방식은 클래스명 앞에 @Controller 사용하는 대신 @RestController를 사용해야 하기 때문에 새로운 클래스를 만들어야 합니다. 따라서 아래 그림과 같이 Controller 패키지 아래 RestBoardController.java 라는 새로운 파일을 생성하고 아래와 같이 코드를 입력해 봅니다.



RestBoardController.java 신규 추가

package com.freehoon.web.board.controller;


import javax.inject.Inject;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;


import com.freehoon.web.board.model.ReplyVO;

import com.freehoon.web.board.service.BoardService;


@RestController

@RequestMapping(value = "/restBoard")

public class RestBoardController {


private static final Logger logger = LoggerFactory.getLogger(BoardController.class);


@Inject

private BoardService boardService;

@RequestMapping(value = "/getReqlyList", method = RequestMethod.POST)

public List<ReplyVO> getReplyList(@RequestParam("bid") int bid) throws Exception {

return boardService.getReplyList(bid);

}

}




댓글 입력 폼 만들기 (boardContent.jsp 수정)

이제 댓글 입력 폼을 만들어 글을 저장하고 리스트를 출력 할 수 있도록 만들어 볼 예정입니다. 최종적으로 만들어질 화면은 아래와 같습니다.

게시글의 본문 아래 댓글을 입력할 수 있는 영역을 만들고, 입력 폼 아래에는 댓글 리스트가 나오도록 합니다.


boardContext.jsp 수정

// 이전 생략

<div style="margin-top : 20px">

<button type="button" class="btn btn-sm btn-primary" id="btnUpdate">수정</button>

<button type="button" class="btn btn-sm btn-primary" id="btnDelete">삭제</button>

<button type="button" class="btn btn-sm btn-primary" id="btnList">목록</button>

</div>

<!-- Reply Form {s} -->

<div class="my-3 p-3 bg-white rounded shadow-sm" style="padding-top: 10px">

<form:form name="form" id="form" role="form" modelAttribute="replyVO" method="post">

<form:hidden path="bid" id="bid"/>

<div class="row">

<div class="col-sm-10">

<form:textarea path="content" id="content" class="form-control" rows="3" placeholder="댓글을 입력해 주세요"></form:textarea>

</div>

<div class="col-sm-2">

<form:input path="reg_id" class="form-control" id="reg_id" placeholder="댓글 작성자"></form:input>

<button type="button" class="btn btn-sm btn-primary" id="btnReplySave" style="width: 100%; margin-top: 10px"> 저 장 </button>

</div>

</div>

</form:form>

</div>

<!-- Reply Form {e} -->

<!-- Reply List {s}-->

<div class="my-3 p-3 bg-white rounded shadow-sm" style="padding-top: 10px">

<h6 class="border-bottom pb-2 mb-0">Reply list</h6>

<div id="replyList"></div>

</div> 

<!-- Reply List {e}-->

</div>

</article>

  // 이하 생략


기존의 boardContent.jsp 파일을 열어 수정/삭제/목록 버튼이 있는 태그 아래에 위와 같이 붉은색으로 표시한 코드를 입력합니다. 코드에는 <!-- 주석 --> 표시로 댓글 입력 폼과 댓글 리스트의 시작 및 끝 영역을 표시하여 코드를 쉽게 구분 할 수 있도록 하였습니다.

입력폼은 게시판 글쓰기와 비교해 크게 다른 부분이 없습니다. 단지 댓글을 넘기기 위해 modelAttribute를 'replyVO'로 설정이 바뀌어 있습니다.

댓글 리스트 부분은 게시판의 리스트 출력 부분과는 상당히 다릅니다. 위 소스 어디에도 리스트를 출력 하라고 명령을 내리는 부분이 없기 때문입니다. 다만 <div id="replyList"></div> 부분이 리스트가 나올 부분 이라고만 짐작 할 수 있습니다. 댓글 리스트의 경우는 입력/수정/삭제/조회가 이벤트가 발생 할 때 바로바로 변경된 내용을 갱신해 주기위해 스크립트로 처리 하였습니다. 그리고 입력/수정/삭제/조회 처리를 하기 위해 앞에서 설명한 AJAX를 사용할 계획 입니다.

AJAX는 jQuery를 사용하여 쉽게 구현 할 수 있습니다.

아래 코드는 AJAX를 이용해 댓글 리스트를 조회해 오는 부분 입니다.


boardContent.jsp 수정

<script>

// 이전 코드 생략

function showReplyList(){

var url = "${pageContext.request.contextPath}/restBoard/getReqlyList";

var paramData = {"bid" : "${boardContent.bid}"};

$.ajax({

            type: 'POST',

            url: url,

            data: paramData,

            dataType: 'json',

            success: function(result) {

                var htmls = "";

if(result.length < 1){

htmls.push("등록된 댓글이 없습니다.");

} else {

                    $(result).each(function(){

                     htmls += '<div class="media text-muted pt-3" id="rid' + this.rid + '">';

                     htmls += '<svg class="bd-placeholder-img mr-2 rounded" width="32" height="32" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false" role="img" aria-label="Placeholder:32x32">';

                     htmls += '<title>Placeholder</title>';

                     htmls += '<rect width="100%" height="100%" fill="#007bff"></rect>';

                     htmls += '<text x="50%" fill="#007bff" dy=".3em">32x32</text>';

                     htmls += '</svg>';

                     htmls += '<p class="media-body pb-3 mb-0 small lh-125 border-bottom horder-gray">';

                     htmls += '<span class="d-block">';

                     htmls += '<strong class="text-gray-dark">' + this.reg_id + '</strong>';

                     htmls += '<span style="padding-left: 7px; font-size: 9pt">';

                     htmls += '<a href="javascript:void(0)" onclick="fn_editReply(' + this.rid + ', \'' + this.reg_id + '\', \'' + this.content + '\' )" style="padding-right:5px">수정</a>';

                     htmls += '<a href="javascript:void(0)" onclick="fn_deleteReply(' + this.rid + ')" >삭제</a>';

                     htmls += '</span>';

                     htmls += '</span>';

                     htmls += this.content;

                     htmls += '</p>';

                     htmls += '</div>';


                }); //each end


}

$("#replyList").html(htmls);

                

            }    // Ajax success end

}); // Ajax end

}

</script>


위 자바스크립트 함수는 댓글 리스트를 조회해 오는 함수입니다. 댓글을 입력하거나, 수정, 삭제 하는 경우 위 함수를 호출하여 댓글 리스트를 계속 새롭게 불러올 것입니다. 

코드에 대해 간략히 설명 드리면 jQuery를 통한 AJAX 통신 방식은 다음과 같은 구조를 가지고 있습니다.


$.ajax({

url : '서비스 주소'

, data : '서비스 처리에 필요한 인자값'

, type : 'HTTP방식' (POST/GET 등)

, dataType : 'return 받을 데이터 타입' (json, text 등)

, success : function('결과값'){

// 서비스 성공 시 처리 할 내용

}, error : function('결과값'){

// 서비스 실패 시 처리 할 내용

}

});


더 자세한 내용은 다음 포스팅일 참조해 주세요.

샘플 게시판의 코드 에서는 url값과 인자값을 모두 변수로 만들어 처리 하였습니다.


var url = "${pageContext.request.contextPath}/restBoard/getReqlyList";

var paramData = {"bid" : "${boardContent.bid}"};


처리 후 데이터는 JSON 방식으로 받을 예정이고, HTTP 통신 방식은 POST로 할 것입니다.

데이터 처리가 정상적으로 이루어 진다면 success 부분을 처리 하게 되는데, 여기서 실제로 댓글의 리스트를 출력하는 코드를 가지고 있습니다. 

success 부분을 조금 더 상세히 살펴 보도록 하겠습니다.

우선 htmls 라는 문자열 변수를 하나 생성합니다. 그리고 처리 결과 데이터가 있다면 반복문을 통해 댓글의 수 만큼 댓글 리스트를 만들어 주는 html 문장을 만들도록 하고 있습니다.


success: function(result) {

var htmls = "";

if(result.length < 1){

htmls = "등록된 댓글이 없습니다.";

} else {

$(result).each(function(){

htmls += '<div class="media text-muted pt-3" id="rid' + this.rid + '">';

htmls += '<svg class="bd-placeholder-img mr-2 rounded" width="32" height="32" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid slice" focusable="false" role="img" aria-label="Placeholder:32x32">';

htmls += '<title>Placeholder</title>';

htmls += '<rect width="100%" height="100%" fill="#007bff"></rect>';

htmls += '<text x="50%" fill="#007bff" dy=".3em">32x32</text>';

htmls += '</svg>';

htmls += '<p class="media-body pb-3 mb-0 small lh-125 border-bottom horder-gray">';

htmls += '<span class="d-block">';

htmls += '<strong class="text-gray-dark">' + this.reg_id + '</strong>';

htmls += '<span style="padding-left: 7px; font-size: 9pt">';

htmls += '<a href="javascript:void(0)" onclick="fn_editReply(' + this.rid + ', \'' + this.reg_id + '\', \'' + this.content + '\' )" style="padding-right:5px">수정</a>';

htmls += '<a href="javascript:void(0)" onclick="fn_deleteReply(' + this.rid + ')" >삭제</a>';

htmls += '</span>';

htmls += '</span>';

htmls += this.content;

htmls += '</p>';

htmls += '</div>';

}); //each end

}

$("#replyList").html(htmls);

} // Ajax success end


htmls 변수에 저장된 html 코드는 가장 아래 있는 $("#replyList").html(htmls) 구문을 통해 댓글 리스트 영역에 출력 됩니다. 

$("선택자").html("html 코드") 구문은 선택자에 있는 html 구문을 "html 코드" 로 완전히 대체하게 합니다. 따라서 댓글을 입력하거나 수정, 삭제하여 새로운 댓글 리스트를 조회해 오면 새롭게 댓글 리스트에 대한 코드가 작성되게 되고 이 부분을 통해 기존 내용은 삭제되고 새로 읽어온 내용은 대체하게 됩니다.


하지만 위 코드 만으로는 게시글을 열었을때 바로 댓글을 불러올 수 없습니다. 위 showReplyList() 함수는 이벤트가 일어날 때 호출이 되는 함수이기 때문입니다. 따라서 게시글 읽기 페이지가 열리면 자동으로 showReplyList() 함수를 호출 할 수 있도록 이벤트를 만들어 줘야 합니다. 자바스크립트의 상단에 아래와 같은 코드를 작성합니다.


<script>

$(document).ready(function(){

showReplyList();

});

// 이하 코드 생략

</script>


위의 코드에서 사용한 $(document).ready(function(){...}); 부분은 페이지가 완전히 로딩되면 호출되는 이벤트 부분 입니다. 따라서 게시판 조회 페이지가 로딩이 되고 나면 이 부분이 실행되어 showReplyList() 함수를 실행하게 됩니다.






 





※ 포스팅에 오타나 잘못된 부분, 추가적으로 더 알고 싶은 부분이 있으면 댓글 주세요~


반응형

관련글 더보기