Byeon's NOTE

스프링 부트 Spring Boot(4) - 간단한 게시판 만들기, 게시글 추가 및 조회 기능 본문

스프링(Spring)

스프링 부트 Spring Boot(4) - 간단한 게시판 만들기, 게시글 추가 및 조회 기능

SUByeon 2019. 10. 24. 20:10

MySQL과 MyBatis의 연동을 테스트 해보았고 이제는 실제로 사용해 보자.

 

게시판을 만들기 위해 게시판 테이블을 하나 만들자.

CREATE TABLE t_board (
  _id INT NOT NULL AUTO_INCREMENT COMMENT '글 번호',
  title VARCHAR(300) NOT NULL COMMENT '제목',
  contents TEXT NOT NULL COMMENT '내용',
  hit_cnt SMALLINT NOT NULL DEFAULT '0' COMMENT '조회수',
  created_At DATETIME NOT NULL COMMENT '작성시간',
  creator_id VARCHAR(50) NOT NULL COMMENT '작성자',
  updated_At DATETIME DEFAULT NULL COMMENT '수정시간',
  updater_id VARCHAR(50) DEFAULT NULL COMMENT '수정자',
  is_deleted CHAR(1) NOT NULL DEFAULT 'N' COMMENT '삭제 여부',
  PRIMARY KEY (_id)
);

 

이제 DTO(Model)을 만들어 보자. 위의 테이블의 애트리뷰트들을 포함하여 만들자. 여기서 패키지는 dto로 한다.

패키지를 만들고 BoardDto.java 파일을 만들고 아래의 코드를 작성한다.

 

package com.example.board.dto;

public class BoardDto {
	private int board_id;
	private String title;
	private String contents;
	private int hit_cnt;
	private String created_At;
	private String creator_id;
	private String updated_At;
	private String updater_id;
}

우리가 보통 DTO를 생성하면 해당 클래스에는 데이트에 대한 getter/setter가 반드시 존재해야 한다. 이클립스와 같은 IDE에는 이것을 하나하나 직접 작성하지 않고 한번에 추가해주는 기능이 있다. 

우클릭 (또는 상단 메뉴바) -> Source -> Generate Getters and Setters

 

 

하지만 이 기능초자 귀찮다는 개발자들의 게으른 습성... 그래서 lombok(롬북)이라는 라이브러리가 나왔다. 롬북은 getter/setter, equals, toString, hashCode와 같은 메서드를 어노테이션을 붙이는것만으로 자동으로 생성해준다. 사용하셔도 되고 안하셔도 된다.

 

롬북을 사용하기 위해서  프로젝트를 생성할 때 의존성을 추가해 주었다. 하지만 의존성을 추가해준것만으로 롬북이 동작하지 않는다. 따로 설치해 줄 필요가 있다. 이클립의스 마켓 플레이스에서 설치할 수 없고 직접 다운로드 받아야 한다.

롬북홈페이지에서 다운로드 받는다. 다운로드 받았으면 더블클릭하여 실행시킨다.

 

 

Specify location ... 을 클릭하여 사용하고 있는 STS 또는 이클립스를 찾아 선택하고 install/Update를 한다.

 

성공화면이 나오면 Quit Installer를 눌러 종료한다.

 

 

이제 DTO를 수정한다. getter/setter를 제거하고 해당 DTO 클래스에 @Data 어노테이션을 추가해준다. @Data 어노테이션을 추가하면 자동으로 getter/setter, toString, equals, hashCode 를 생성해 준다. 단, final이 선언되지 않은 필드에만 적용된다.

package com.example.board.dto;

import lombok.Data;

@Data
public class BoardDto {
	private int boardId;
	private String title;
	private String contents;
	private int hitCnt;
	private String createdAt;
	private String creatorId;
	private String updatedAt;
	private String updaterId;	
	
}

 

자바는 카멜표기법, 데이터베이스에서는 스네이크 표기법을 사용했다. 이 서로 다른 표기법을 맵핑하기 위한 설정을 해주어야 한다. application.properties에 아래의 코드를 추가해준다.

mybatis.configuration.map-underscore-to-camel-case=true

