본문 바로가기
Spring

[Spring] Spring Framework MVC 커뮤니티 사이트 Tutorial - 2

by BENGGRI 2021. 1. 21.
반응형

[Spring] Spring Framework MVC 커뮤니티 사이트 Tutorial - 1


1. 게시판 등록 기능 구현

이전 [Spring] Spring Framework MVC 게시판 Tutorial - 1 에서 프로그램을 어떻게 만들지 설계를 했다면 이번에는 실제로 코딩을 하는 튜토리얼입니다.

 

1. 프로젝트 세팅

※ 먼저 프로젝트 세팅을 하셔야합니다.

[Spring] Spring Framework MVC MySQL & Mybatis 연동


2. DB 생성

게시글 등록을 위한 데이터베이스를 먼저 생각해봐야합니다.

일반적인 커뮤니티 사이트에서 게시글을 등록할 때 순서를 기억해보겠습니다.

 

1. 필요한 정보 식별

아래 그림처럼 1. 좌측 메뉴를 하나 클릭하면 2. 오른쪽 영역에 클릭한 메뉴에 등록된 게시글 목록이 보입니다.

그리고 난 뒤에 글쓰기 버튼을 누릅니다.

그리고 글쓰기 등록화면이 표시되고 내용을 작성한 후 1. 등록 버튼을 누르면 등록이 됩니다.

게시글 등록에 필요한 정보를 식별해보는 과정을 거칩니다.

작성한 게시글은 어떤 메뉴에서 작성된 것인가?(게시글 메뉴 정보)
게시글은 누가 작성한 것인가?(사용자 정보)
게시글은 언제 작성되었는가?
게시글은 언제 수정되었는가?
게시글은 누가 수정하였는가?
게시글 조회수, 추천수, 비추천수 는 어떻게 처리할 수 있는가?
게시글 제목과 게시글 내용은 꼭 필요하다.
... 등

식별한 정보를 표로 만들어 봅니다.

식별 정보 필요한 정보 비고
작성한 게시글은 어떤 메뉴에서 작성된 것인가? 게시글 메뉴 정보 게시글 메뉴 정보를 어느 수준까지 알아야하는가?
게시글은 누가 작성한 것인가? 게시글 등록 사용자 정보 사용자 정보를 어느 수준까지 알아야하는가?
게시글은 언제 작성되었는가? 게시글 등록일 게시글 등록 시점
게시글은 언제 수정되었는가? 게시글 수정일 게시글 수정 시점
게시글은 누가 수정하였는가? 게시글 수정 사용자 정보 게시글을 누가 수정할 수 있는가?
1. 작성자 본인만
2. 작성자와 관리자
게시글 조회수 게시글 조회수 1. 게시글을 등록하면 기본적으로 0 이어야 한다. 
2. 게시글 상세보기를 할 경우 +1 이 되어야하는데 작성자가 상세보기를 할 경우에도 +1 이 되어야 할까?
게시글 추천수 게시글 추천수 1. 게시글을 등록하면 기본적으로 0 이어야 한다. 
2. 추천 버튼을 클릭할 경우 +1 이 되어야하는데 게시글 작성자가 추천 버튼을 클릭할 경우에도 +1 이 되어야할까?
게시글 비추천수 게시글 비추천수 1. 게시글을 등록하면 기본적으로 0 이어야 한다. 
2. 비추천 버튼을 클릭할 경우 +1 이 되어야하는데 게시글 작성자가 비추천 버튼을 클릭할 경우에도 +1 이 되어야할까?
게시글 제목 게시글 제목 필수 정보
게시글 내용 게시글 내용 필수 정보

2. 식별한 정보 분리

작성한 표에서 데이터베이스에서 해야하는 일과 로직에서 해야하는 일을 분리합니다.

먼저 데이터베이스에서 해야하는 일입니다.

아래 정보를 등록할 수 있는 준비가 되어야 합니다.

필요한 정보 데이터 타입 비고
게시글 식별 정보 문자열 많은 사용자들이 작성한 게시글을 식별할 수 있는 정보
게시글 메뉴 정보 문자열 메뉴를 식별할 수 있는 정보
게시글 등록 사용자 정보 문자열 사용자를 식별할 수 있는 정보
게시글 등록일 날짜 게시글 등록 시점
게시글 수정일 날짜 게시글 수정 시점
게시글 수정 사용자 정보 문자열 사용자를 식별할 수 있는 정보
게시글 조회수 숫자 기본값 0
게시글 추천수 숫자 기본값 0
게시글 비추천수 숫자 기본값 0
게시글 제목 문자열 필수 정보
게시글 내용 문자열(아주 긴 문자열) 필수 정보

