Bigfat

[Spring] MyBatis 사용하기, 댓글 쓰기 구현하기 (게시판 구현) 본문

Java/web

[Spring] MyBatis 사용하기, 댓글 쓰기 구현하기 (게시판 구현)

kyou 2017. 4. 24. 18:47

스프링 MVC 구조로 MyBatis를 사용하여 댓글 쓰기를 구현해보자

  기존에 Ajax를 이용한 댓글 쓰기 구현하기에서 모델2 구조로 구현했던 댓글 쓰기 기능을 MVC 구조로 MyBatis(이하 마이바티스)를 사용하여 구현해보자. 비동기 통신 Ajax와 데이터포맷인 Json은 기존 글을 참고하거나 검색해보도록 한다.

  프로젝트 구조는 아래와 같이 진행되며 Comment 서블릿의 설정 파일인 comment-context.xml, 마이바티스 설정 파일인 mybatisConfig.xml, 쿼리를 작성할 Mapper XML 파일인 comment.xml, 그리고 com.edu.comment 패키지 아래에 컨트롤러, 서비스, DAO의 위치를 확인하자.


  기존의 프로젝트에서는 *.bbs에 대한 요청을 받는 DispatcherServlet인 appServlet만 사용하였다. 이제 디스패처 서블릿을 하나 더 추가해 *.comment에 대한 요청을 처리할 수 있도록 하자.

  web.xml을 열어 commentServlet이라는 서블릿에 관해 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  ...
  
  <servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <!-- Dispatcher Servlet Mapping -->
  <servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>*.bbs</url-pattern>
  </servlet-mapping>
  
  <!-- Comment Servlet을 추가한다. -->
  <servlet>
      <servlet-name>commentServlet</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>/WEB-INF/spring/appServlet/comment-context.xml</param-value>
      </init-param>
      <load-on-startup>2</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>commentServlet</servlet-name>  
    <url-pattern>*.comment</url-pattern>
  </servlet-mapping>
  
  ...
cs


  commentServlet의 설정 파일인 comment-context.xml은 /WEB-INF/spring/appServlet/에 아래와 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
 
    <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
    
    <!-- Enables the Spring MVC @Controller programming model -->
    <annotation-driven />
 
    <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
    <resources mapping="/resources/**" location="/resources/" />
 
    <!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
    <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <beans:property name="prefix" value="/WEB-INF/views/" />
        <beans:property name="suffix" value=".jsp" />
    </beans:bean>
    
    <context:component-scan base-package="com.edu.comment" />
    
</beans:beans>
cs


  /WEB-INF/spring/root-context.xml의 마지막 부분에 id가 commentDao인 MapperFactoryBean을 생성하고, CommentDao 인터페이스를 mapperInterface로 지정해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <!-- Root Context: defines shared resources visible to all other web components -->
    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:db.properties"></property>
    </bean>
 
    <bean id="dataSource"
            class="org.apache.tomcat.dbcp.dbcp2.BasicDataSource">
        <property name="driverClassName" value="${driverClassName}"></property>
        <property name="url" value="${url}"></property>
        <property name="username" value="${username}"></property>
        <property name="password" value="${password}"></property>
        <!-- <property name="validationQuery"></property> -->
    </bean>
    
    <bean id="sqlSessionFactory"
            class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
        <property name="configLocation" value="classpath:/mybatisConfig/mybatisConfig.xml"></property>
    </bean>
    
    <bean id="sqlSessionTemplate"
            class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg ref="sqlSessionFactory"></constructor-arg>
    </bean>
 
    <bean id="bbsDao"
            class="org.mybatis.spring.mapper.MapperFactoryBean">
        <property name="mapperInterface" value="com.edu.bbs.dao.BBSDao"></property>
        <property name="sqlSessionTemplate" ref="sqlSessionTemplate"></property>
    </bean>
    
    <!-- mapper xml과 interface를 연결해준다. -->
    <!-- 단점: 인터페이스가 늘어날수록 bean을 등록해줘야한다. -->
    <bean id="commentDao"
            class="org.mybatis.spring.mapper.MapperFactoryBean">
        <property name="mapperInterface" value="com.edu.comment.dao.CommentDao"></property>
        <property name="sqlSessionTemplate" ref="sqlSessionTemplate"></property>
    </bean>
    