DatabaseConfig.java 파일도 수정하자. mybatisConfig Bean을 추가해주고 sqlSessionFactory를 수정한다.

 

@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
	SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
	sqlSessionFactoryBean.setDataSource(dataSource);
    sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:/mapper/**/*.xml"));
	sqlSessionFactoryBean.setConfiguration(mybatisConfig()); //추가
	return sqlSessionFactoryBean.getObject();
}
    
@Bean
@ConfigurationProperties(prefix="mybatis.configuration")
public org.apache.ibatis.session.Configuration mybatisConfig() {
	return new org.apache.ibatis.session.Configuration();
}

 

이제 데이터베이스 설정을 끝났다. Controller와 Service 그리고 Repository(DAO), Mapper설정을 해보자. 먼저 아래와 같이 패키지와 클래스들을 생성해준다. service의 경우 BoardService, BoardMapper는 인터페이스, 나머지는 클래스이다.

 

 

이제 Controller를 구현하자. selectBoardList() 부분에서 에러가 발생하지만 아직 메서드를 만들지 않아서 그런거니 괜찮다. 넘거간다.

@Controller
public class BoardController {

  @Autowired
  BoardService boardService;

  @RequestMapping("/board/boardList")
  public ModelAndView openBoardList() {
    ModelAndView mv = new ModelAndView("/board/boardList");   //view를 설정해준다.
    List<BoardDto> list = boardService.selectBoardList();     //service를 이용하여 게시판 목록을 데이터베이스에서 조회한다.
    mv.addObject("list",list);                                //설정한 뷰로 넘겨줄 데이터를 추가

    return mv;
  }

}

 

이제 service를 구현하자. BoardService 인터페이스와 BoardServiceImpl 클래스를 구현하면 된다. 이렇게 두 가지로 나눈 이유는 분리함으로써 느슨한 결함을 유지하여 각 기능간 의존관계를 최소화하고, 이로 인해 기느의 변화에도 최소한의 수정으로 개발할 수 있는 유연함을 가질 수 있다. 또한, 모듈화를 통해 어디서든 사용할 수 있도록 하여 재사용성을 높이고 스프링의 IoC/DI 기능을 이용한 빈 관리 기능을 사용할 수 있기 때문이다. 다시 이제 코드를 작성해보자.

// sevice 인터페이스

public interface BoardService {

	List<BoardDto> selectBoardList() throws Exception;

}
// service 클래스
@Service
public class BoardServiceImpl implements BoardService{
	
	@Autowired
	BoardMapper boardMapper;

	@Override
	public List<BoardDto> selectBoardList() throws Exception {
		
		return boardMapper.selectBoardList();
	}

}

 

이제 남은 BoardMapper를 구현하면 된다. 마이바티스는 데이터 접근 객체인 DAO를 만드는 것보다 SqlSessionDaoSupport나 SqlSessionTemplate를 사용하기를 권장한다. 이렇게 함으로써 마이바티스 스프링 연동 모듈은 다른 빈에 직접 주입할 수 있는 Mapper를 생성할 수 있다. Mapper를 사용하면 일일이 DAO를 만들지 않고 인터페이스만을 이용해서 좀 더 편하게 개발할 수 있다.

@Mapper  //Mapper 인터페이스 선언
public interface BoardMapper {

	List<BoardDto> selectBoardList() throws Exception;
}

 

이제 src/main/resources 아래 생성한 mapper 폴더에 boardMapper.xml 파일을 하나 만들어 준다. 파일 이름은 설정에서 *Mapper.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.example.board.mapper.BoardMapper">   <!-- mapper의 네임스페이스를 지정 인터페이스의 경로 -->
	<!-- 쿼리 id 해당 (id를 이용하여 질의를 찾아 수행함), 실행 결과를 어떤 형식으로 반환할 것인지 -->
    <select id="selectBoardList" resultType="com.exam.board.dto.BoardDto"> 
		<![CDATA[
				SELECT board_id, title, hit_cnt, DATE_FORMAT(created_At, '%Y.%m.%d %H:%i:%s') as created_At
				FROM t_board
				WHERE is_deleted='N'
				ORDER BY board_id DESC
		]]>
	</select>
</mapper>

	

 