로직에서 해야하는 일입니다.

필요한 정보 로직 비고
게시글 메뉴 정보 선택한 메뉴 정보 화면에서 전달 가능
게시글 등록 사용자 정보 게시글 등록을 위해 로그인한 사용자 정보 세션에서 전달 가능
게시글 등록일 없음  
게시글 수정일 없음  
게시글 수정 사용자 정보 게시글 수정을 위해 로그인한 사용자 정보 세션에서 전달 가능
게시글 조회수 게시글 상세보기를 클릭한 경우 +1 을 위한 UPDATE 실행 게시글 등록 사용자는 +1 하지 않음
게시글 추천수 추천 버튼을 클릭한 경우 +1 을 위한 UPDATE 실행 게시글 등록 사용자는 +1 하지 않음
게시글 비추천수 비추천 버튼을 클릭한 경우 +1 을 위한 UPDATE 실행 게시글 등록 사용자는 +1 하지 않음
게시글 제목 사용자가 입력하지 않은 경우 등록하지 않음 화면, 서비스에서 검사 가능
게시글 내용 사용자가 입력하지 않은 경우 등록하지 않음 화면, 서비스에서 검사 가능

테이블에서 해야하는 일과 로직에서 해야하는 일을 분리하고 난 뒤 실제 데이터베이스에 Table을 생성합니다.

3. Table 생성

분리한 정보를 ERD (Entity Relationship Diagram)로 표현합니다.

MySQL Workbench 에서 할 수 있지만 지금은 그림으로 표현하겠습니다.

※ ERD (Entity Relationship Diagram) 에 대해서는 추후 DB 에서 작성할 예정입니다.

메뉴와 게시글의 관계만 표현을 했지만 이런 방식으로 테이블과 테이블의 관계를 표현하고 이해하는 것이 중요합니다.

테이블을 설계할 때 생각하는 방법을 설명하기 위해서 글을 작성했지만 튜토리얼에서는 게시글 등록만 진행하기 위해서 게시글 테이블만 생성할 것입니다.

 

CREATE TABLE `study`.`board` (
  `MENU_ID` VARCHAR(30) NOT NULL COMMENT '메뉴 ID',
  `BOARD_ID` VARCHAR(200) NOT NULL COMMENT '게시글ID',
  `TITLE` VARCHAR(100) NOT NULL COMMENT '제목',
  `CONTENT` LONGTEXT NOT NULL COMMENT '내용',
  `VIEW_CNT` INT NOT NULL DEFAULT 0 COMMENT '조회수',
  `LIKE_CNT` INT NOT NULL DEFAULT 0 COMMENT '추천수',
  `UNLIKE_CNT` INT NOT NULL DEFAULT 0 COMMENT '비추천수',
  `INSERT_USER_ID` VARCHAR(50) NOT NULL COMMENT '작성자',
  `INSERT_DT` DATE NOT NULL COMMENT '작성일',
  `UPDATE_USER_ID` VARCHAR(50) NULL COMMENT '수정자',
  `UPDATE_DT` DATE NULL COMMENT '수정일',
  PRIMARY KEY (`MENU_ID`, `BOARD_ID`))
COMMENT = '게시글';

테이블 생성 후 테스트를 위해 INSERT 쿼리를 실행해봅니다.

INSERT INTO BOARD(
    MENU_ID,
    BOARD_ID,
    TITLE,
    CONTENT,
    INSERT_USER_ID,
    INSERT_DT
)VALUES(
    'TEST',
    'TEST',
    'TEST',
    'TEST',
    'TEST',
    NOW()
);

Java 의 데이터 타입과 MySQL 의 데이터 타입이 어떻게 다르고 어떻게 연결해야하는지는 아래 링크를 확인 바랍니다.

Java, JDBC, and MySQL Types에 관련된 공식 홈페이지 글입니다.


3. 소스코드(Back-end)

1. 설정 파일 

BOARD Table 의 Column 인 MENU_ID 를 menuId 로 바꿔서 사용할 수 있습니다.

CamelCase 라고 부릅니다.