</beans>
cs


  src/main/resources/mybatisConfig/ 아래 마이바티스 설정 파일인 mybatisConfig.xml에 Type Alias를 추가하고, mapper 또한 추가한다. CommentDto클래스는 기존 프로젝트와 동일하게 사용한다. 또한 Comments 테이블도 기존의 테이블을 그대로 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?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>
        <!-- http://www.mybatis.org/mybatis-3/ko/configuration.html -->
        <setting name="cacheEnabled" value="true"/>
        <setting name="lazyLoadingEnabled" value="false"/>
        <setting name="multipleResultSetsEnabled" value="true"/>
        <setting name="useColumnLabel" value="true"/>
        <setting name="useGeneratedKeys" value="false"/>
        <setting name="defaultExecutorType" value="SIMPLE"/>
        <setting name="defaultStatementTimeout" value="25000"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="jdbcTypeForNull" value="VARCHAR"/>
        <!-- <setting name="callSettersOnNulls" value="true" /> -->
    </settings>
    
    <typeAliases>
        <typeAlias alias="article" type="com.edu.bbs.dto.BBSDto"/>
        <!-- CommentDto를 resultType에 바로 명시하기 위해 Type Alias를 추가한다. -->
        <typeAlias alias="comment" type="com.edu.comment.dto.CommentDto"/>
    </typeAliases>
    
    <mappers>
        <mapper resource="com/edu/mapper/bbs/bbs.xml"/>
        <!-- comment 관련 쿼리를 작성한 mapper xml을 추가한다. -->
        <mapper resource="com/edu/mapper/comment/comment.xml"/>
    </mappers>
    
</configuration>
cs



  Ajax를 이요한 댓글 쓰기 구현하기에서와 같이 게시글을 열었을 때, 댓글의 총 개수를 카운팅해오기 위해 com.edu.bbs.BBSServiceImpl클래스의 content메서드를 아래와 같이 변경한다.

1
2
3
4
5
6
7
8
9
@Override
public BBSDto content(String articleNumber) {
    BBSDto article = bbsDao.selectArticle(articleNumber);
    // 댓글 개수를 카운팅해오는 메서드의 return값을 BBSDto의 commentCount 변수에 저장한다.
    article.setCommentCount(bbsDao.countComments(articleNumber));
    bbsDao.upHit(articleNumber);
    
    return article;
}
cs


  com.edu.bbs.BBSDao 인터페이스에 댓글 총 개수를 카운팅해오는 countComments 메서드를 선언한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.edu.bbs.dao;
 
import java.util.HashMap;
import java.util.List;
 
import com.edu.bbs.dto.BBSDto;
 
public interface BBSDao {
    
    ...
    
    public int deleteArticle(String articleNumber);
    
    // countComments 메서드를 선언한다.
    public int countComments(String articleNumber);
}
cs


  Mapper XML인 src/main/resources/com/edu/mapper/bbs/bbs.xml에 아래와 같이 쿼리를 작성한다. 이제 Comments 테이블 내에 데이터가 있다면 글을 확인했을 때 댓글의 총 개수를 기존 프로젝트와 같이 불러온다.

1
2
3
4
5
<select id="countComments" parameterType="String" resultType="int">
    SELECT count(*)
      FROM comments 
    WHERE article_number = #{articleNumber}
</select>
cs


  댓글(comment)은 비동기 통신인 Ajax로 JSON 데이터를 주고 받기 때문에 pom.xml에 org.json 의존성을 추가해준다.

1
2
3
4
5
6
7
<!-- JSON -->
<!-- https://mvnrepository.com/artifact/org.json/json -->
<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20160810</version>
</dependency>
cs


  CommentController 클래스를 아래와 같이 작성한다. JSON 데이터 전송 코드들은 중복되므로 따로 메서드를 만들어 분리해주는 것이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.edu.comment.controller;
 
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
 
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
 
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
 