이제 마지막으로 뷰를 만들자. Controller에서 view경로와 이름을 /board/boardList 로 설정하였다. Thymeleaf를 이용할 것이기 때문에 template아래 board폴더를 만들고 boardList.html 파일을 생성해준다.

 

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
  <div class="container">
  <h2>게시판 목록</h2>
    <table class="board_list">
      <colgroup>
        <col width="15%"/>
        <col width="*"/>
        <col width="15%"/>
        <col width="20%"/>
      </colgroup>
      <thead>
        <tr>
          <th scope="col">글번호</th>
          <th scope="col">제목</th>
          <th scope="col">조회수</th>
          <th scope="col">작성일</th>
        </tr>
      </thead>
      <tbody>
        <tr th:if="${#lists.size(list)} > 0" th:each="list:${list}">
          <td th:text="${list.boardId}"></td>
          <td class="totle" th:text="${list.title}"></td>
          <td th:text="${list.hitCnt}"></td>
          <td th:text="${list.createdAt}"></td>
        </tr>
        <tr th:unless="${#lists.size(list)} > 0">
          <td colspan="4">조회된 결과가 없습니다.</td>
        </tr>
      </tbody>
    </table>
    <a href="openBoardWrite" class="btn">글 쓰기</a>
  </div>
</body>
</html>

 

http://localhost:8080/board/boardList로 접속해보면 아래와 같은 화면이 나온다. 하지만 이쁘지가 않다. 조금 꾸며보자.

 

static 폴더 아래 css폴더를 생성하고 board.css파일을 하나 생성한 후 아래의 코드를 작성해준다. 그리고 boardList.html 파일에 해당 css파일을 적용시킨다. 꾸미는건 자유. 아무렇게나..

 

@CHARSET "UTF-8";