( _ 뒤에 첫글자는 대문자로 치환합니다.)

먼저 context-datasource.xml 의 내용을 변경합니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
        http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
    <!-- Root Context: defines shared resources visible to all other web components -->

      <!-- MySQL DataSource -->
      <bean id="dataSource"
        class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/study?useSSL=false&amp;serverTimezone=UTC">
        </property>
        <property name="username" value="study"></property>
        <property name="password" value="study"></property>
    </bean>

      <!-- mybatis SqlSessionFactoryBean -->
      <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="mapperLocations">
            <list>
                <value>classpath:/mapper/*_SQL.xml</value>
            </list>
        </property>
        <!-- 이 부분이 추가 되었습니다. -->
        <property name="configLocation" value="classpath:/mybatis-config.xml"></property>
    </bean>

</beans>

configLocation 을 추가하고 mybatis-config.xml 파일을 생성합니다.

위치는 src/main/resources 아래 입니다.

src/main/resources/

해당 파일에 mapUnderscoreToCamelCase 속성을 true 로 설정합니다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="callSettersOnNulls" value="true"/>
        <setting name="jdbcTypeForNull" value="NULL"/>
    </settings>
</configuration>

 

SELECT 결과를 조회하기 위한 jackson 라이브러리를 pom.xml 에 추가합니다.

<dependencies> 태그 안에 작성하시면 됩니다.

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.4.1</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.4.1.1</version>
        </dependency>

        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-core-asl</artifactId>
            <version>1.9.13</version>
        </dependency>

        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-mapper-asl</artifactId>
            <version>1.9.13</version>
        </dependency>

 

2. Mapper.xml(Query 작성 영역)

BOARD Table 에 INSERT 할 쿼리와 결과를 확인할 SELECT 쿼리를 생성합니다.

board_SQL.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.study.project_name.board.dao.BoardDao">

    <select id="getBoardList" resultType="com.study.project_name.board.vo.BoardVO">
        SELECT 
               MENU_ID       
             , BOARD_ID      
             , TITLE         
             , CONTENT       
             , VIEW_CNT      
             , LIKE_CNT      
             , UNLIKE_CNT    
             , INSERT_USER_ID
             , INSERT_DT     
             , UPDATE_USER_ID
             , UPDATE_DT     
          FROM STUDY.BOARD
    </select>

    <insert id="createBoard" parameterType="com.study.project_name.board.vo.BoardVO">
        INSERT INTO BOARD(
            MENU_ID           ,
            BOARD_ID          ,
            TITLE             ,
            CONTENT           ,
            INSERT_USER_ID    ,
            INSERT_DT
        ) VALUES (
            #{menuId}         ,
            #{boardId}        ,
            #{title}          ,
            #{content}        ,
            #{insertUserId}   ,
            NOW()
        );
    </insert>

</mapper>

 

 

3. VO, DTO(데이터 교환을 위한 객체)

아직은 Lombok 라이브러리를 사용하지 않고 VO, DTO를 생성합니다.

BoardVO.java

package com.study.project_name.board.vo;

import java.sql.Date;

public class BoardVO {

    private String menuId;
    private String boardId;
    private String title;
    private String content;
    private String viewCnt;
    private String likeCnt;
    private String unlikeCnt;
    private String insertUserId;
    private Date insertDt;
    private String updateUserId;
    private Date updateDt;

    /* getter, setter 를 생성하셔야합니다. */

    /* 아래 toString Method 는 자동 생성 후 보기 편하도록 제가 수정한 것입니다. */
    @Override
    public String toString() {
        return "menuId        =" + menuId       + "\n" +
                "boardId      =" + boardId      + "\n" +
                "title        =" + title        + "\n" +
                "content      =" + content      + "\n" +
                "viewCnt      =" + viewCnt      + "\n" +
                "likeCnt      =" + likeCnt      + "\n" +
                "unlikeCnt    =" + unlikeCnt    + "\n" +
                "insertUserId =" + insertUserId + "\n" +
                "insertDt     =" + insertDt     + "\n" +
                "updateUserId =" + updateUserId + "\n" +
                "updateDt     =" + updateDt ;
    }

}

 

4. DAO(실제 DB에 접근하는 역할)

mapper.xml 의 namespace 를 DAO 와 맞추셨다면 해당클래스의 Name 을 찾아주는 메서드를 사용하셔도 됩니다.