import com.edu.comment.dto.CommentDto;
import com.edu.comment.service.CommentService;
 
@Controller
public class CommentController {
    
    @Autowired
    private CommentService commentService;
    
    @RequestMapping("/commentWrite.comment")
    public String writeAndReadComments(HttpSession session, HttpServletResponse resp, CommentDto comment) {
        comment.setId(session.getAttribute("id").toString());
        
        this.sendJsonObject(resp, commentService.writeAndReadComments(comment));
        
        return null;
    }
    
    @RequestMapping("/commentRead.comment")
    public String readComments(HttpServletResponse resp, int articleNumber, int commPageNum) {
        this.sendJsonObject(resp, commentService.readComments(articleNumber, commPageNum));
        
        // return ""로 하면 404에러 때문에 데이터 안 넘어간다.
        return null;
    }
 
    @SuppressWarnings("unchecked")
    public void sendJsonObject(HttpServletResponse resp, Object obj) {
        resp.setCharacterEncoding("utf-8");
        PrintWriter pw = null;
        JSONObject jsonObj = null;
        JSONArray jsonArr = null;
        
        try {
            pw = resp.getWriter();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        if(obj instanceof HashMap) {        // 무인자 자료형(raw type)
            jsonObj = new JSONObject((HashMap<String, Object>) obj);
            pw.println(jsonObj);
        } else if(obj instanceof ArrayList<?>) { // 비한정적 와일드카드 자료형(unbounded wildcard type)
            jsonArr = new JSONArray((ArrayList<CommentDto>) obj);
            pw.println(jsonArr);
        }
    }
}
cs

  참고로 sendJsonObject 메서드 내에 if-else if문에서 파라미터로 넘겨받은 obj를 instanceof 연산자로 obj 인스턴스의 실제 타입을 알아내려 하고 있다. sendJsonObject 메서드를 호출하면서 writeAndReadComments 메서드에서는 HashMap<String, Object> 타입의 파라미터를, readComments 메서드에서는 ArrayList<CommentDto> 타입의 파라미터로 던져준다. 이런 HashMap<String, Object>과 ArrayList<CommentDto>와 같은 형인자 자료형(parameterized type)의 실형인자(actual type parameter)는 컴파일 타임(Compile time)에 타입에 대한 검증을 할 뿐, 실행 타임(Run time)에는 실제 타입 정보를 유지하지 않는다. instanceof 연산자는 런타임에 인스턴스의 타입 정보를 체크하기 위한 연산자인데, 만약 obj instanceof HashMap<String, Object>와 같이 비교했을 경우 'Cannot perform instanceof check against parameterized type HashMap<String,Object>. Use the form HashMap<?,?> instead since further generic type information will be erased at runtime'와 같은 에러메시지를 보여준다. 런타임에는 형인자(제네릭 자료형 정보)가 모든 객체의 부모 클래스인 Object Type으로 처리된다.


  CommentService 인터페이스는 메서드를 선언만 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.edu.comment.service;
 
import java.util.ArrayList;
import java.util.HashMap;
 
import com.edu.comment.dto.CommentDto;
 
public interface CommentService {
 
    public HashMap<String, Object> writeAndReadComments(CommentDto comment);
    
    public ArrayList<CommentDto> readComments(int articleNumber, int commPageNum);
    
}
cs


  CommentService 인터페이스를 구현하는 CommentServiceImpl 클래스를 아래와 같이 작성한다. 마이바티스는 파라미터를 오직 하나밖에 받지 못하므로 복수개의 파라미터는 HashMap에 담아 던져주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.edu.comment.service;
 
import java.util.ArrayList;
import java.util.HashMap;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import com.edu.comment.dao.CommentDao;
import com.edu.comment.dto.CommentDto;
 
@Service
public class CommentServiceImpl implements CommentService {
 
    @Autowired
    private CommentDao commentDao;
 
    HashMap<String, Object> paramMap, resultMap;
    