@import url(http://fonts.googleapis.com/earlyaccess/nanumgothic.css);
@import url(http://cdn.jsdelivr.net/font-nanum/1.0/nanumbarungothic/nanumbarungothic.css);

html{overflow:scorll;}
html, body, div, h1, h2, a, form, table, caption, thead, tbody, tr, th, td, submit {
	margin:0; outline:0; border:0; padding:0; font-size:100%; vertical-align:baseline; background:transparent;
}
body { 
	font-size:0.875em; line-height:1.5; color:#666; -webkit-text-size-adjust:none; min-width:320px;
	font-family:'NanumGothic','나눔고딕',dotum, "Helvetica Neue", Helvetica, Verdana, Arial, Sans-Serief;
}
h1, h2, h3 {font-size: 1.5em;}
p{margin:0; padding:0;}
ul{margin:0;}
a:link, a:visited {text-decoration:none; color: #656565;}
input{vertical-align:middle;}
input:focus {outline:0;}
caption {display:none; width:0; height:0; margin-top:-1px; overflow:hidden; visibility:hidden; font-size:0; line-height:0;}

.container {max-width:1024px; margin:30px auto;}
.board_list {width:100%; border-top:2px solid #252525; border-bottom:1px solid #ccc; margin:15px 0; border-collapse: collapse;}
.board_list thead th:first-child {background-image:none;}
.board_list thead th {border-bottom:1px solid #ccc; padding:13px 0; color:#3b3a3a; text-align: center; vertical-align:middle;}
.board_list tbody td {border-top:1px solid #ccc; padding:13px 0; text-align:center; vertical-align:middle;}
.board_list tbody tr:first-child td {border:none;}
.board_list tbody tr:hover{background:#ffff99;} 
.board_list tbody td.title {text-align:left; padding-left:20px;}
.board_list tbody td a {display:inline-block}

.board_detail {width:100%; border-top:2px solid #252525; border-bottom:1px solid #ccc; border-collapse:collapse;}
.board_detail tbody input {width:100%;}
.board_detail tbody th {text-align:left; background:#f7f7f7; color:#3b3a3a; vertical-align:middle; text-align: center;}
.board_detail tbody th, .board_detail tbody td {padding:10px 15px; border-bottom:1px solid #ccc;}
.board_detail tbody textarea {width:100%; min-height:170px}

.btn {margin:5px; padding:5px 11px; color:#fff !important; display:inline-block; background-color:#7D7F82; vertical-align:middle; border-radius:0 !important; cursor:pointer; border:none;}
.btn:hover {background: #6b9ab8;}

.file_list a {display:inherit !important;}
<link rel="stylesheet" th:href="@{/css/board.css}"/>

 

조금 깔끔해졌다.

 

이제 게시글을 추가하는 작업을 통해 제대로 데이터베이스에 저장되고 목록에 제대로 나오는지 확인해 본다. 그냥 INSERT문을 이용해 추가할 수도 있지만 게시글을 작성하는 HTML파일을 하나 만들어 추가해 본다. boardWrite.html을 생성해주고 코드를 작성한다.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" th:href="@{/css/board.css}"/>
<title>Insert title here</title>
</head>
<body>
  <div class="container">
    <h2>게시판 등록</h2>
    <form id="frm" name="frm" method="post" action="/board/insertBoard">
      <table class="board_detail">
        <tr>
          <td>제목</td>
          <td><input type="text" id="title" name="title"/></td>
        </tr>
        <tr>
          <td colspan="2">
            <textarea id="contents" name="contents"></textarea>
          </td>
        </tr>
      </table>
      <input type="submit" id="submit" value="저장" class="btn"/>
    </form>
  </div>
</body>
</html>

 

이제 게시글 작성 페이지로 이동하기 위한 코드와 저장버튼을 눌렀을 때 처리하는 코드를 Controller에 작성한다.

 

@RequestMapping("/board/openBoardWrite")
  public String openBoardWrite() {
  return "/borad/boardWrite";
}

@RequestMapping("/board/insertBoard")
  public String insertBoard(BoardDto board) throws Exception {
  boardService.insertBoard(board);
  return "redirect:/board/boardList";
}

 

게시글 작성 요청을 처리하는 Service, Mapper 도 구현해준다.

// sevice

public interface BoardService {
  List<BoardDto> selectBoardList() throws Exception;
  void insertBoard(BoardDto board) throws Exception;
}

// BoardServiceImpl.java에 추가

@Override
public void insertBoard(BoardDto board) throws Exception {
  boardMapper.insertBoard(board);
}

 

// BoardMapper Interface

public interface BoardMapper {
  List<BoardDto> selectBoardList() throws Exception;
  void insertBoard(BoardDto board) throws Exception;
}

// boardMapper.xml에 추가
<insert id="insertBoard" parameterType="com.example.board.dto.BoardDto">
  <![CDATA[
    INSERT INTO t_board(
      title,
      contents,
      created_At,
      creator_id
    ) VALUES (
      #{title},
      #{contents},
      NOW(),
      'admin'
    )
  ]]>
</insert>

이제 제대로 동작하는지 확인해 보자. http://localhost:8080/board/boardList에 접속해서 글 쓰기 버튼을 눌러 게시글을 작성해 보자.

 

 

제대로 데이터베이스에 저장된 것을 확인할 수 있다.

이제 게시글 상세보기를 구현해보자.

보고싶은 게시글을 선택했을때 상세보기 페이지로 넘어가야하기 때문에 boardList.html파일을 조금 수정하자.

 

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" th:href="@{/css/board.css}"/>
<title>Insert title here</title>
</head>
<body>
  <div class="container">
  <h2>게시판 목록</h2>
    <table class="board_list">
      <colgroup>
        <col width="15%"/>
        <col width="*"/>
        <col width="15%"/>
        <col width="20%"/>
      </colgroup>
      <thead>
        <tr>
          <th scope="col">글번호</th>
          <th scope="col">제목</th>
          <th scope="col">조회수</th>
          <th scope="col">작성일</th>
        </tr>
      </thead>
      <tbody>
        <tr th:if="${#lists.size(list)} > 0" th:each="list:${list}">
          <td th:text="${list.boardId}"></td>
          <!--   바뀐부분   -->
          <td class="title">
            <a href="openBoardDetail?board_id=" th:attrappend="href=${list.boardId}" th:text="${list.title}"></a>
          </td>
          <!--   바뀐부분   -->
          <td th:text="${list.hitCnt}"></td>
          <td th:text="${list.createdAt}"></td>
        </tr>
        <tr th:unless="${#lists.size(list)} > 0">
          <td colspan="4">조회된 결과가 없습니다.</td>
        </tr>
      </tbody>
    </table>
    <a href="openBoardWrite" class="btn">글 쓰기</a>
  </div>
</body>
</html>

boardDetail.html 을 생성하고 작성

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" th:href="@{/css/board.css}"/>
<title>Insert title here</title>
</head>
<body>
  <div>
    <h2>게시글 상세 화면</h2>
    <form id="frm" method="post">
      <table>
        <colgroup>
          <col width="15%">
          <col width="35%">
          <col width="15%">
          <col width="35%">
        </colgroup>
        <caption>게시글 상세 내용</caption>
        <tbody>
          <tr>
            <th scope="row">글 번호</th>
            <td th:text="${board.boardId}"></td>
            <th scope="row">조회수</th>
            <td th:text="${board.hitCnt}"></td>
          </tr>
          <tr>
            <th scope="row">작성자</th>
            <td th:text="${board.creatorId}"></td>
            <th scope="row">작성일</th>
            <td th:text="${board.createdAt}"></td>
          </tr>
          <tr>
            <th scope="row">제목</th>
            <td colspan="3"><input type="text" id="title" name="title" th:text="${board.title}" /></td>
          </tr>
          <tr>
            <td colspan="4" class="view_text">
              <textarea title="내용" id="contents" name="contents" th:text="${board.contents}"></textarea>
            </td>
          </tr>
        </tbody>
      </table>
      <input type="hidden" id="boardId" th:value="${board.boardId}" />
    </form>

    <a href="#this" class="btn" id="list">목록으로</a>
    <a href="#this" class="btn" id="edit">수정</a>
    <a href="#this" class="btn" id="delete">삭제</a>
  </div>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
  <script>
    $(document).ready(function(){
      $('#list').on("click",function(){
        location.href="/board/openBoardList";
      });

      $('#edit').on("click",function(){
        var frm = $('#frm')[0];
        frm.action = "/board/updateBoard";
        frm.submit();

      });

      $('#delete').on("click",function(){
        var frm = $('#frm')[0];
        frm.action = "/board/deleteBoard";
        frm.submit();
      });
    });
  </script>
</body>
</html>

 

Controller, Service, Mapper도 기능을 구현하자.

 

// Controller

@RequestMapping("/board/openBoardDetail")
public ModelAndView openBoardDetail(@RequestParam int board_id) throws Exception{
  ModelAndView mv = new ModelAndView("/board/boardDetail");
  BoardDto board = boardService.selectBoardDetail(board_id);
  mv.addObject("board",board);
  return mv;
}
// Service Interface

public interface BoardService {
  List<BoardDto> selectBoardList() throws Exception;
  void insertBoard(BoardDto board) throws Exception;
  BoardDto selectBoardDetail(int board_id) throws Exception;
}


// ServiceImpl
@Override
public BoardDto selectBoardDetail(int board_id) throws Exception {
  boardMapper.updateHitCount(board_id);             // 조회수 증가시키기
  return boardMapper.selectBoardDetail(board_id);
}
// Mapper

public interface BoardMapper {
  List<BoardDto> selectBoardList() throws Exception;
  void insertBoard(BoardDto board) throws Exception;
  void updateHitCount(int board_id) throws Exception;
  BoardDto selectBoardDetail(int board_id) throws Exception;
}
// Mapper XML

<update id="updateHitCount" parameterType="int">
  <![CDATA[
    UPDATE t_board
    SET hit_cnt = hit_cnt+1
    WHERE board_id = #{board_id}
  ]]>
</update>
    
<select id="selectBoardDetail" parameterType="int" resultType="com.example.board.dto.BoardDto">
  <![CDATA[
    SELECT board_id, title, contents, hit_cnt, DATE_FORMAT(created_At, '%Y.%m.%d %H:%i:%s') as created_At, creator_id
    FROM t_board
    WHERE board_id=#{board_id} AND is_deleted='N'
  ]]>
</select>

 

이제 다시 실행시키고 게시글 제목을 클릭해보자. 

제대로 동작하여 정보를 가져오고 조회수가 1증가한 것을 볼 수 있다. 다음 포스팅에서 수정, 삭제 기능을 구현해보려 한다.

Comments