이 부분은 튜토리얼을 진행하면서 추가할 예정입니다.

BoardDao.java

package com.study.project_name.board.dao;

import java.util.List;

import javax.inject.Inject;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.stereotype.Repository;

import com.study.project_name.board.vo.BoardVO;

@Repository
public class BoardDao {

    @Inject
    private SqlSessionFactory sqlSessionFactory;

    public List<BoardVO> getBoardList() throws Exception {
        SqlSession session = sqlSessionFactory.openSession();
        return session.selectList( "com.study.project_name.board.dao.BoardDao.getBoardList" );
    }

    public void createBoard(BoardVO vo) throws Exception {
        SqlSession session = sqlSessionFactory.openSession();
        session.insert( "com.study.project_name.board.dao.BoardDao.createBoard", vo );
    }
}

 

5. Service(Business Logic을 처리하는 역할)

Service 는 interface, class 두개의 파일로 생성합니다.

※ interface, abstract class, class .. 등의 차이는 java 기초 개념에서 다루도록 하겠습니다.

 

interface - BoardService.java

package com.study.project_name.board.service;

import java.util.List;

import com.study.project_name.board.vo.BoardVO;

public interface BoardService {

    public List<BoardVO> getBoardList();
    void createBoard(BoardVO vo) throws Exception;

}

class - BoardServiceImpl.java

package com.study.project_name.board.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.study.project_name.board.dao.BoardDao;
import com.study.project_name.board.vo.BoardVO;

@Service
public class BoardServiceImpl implements BoardService {

    @Autowired
    BoardDao boardDao;