    @Override
    public HashMap<String, Object> writeAndReadComments(CommentDto comment) {
        int result = commentDao.writeComment(comment);
        
        resultMap = new HashMap<>();
        resultMap.put("result", result);
        resultMap.put("comments"this.readComments(comment.getArticleNumber(), 10));
        
        return resultMap;
    }
 
    @Override
    public ArrayList<CommentDto> readComments(int articleNumber, int commPageNum) {
        paramMap = new HashMap<>();
        paramMap.put("articleNumber", articleNumber);
        paramMap.put("commPageNum", commPageNum);
        
        return commentDao.readComments(paramMap);
    }
    
}
cs


  CommentDao 인터페이스를 아래와 같이 작성한다. 이 Mapper 인터페이스에 선언되어있는 메서드명과 Mapper XML인 comment.xml에 id가 일치하는 쿼리를 수행하고 결과를 return 해준다. 만약 메서드는 선언해놨는데 일치하는 id의 엘리먼트가 없다면 에러가 발생하므로 조심하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.edu.comment.dao;
 
import java.util.ArrayList;
import java.util.HashMap;
 
import com.edu.comment.dto.CommentDto;
 
public interface CommentDao {
    
    public int writeComment(CommentDto comment);
    
    public ArrayList<CommentDto> readComments(HashMap<String, Object> paramMap);
    
}
cs


  Mapper XML인 comment.xml을 아래와 같이 작성한다. parameterType나 resultType은 int와 같은 기본 자료형(Primitive Type), 패키지를 포함한 클래스명(예를 들면, java.util.HashMap) 혹은 클래스명(HashMap)을 줄 수 있다. 물론 Wrapper클래스들이나 컬렉션 프레임워크 클래스들만 클래스명으로 기입 가능하며, 그렇지 않을 경우 마이바티스 설정 파일에서 Type Alias를 설정해주면 된다. 기본 자료형의 경우 상응하는 Wrapper클래스로 자동 변경(Auto Boxing)된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN" "http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="com.edu.comment.dao.CommentDao">
 
    <insert id="writeComment" parameterType="comment">
        INSERT INTO comments
             VALUES (comment_seq.nextval
                     , #{id}
                     , #{commentContent}
                     , sysdate
                     , #{articleNumber})
    </insert>
    
    <select id="readComments" parameterType="HashMap" resultType="comment">
        SELECT *
          FROM (SELECT id, comment_content, comment_date, article_number
                  FROM comments
                 WHERE article_number = #{articleNumber}
                 ORDER BY comment_number DESC
               ) comments
         WHERE rownum BETWEEN 1 AND #{commPageNum}
    </select>
    
</mapper>
cs


  content.jsp의 스크립트도 수정이 필요한데, Ajax의 url을 *.comment 요청으로 변경하도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$(function() {
    $("#commentWrite").on("click"function() {
        $.ajax({
            url : "/bbs/commentWrite.comment",
            data : {
                commentContent : $("#commentContent").val(),
                articleNumber : "${article.articleNumber}"
            },
            beforeSend : function() {
                console.log("시작 전...");
            },
            complete : function() {
                console.log("완료 후...");
            },
            success : function(data) {
                if (data.result == 1) {
                    console.log("comment가 정상적으로 입력되었습니다.");
                    $("#commentContent").val("");
                    showHtml(data.comments, 1);
                }
            }
        })
    });
});
 
...
 
function getComment(commPageNum, event) {
    $.ajax({
        url : "/bbs/commentRead.comment",
        data : {
            commPageNum : commPageNum * 10,
            articleNumber : "${article.articleNumber}"
        },
        beforeSend : function() {
            console.log("읽어오기 시작 전...");
        },
        complete : function() {
            console.log("읽어오기 완료 후...");
        },
        success : function(data) {
            console.log("comment를 정상적으로 조회하였습니다.");
            showHtml(data, commPageNum);
 
            let position = $("#showComment table tr:last").position();
            $('html, body').animate({
                scrollTop : position.top
            }, 400);
        }
    })
}
cs



[Effective Java 5장 제네릭 참고]

[Java Generics(제네릭스) 정리 01_01 참고]