    @Override
    public List<BoardVO> getBoardList() {
        List<BoardVO> result = new ArrayList<BoardVO>();
        try {
            result = boardDao.getBoardList();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    @Override
    @Transactional
    public void createBoard(BoardVO vo) throws Exception {
        boardDao.createBoard(vo);
    }

}

BoardServiceImpl.java 의 createBoard Method 상단에 있는 @Transactional 의 내용은 다음과 같습니다.

스프링은 코드 기반의 트랜잭션 처리(Programmatic Transaction) 뿐만 아니라 선언적 트랜잭션(Declarative Transaction)을 지원하고 있습니다. 

스프링이 제공하는 트랜잭션 템플릿 클래스를 이용하거나 설정 파일, 어노테이션을 이용해서 트랜잭션의 범위 및 규칙을 정의할 수 있습니다.

@Transactional 관련 옵션에 대한 내용은 튜토리얼을 진행하면서 추가하겠습니다.

 

6. Controller(URL 요청을 처리하는 역할)

Controller 에서는 세개의 URL 을 처리해야합니다.

  1. 게시글 등록 페이지 요청 처리
  2. 게시글 등록 요청 처리
  3. 게시글 목록 요청 처리(등록 후 확인을 위한 URL)

BoardController.java

package com.study.project_name.board.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.study.project_name.board.service.BoardService;
import com.study.project_name.board.vo.BoardVO;

@Controller
public class BoardController {

    @Autowired
    BoardService boardService;

    @RequestMapping(value="/board/create", method=RequestMethod.GET)
    public String getBoardCreateView() throws Exception {
        return "board/create";
    }

    @ResponseBody
    @RequestMapping(value="/board/get", method=RequestMethod.GET)
    public List<BoardVO> getBoardList() throws Exception {
        return boardService.getBoardList();
    }

    @ResponseBody
    @RequestMapping( value="/board/create", method=RequestMethod.POST, headers= {"Accept=application/json"} )
    public String getBoardCreate(@RequestBody BoardVO vo) throws Exception {
        System.out.println( vo.toString() );
        vo.setMenuId( "TEST001" );
        vo.setBoardId( "TEST001" );
        vo.setInsertUserId( "TEST001" );
        boardService.createBoard(vo);
        return vo.toString();
    }

}

※ 현재 Log4j 설정을 따로 하지 않았기 때문에 게시글 등록 후 별도의 URL 을 생성하여 처리합니다.

※ 튜토리얼을 진행하면서 Log4j 설정 등 기본 설정도 추가할 예정입니다.

 

7. 체크해야할 부분

Controller, Service, Dao 상단에 붙어있는 Annotation 이 있습니다.

Anootation 설명 사용
@Controller 웹 어플리케이션에서 웹 요청과 응답을 처리하는 Class 에 사용
(프리젠테이션 레이어)
Controller Class
@Service 비즈니스 로직을 가진 Class 에 사용
(서비스 레이어)
Service Class
@Repository 영속성을 가지는 속성
(퍼시스턴스 레이어)
DAO 같은 DB 관련 Class
@Component 개발자가 직접 작성한 Class 를 Bean 으로 등록하기 위한 Annotation 그 외 사용하고 싶은 Class

해당 내용은 아래 링크를 참고하시기 바랍니다.

 

[Spring] Annotation

 


4. 소스코드(Front-end)

1. jsp

jsp 는 게시글 등록 페이지만 만들었습니다. css 는 적용하지 않았습니다.

jsp 페이지 <script> 태그를 보시면 src="/resources 라고 되어있습니다.

이 부분은 src/main/webapp/WEB-INF/appServlet/servlet-context.xml 에서 내용을 확인하실 수 있습니다.

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" contentType = "text/html;charset=utf-8" %>
<html>
<head>
    <title>Board Create</title>
    <!-- servlet-context.xml : <resources mapping="/resources/**" location="/resources/" /> -->
    <script type="text/javascript" src="/resources/js/lib/jquery/jquery-3.5.1.js"></script> 
</head>
<body>
    <h1>
    	게시글 등록
    </h1>

    <table>
        <tr>
            <th></th>
            <td align="right">
                <input type="button" value="등록" onclick="onclickCreate();" />
            </td>
        </tr>
        <tr>
            <th>제목</th>
            <td>
                <input id="title" type="text" maxlength="30" />
            </td>
        </tr>
        <tr>
            <th>내용</th>
            <td>
                <textarea id="content" rows="10"></textarea>
            </td>
        </tr>
    </table>

    <script type="text/javascript" src="/resources/js/board/create.js"></script> 
</body>
</html>

2. js

javascript 파일은 총 두개를 만들어야 합니다.

  1. jquery-3.5.1.js - ajax 를 사용하기 위한 파일
  2. create.js - 실제 ajax 를 사용하는 파일

먼저 jquery 를 다운받는 방법입니다.

 

jQuery 공식 홈페이지

다운로드 페이지로 이동합니다.

 

Download the uncompressed, development jQuery 3.5.1 클릭하시면 페이지가 열립니다.

페이지에 표신된 부분을 전체 복사 후 jquery-3.5.1.js 로 저장하시면 됩니다.

 

create.js

$(function() {
   console.log("foo");
});

function onclickCreate() {

    var title   = $('#title').val();
    var content = $('#content').val();
    console.log(title);
    console.log(content);
    var data = { 
        title   : title   ,
        content : content 
    };
    $.ajax({
        type        : "POST"               , // HTTP 요청 방식(GET, POST)
        url         : "/board/create"      , // 클라이언트가 요청을 보낼 서버의 URL 주소
        contentType : "application/json"   , 
        dataType    : "json"               , // 서버에서 보내줄 데이터의 타입
        data        : JSON.stringify(data) ,
        success     : function(data) {
            console.log(data);
        } , 
        error       : function( xhr, status, err ) {
            console.log(xhr);
        }
        
    });

}

프로젝트에서 필요한 파일은 모두 생성 되었습니다.


5. 최종 프로젝트 폴더의 모습


6. 테스트 및 결과 확인

설정된 port 확인하고 서버를 기동합니다.

브라우저를 켜고 게시글 등록을 위한 페이지로 이동합니다. 

저의 경우 http://localhost:9099/board/create 입니다. port 를 9099 으로 설정했기 때문에 9099 로 이동 하였습니다.

적당한 값을 채우고 등록 버튼을 클릭합니다.

현재 필수정보인 값을 체크하지 않기 때문에 값을 무조건 채워주셔야합니다.

그리고 확인을 위해 ip:port/board/get 로 이동합니다

※ 저의 경우 http://localhost:9099/board/get 입니다.

브라우저 화면에 위와 같은 데이터가 출력되면 성공입니다.

MySQL Workbench 에서 쿼리를 이용해도 해당 내용을 확인하실 수 있습니다.


※ 게시글 등록 기능 완료

지금까지 Spring Framework MVC Project 를 이용하여 가장 기초적인 웹 서비스를 생성하였습니다. 

앞으로는 기초가 아닌 실제 커뮤니티 사이트 만들기 프로젝트를 진행할 예정입니다.

반응형

댓글