Bigfat

[Spring] 스프링 MVC 구조로 게시판 구현하기 (댓글 제외) 본문

Java/web

[Spring] 스프링 MVC 구조로 게시판 구현하기 (댓글 제외)

kyou 2017. 4. 18. 17:30

기존의 게시판 프로젝트를 스프링 MVC 구조로 구현해보자

  먼저 프로젝트가 어떤 구조로 진행될 지 확인하자.


  /WEB-INF/web.xml에 아래와 같은 설정을 추가해줬다. URL을 루트로, 즉 http://localhost/bbs/로 접근하였을 때 login.jsp화면을 출력해주기 위해 <welcome-file>를, UTF-8 인코딩을 위한 <filter>를, 에러 페이지를 처리하기 위한 <error-page>를 추가해준 코드다. 컨트롤러에서 request를 받아서 setCharacterEncoding메서드로 변경해줘도 안되는 이유는 아직 모르겠다..

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
64
65
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
 
    <welcome-file-list>
        <welcome-file>/WEB-INF/views/login.jsp</welcome-file>
    </welcome-file-list>
 
    <!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring/root-context.xml</param-value>
    </context-param>
    
    <!-- Creates the Spring Container shared by all Servlets and Filters -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
 
    <!-- Processes application requests -->
    <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>
        
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>*.bbs</url-pattern>
    </servlet-mapping>
 
    <!-- Encoding Filter -->
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
    <!-- Error Page -->
    <error-page>
        <error-code>400</error-code>
        <location>/WEB-INF/views/errorPage.jsp</location>
    </error-page>
    <error-page>
        <error-code>404</error-code>
        <location>/WEB-INF/views/errorPage.jsp</location>
    </error-page>
    <error-page>
        <error-code>500</error-code>
        <location>/WEB-INF/views/errorPage.jsp</location>
    </error-page>
</web-app>
 
cs


  common패키지 내의 LoginStatus인터페이스와 BBSDto클래스는 기존의 코드를 그대로 사용할 것이기 때문에 로그인, 로그아웃 구현하기를 참고하자.


  BBSController는 아래와 같이 작성하였다. 컨트롤러는 @Controller 애노테이션을 사용해 Beans에 등록해준다. Spring Explorer를 사용하면 이렇게 등록된 Bean들을 확인할 수 있다. Bean객체는 스프링 컨테이너에 의해 관리되는 객체를 의미한다(참고1). 스프링은 모든 Bean을 싱글톤(Singleton) 객체로 만든다. 그러므로 DTO객체의 경우는 new연산자로 인스턴스를 얻어와야 한다.

  @Autowised 애노테이션은 스프링이 지원하는 애노테이션으로 의존 관계를 자동 설정해주며, 타입을 이용해 의존하는 객체를 삽입한다. 만약 프레임워크의 확장이나 변경이 필요하다면 Java가 지원하는 애노테이션을 사용해야하며, @Inject는 타입을, @Resource는 이름을 이용해 객체를 삽입한다. @Resource는 같은 타입이라도 이름을 달리하여 구분할 수 있다.

  이렇듯 어노테이션을 활용하여 클래스들의 관계를 직접 코딩하지 않고 스프링에게 알려주면, 스프링 프레임워크가 필요한 객체(의존하는 객체, 의존 관계에 있는 객체)를 알아서 주입해준다는 것이 스프링 프레임워크 3대 개념 중 하나인 IoC/DI(Inversion of Control/Dependency Injection)다.

  @RequestMapping을 사용해 URL을 컨트롤러의 메서드와 맵핑시켜준다. @RequestMapping 또한 스프링 프레임워크 애노테이션이다. content메서드를 보면 별도의 옵션을 더 지정하지 않는다면 URL만 기입해줘도 된다는 것을 알 수 있다. 그리고 writeForm메서드의 어노테이션을 보면 같은 URL 요청에도 GET, POST방식을 구분하는 옵션을 줄 수 있는 것을 알 수 있다(참고2).

  Model객체를 사용하여 뷰(*.jsp)에 데이터를 던져줄 수 있고, 그 데이터는 뷰에서 EL이나 JSTL로 받아 사용할 수 있다. 컨트롤러는 최대한 데이터를 파라미터로 얻거나 Model객체에 심어주는 역할만 수행하도록 하자.

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package com.edu.bbs.controller;
 
import javax.servlet.http.HttpSession;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
import com.edu.bbs.dto.BBSDto;
import com.edu.bbs.service.BBSService;
 
@Controller
public class BBSController {
 
    @Autowired
    private BBSService bbsService;
    
    BBSDto article;
    
    /**
     * 게시판 리스트 출력
     */
    @RequestMapping(value="/list.bbs")
    public String list(Model model, int pageNum) {
        bbsService.list(pageNum, model);
        
        return "list";
    }
    
    /**
     * 글 읽기 화면 및 기능
     */
    @RequestMapping("/content.bbs")        // value값만 줄 땐 value= 생략 가능하다.
    public String content(Model model, String articleNumber, int pageNum) {
        article = bbsService.content(articleNumber);
        model.addAttribute("article", article);
        model.addAttribute("pageNum", pageNum);
        
        return "content";
    }
    
    /**
     * 로그인 기능
     */
    @RequestMapping(value="/login.bbs", method=RequestMethod.POST)
    public String login(HttpSession session, String id, String pw, Model model) {
        return bbsService.login(session, id, pw);
    }
    
    /**
     * 로그아웃 기능
     */
    @RequestMapping(value="/logout.bbs")
    public String logout(HttpSession session) {
        session.invalidate();
        
        return "login";
    }
    
    /**
     * 글 쓰기 화면
     */
    @RequestMapping(value="/write.bbs", method=RequestMethod.GET)        // GET, POST방식으로 구분할 수 있다, writeForm.jsp 변경
    public String writeForm(HttpSession session, Model model, String pageNum) {
        model.addAttribute("pageNum", pageNum);
        
        // Interceptor를 이용하면 더이상 이런 코드를 작성하지 않아도 된다.
        if(session.getAttribute("id"!= null)
            return "writeForm";
        else
            return "login";
    }
    
    /**
     * 글 쓰기 기능
     */
    @RequestMapping(value="/write.bbs", method=RequestMethod.POST)
    public String write(HttpSession session, BBSDto article) {        // DTO로 바로 받아올 수 있다.
        article.setId(session.getAttribute("id").toString());
        bbsService.write(article);
        
        return "redirect:/list.bbs?pageNum=1";
    }
 
    /**
     * 답글 쓰기 화면
     */
    @RequestMapping(value="/replyForm.bbs")
    public String replyForm(Model model, String pageNum, String groupId, String depth, String pos) {
        // 게시판 플로우를 이해해야 한다.
        // Model2에서 request.getParameter로 들고 오던 값들을 생각해야 한다.
        model.addAttribute("pageNum", pageNum);
        model.addAttribute("groupId", groupId);
        model.addAttribute("depth", depth);
        model.addAttribute("pos", pos);
        
        return "replyForm";
    }
    
    /**
     * 답글 쓰기 기능
     */
    @RequestMapping(value="/reply.bbs")
    public String reply(HttpSession session, BBSDto article, String pageNum) {    
        article.setId(session.getAttribute("id").toString());
        bbsService.reply(article);
        
        return "redirect:/list.bbs?pageNum=" + pageNum;
    }
    
    /**
     * 글 수정 화면
     */
    @RequestMapping(value="/updateForm.bbs")
    public String updateForm(Model model, String pageNum, String articleNumber) {
        model.addAttribute("pageNum", pageNum);
        model.addAttribute("article", bbsService.updateForm(articleNumber));
        
        return "updateForm";
    }
    
    /**
     * 글 수정 기능
     */
    @RequestMapping(value="/update.bbs")
    public String update(String pageNum, BBSDto article) {
        bbsService.update(article);
        return "redirect:/list.bbs?pageNum=" + pageNum;
    }
    
    /**
     * 글 삭제 기능
     */
    @RequestMapping(value="/delete.bbs")
    public String delete(String articleNumber, String pageNum) {
        bbsService.delete(articleNumber);
 
        return "redirect:/list.bbs?pageNum=" + pageNum;
    }
    
}
cs


  컨트롤러 내에 선언된 메서드들의 타입을 예전에는 ModelAndView로 선언했었는데, 지금은 String으로 선언해준다. Spring MVC Architecture를 보면 ViewResolver가 view name을 받아 View를 return하는 것을 확인할 수 있다. ViewResolver는 /WEB-INF/spring/appServlet/servlet-context.xml에서 설정한다. InternalResourceViewResolver클래스가 컨트롤러로부터 return 받은 view name(예를 들면 'login')에 앞(prefix)에는 /WEB-INF/views/를, 뒤(suffix)에는 .jsp를 붙여 forward해준다(즉, /WEB-INF/views/login.jsp로 포워드해준다).

  <context:component-scan>의 base-package속성은 com.edu.bbs패키지와 하위 패키지의 모든 클래스에서 @Component, @Controller, @Service, @Repository를 검색한다.

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.bbs" />
    
</beans:beans>
cs


  BBSService인터페이스를 아래와 같이 작성한다. Service단을 Business Layer라고 하며, Service 인터페이스를 상속받아 구현하는 클래스는 비지니스 로직(Business Logic)을 수행하는 역할을 한다.

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
package com.edu.bbs.service;
 
import javax.servlet.http.HttpSession;
 
import org.springframework.ui.Model;
 
import com.edu.bbs.dto.BBSDto;
 
public interface BBSService {
    
    public Model list(int pageNum, Model model);
    
    public BBSDto content(String articleNumber);
    
    public String login(HttpSession session, String id, String pw);
    
    public int write(BBSDto article);
    
    public int reply(BBSDto article);
    
    public BBSDto updateForm(String articleNumber);
    
    public int update(BBSDto article);
    
    public int delete(String articleNumber);
    
}
cs


  BBSService인터페이스를 상속받아 구현하는 BBSServiceImpl클래스다. @Service 애노테이션을 사용하여 Beans에 등록시켜준다.

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.edu.bbs.service;
 
import java.util.ArrayList;
 
import javax.servlet.http.HttpSession;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
 
import com.edu.bbs.common.LoginStatus;
import com.edu.bbs.common.Page;
import com.edu.bbs.dao.BBSDao;
import com.edu.bbs.dto.BBSDto;
 
@Service
public class BBSServiceImpl implements BBSService {
    
    @Autowired
    private BBSDao bbsDao;
    
    @Autowired
    private Page page;
    
    @Override
    public Model list(int pageNum, Model model) {
        ArrayList<BBSDto> articles = null;
 
        int totalCount = 0;
        int pageSize = 10;
        int pageBlock = 10;
        
        totalCount = bbsDao.getArticleTotalCount();
        page.paging(pageNum, totalCount, pageSize, pageBlock);
        articles = bbsDao.selectArticles(page.getStartRow(), page.getEndRow());
        
        model.addAttribute("totalCount", totalCount);
        model.addAttribute("articles", articles);
        model.addAttribute("pageNum", pageNum);
        model.addAttribute("pageCode", page.getSb().toString());
        
        return model;
    }
    
    @Override
    public BBSDto content(String articleNumber) {
        BBSDto article = bbsDao.selectArticle(articleNumber);
        bbsDao.upHit(articleNumber);
        
        return article;
    }
    
    @Override
    public String login(HttpSession session, String id, String pw) {
        String view = null;
        int result = bbsDao.loginCheck(id, pw);
        
        if(result == LoginStatus.LOGIN_SUCCESS) {
            session.setAttribute("id", id);
            view = "redirect:/list.bbs?pageNum=1";
        } else if(result == LoginStatus.PASS_FAIL) {
            view = "login";        // 추후 변경
        } else if(result == LoginStatus.NOT_MEMBER) {
            view = "login";        // 추후 변경
        }
            
        return view;
    }
 
    @Override
    public int write(BBSDto article) {
        return bbsDao.insertArticle(article);
    }
 
    @Override
    public int reply(BBSDto article) {
        return bbsDao.replyArticle(article);
    }
 
    @Override
    public BBSDto updateForm(String articleNumber) {
        return bbsDao.selectArticle(articleNumber);
    }
 
    @Override
    public int update(BBSDto article) {
        return bbsDao.updateArticle(article);
    }
 
    @Override
    public int delete(String articleNumber) {
        return bbsDao.deleteArticle(articleNumber);
    }
    
}
cs


  BBSDao인터페이스를 아래와 같이 작성한다. DAO(Data Access Object)단을 Persistent Layer라고 하며, DAO 인터페이스를 상속받아 구현하는 클래스는 데이터베이스의 데이터에 접근(Data Access)하는 역할을 한다.

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
package com.edu.bbs.dao;
 
import java.util.ArrayList;
 
import com.edu.bbs.dto.BBSDto;
 
public interface BBSDao {
    
    public ArrayList<BBSDto> selectArticles(int startRow, int endRow);
    
    public int getArticleTotalCount();
    
    public BBSDto selectArticle(String articleNumber);
    
    public int upHit(String articleNumber);
    
    public int loginCheck(String id, String pw);
    
    public int insertArticle(BBSDto article);
    
    public int replyArticle(BBSDto article);
    
    public int updateArticle(BBSDto article);
    
    public int deleteArticle(String articleNumber);
    
cs


  BBSDao인터페이스를 상속받아 구현하는 BBSDaoImpl클래스다. @Repository 애노테이션을 사용하여 Beans에 등록시켜준다.

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
package com.edu.bbs.dao;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
 
import com.edu.bbs.common.LoginStatus;
import com.edu.bbs.dto.BBSDto;
 
@Repository
public class BBSDaoImpl implements BBSDao, LoginStatus {
    
    @Autowired
    private OracleDBConnector orclDbc;
    
    Connection conn;
    PreparedStatement pstmt;
    ResultSet rs;
    StringBuffer query;
    BBSDto article;
    ArrayList<BBSDto> articles;
 
    @Override
    public ArrayList<BBSDto> selectArticles(int startRow, int endRow) {
        conn = orclDbc.getConnection();
        query = new StringBuffer();
        articles = new ArrayList<>();
        
        query.append("SELECT bbs.* ");
        query.append("  FROM (SELECT rownum AS row_num, bbs.* ");
        query.append("            FROM (SELECT article_number, id, title, depth, hit, write_date ");
        query.append("                        FROM bbs ");
        query.append("                       ORDER BY group_id DESC, pos ");
        query.append("                    ) bbs ");
        query.append("          ) bbs ");
        query.append(" WHERE row_num BETWEEN ? AND ?");
        
        try {
            pstmt = conn.prepareStatement(query.toString());
            pstmt.setInt(1, startRow);
            pstmt.setInt(2, endRow);
            rs = pstmt.executeQuery();
        
        
            while(rs.next()) {
                article = new BBSDto();
                article.setArticleNumber(rs.getInt("article_number"));
                article.setId(rs.getString("id"));
                article.setTitle(rs.getString("title"));
                article.setDepth(rs.getInt("depth"));
                article.setHit(rs.getInt("hit"));
                article.setWriteDate(rs.getTimestamp("write_date"));
                articles.add(article);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        disconnect();
        
        return articles;
    }
    
    @Override
    public int getArticleTotalCount() {
        conn = orclDbc.getConnection();
        int totalCount = 0;
        
        try {
            pstmt = conn.prepareStatement("SELECT count(*) AS total_count FROM bbs");
            rs = pstmt.executeQuery();
            
            if(rs.next()) {
                totalCount = rs.getInt(1);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        disconnect();
        
        return totalCount;
    }
    
    @Override
    public BBSDto selectArticle(String articleNumber) {
        conn = orclDbc.getConnection();
        query = new StringBuffer();
        query.append("SELECT * FROM bbs WHERE article_number = ?");
        
        try {
            pstmt = conn.prepareStatement(query.toString());
            pstmt.setString(1, articleNumber);
            rs = pstmt.executeQuery();
            
            if(rs.next()) {
                article = new BBSDto();
                article.setArticleNumber(rs.getInt("article_number"));
                article.setId(rs.getString("id"));
                article.setTitle(rs.getString("title"));            
                article.setDepth(rs.getInt("depth"));            
                article.setContent(rs.getString("content"));
                article.setHit(rs.getInt("hit"));
                article.setGroupId(rs.getInt("group_id"));
                article.setPos(rs.getInt("pos"));
                article.setWriteDate(rs.getTimestamp("write_date"));
                article.setFileName(rs.getString("file_name"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        disconnect();
        
        return article;
    }
    
    @Override
    public synchronized int upHit(String articleNumber) {
        conn = orclDbc.getConnection();
        int result = 0;
        
        try {
            pstmt = conn.prepareStatement("UPDATE bbs SET hit = hit + 1 WHERE article_number = ?");
            pstmt.setString(1, articleNumber);
            result = pstmt.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        disconnect();
        
        return result;
    }
    
    @Override
    public int loginCheck(String id, String pw) {
        conn = orclDbc.getConnection();
        int result = 0;
        
        try {
            pstmt = conn.prepareStatement("SELECT pw FROM users WHERE id = ?");
            pstmt.setString(1, id);
            rs = pstmt.executeQuery();
            
            
            if(rs.next()) {
                if(pw.equals(rs.getString("pw")))
                    // 직관적으로 알 수 있도록 상수로 정의하자.
                    result = LOGIN_SUCCESS;
                else
                    result = PASS_FAIL;
            } else
                result = NOT_MEMBER;
            
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        disconnect();
        
        return result;
    }
    
    @Override
    public synchronized int insertArticle(BBSDto article) {
        conn = orclDbc.getConnection();
        int result = 0;
        
        query = new StringBuffer();
        query.append("INSERT INTO bbs ");
        query.append("VALUES(bbs_seq.nextval, ?, ?, ?, bbs_seq.currval, 0, 0, 0, sysdate, ?)");
        
        try {
            pstmt = conn.prepareStatement(query.toString());
            pstmt.setString(1, article.getId());
            pstmt.setString(2, article.getTitle());
            pstmt.setString(3, article.getContent());
            pstmt.setString(4, article.getFileName());
            result = pstmt.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        disconnect();
        
        return result;
    }
    
    public synchronized int upPos(int groupId, int pos) {
        conn = orclDbc.getConnection();
        int result = 0;
        query = new StringBuffer();
        query.append("UPDATE bbs");
        query.append("     SET pos = pos + 1");
        query.append(" WHERE group_id = ?");
        query.append("     AND pos > ?");
        
        try {
            pstmt = conn.prepareStatement(query.toString());
            pstmt.setInt(1, groupId);
            pstmt.setInt(2, pos);
            result = pstmt.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }
    
        disconnect();
        
        return result;
    }
    
    @Override
    public synchronized int replyArticle(BBSDto article) {
        this.upPos(article.getGroupId(), article.getPos());
        conn = orclDbc.getConnection();
        int result = 0;
        query = new StringBuffer();
        query.append("INSERT INTO bbs ");
        query.append("VALUES(bbs_seq.nextval, ?, ?, ?, ?, ?, ?, 0, sysdate, ?)");
        try {
            pstmt = conn.prepareStatement(query.toString());
            pstmt.setString(1, article.getId());
            pstmt.setString(2, article.getTitle());
            pstmt.setString(3, article.getContent());
            pstmt.setInt(4, article.getGroupId());
            pstmt.setInt(5, article.getDepth() + 1);
            pstmt.setInt(6, article.getPos() + 1);
            pstmt.setString(7, article.getFileName());
 
            result = pstmt.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        disconnect();
        
        return result;
    }
 
    @Override
    public synchronized int updateArticle(BBSDto article) {
        conn = orclDbc.getConnection();
        int result = 0;
        
        try {
            pstmt = conn.prepareStatement("UPDATE bbs SET title=?, content=? WHERE article_number=?");
            pstmt.setString(1, article.getTitle());
            pstmt.setString(2, article.getContent());
            pstmt.setInt(3, article.getArticleNumber());
            result = pstmt.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        disconnect();
        
        return result;
    }
    
    @Override
    public synchronized int deleteArticle(String articleNumber) {
        conn = orclDbc.getConnection();
        int result = 0;
        
        try {
            pstmt = conn.prepareStatement("DELETE FROM bbs WHERE article_number = ?");
            pstmt.setString(1, articleNumber);
            result = pstmt.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        disconnect();
        
        return result;
    }
    
    public void disconnect() {
        try {
            if(rs != null) {
                rs.close();
            }
            pstmt.close();
            conn.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
}
 
cs


  참고로 DAO는 단일 데이터 접근/갱신만 처리한다. Service는 여러 DAO를 호출하여 여러번의 데이터 접근/갱신을 하며 그렇게 읽은 데이터에 대한 비즈니스 로직을 수행하고, 그것을 하나의(혹은 여러개의) 트랜잭션으로 묶는다. 즉, Service는 트랜잭션 단위라고 한다(참고3).


  기존의 OracleDBConnector클래스에 @Repository 애노테이션을 붙여주고, 예외 처리를 위해 try-catch문을 사용하였다. 물론 스프링은 모든 Bean객체의 범위를 싱글톤으로 설정하므로 싱글톤 패턴을 구현한 코드는 제거하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.edu.bbs.dao;
 
import java.sql.Connection;
import java.sql.DriverManager;
 
import org.springframework.stereotype.Repository;
 
@Repository
public class OracleDBConnector {
    Connection conn;
    
    public Connection getConnection() {
        try {
            Class.forName("core.log.jdbc.driver.OracleDriver");
            String url = "jdbc:oracle:thin:@localhost:1521:XE";
            conn = DriverManager.getConnection(url, "Kyou""1234");
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return conn;
    }
}
cs


  기존의 Page클래스에 @Component 애노테이션을 붙여준다. Controller, Service, Repository 외에 Bean으로 등록해주기 위해 사용하는 애노테이션이다. 메서드 내의 코드는 부트스트랩 적용하기, Paginaion에서 작성한 코드와 동일하다. 이제 서버사이드 구현은 끝났다.

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
package com.edu.bbs.common;
 
import org.springframework.stereotype.Component;
 
@Component
public class Page {
    private int startRow, endRow;
    private StringBuffer sb;
    
    public synchronized void paging(int pageNum, int totalCount, int pageSize, int pageBlock) {
        ...
    }
    
    public StringBuffer getSb() {
        return sb;
    }
 
    public int getStartRow() {
        return startRow;
    }
 
    public int getEndRow() {
        return endRow;
    }
}
cs


  화면(*.jsp)단도 수정할 부분이 조금 있다. 먼저 모든 뷰에서 CSS를 참조하는 부분을 아래와 같이 수정하도록 하자(참고4).

1
<link href="<c:url value='/resources/css/bootstrap.min.css'/>" rel="stylesheet">
cs


  그다음 /WEB-INF/views/list.jsp 코드 내에 '글쓰기'링크를 수정해준다.

1
2
3
4
<!--url -->
<div id="writeLink">
    <a href="/bbs/write.bbs?pageNum=${pageNum}">글쓰기</a>
</div>
cs


  그리고 writeForm.jsp 코드 내에 hidden으로 받는 데이터들을 제거한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
    <form action="/bbs/write.bbs" method="post">
        <div id="contentForm">
            <input type="hidden" name="pageNum" value="${pageNum}">
 
            <!-- 컨트롤러에서 DTO로 받을 때 400에러(Bad Request) 뜬다. -->
            <%-- 
                <input type="hidden" name="depth" value="${article.depth}">
                <input type="hidden" name="pos" value="${article.pos}">
                <input type="hidden" name="groupId" value="${article.groupId}">
            --%>
 
            ...
cs


  errorPage.jsp도 아래와 같이 400에러를 처리할 수 있도록 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Error Page</title>
</head>
<body>
    <c:if test="${requestScope['javax.servlet.error.status_code'] == 400}">
        <p>400(잘못된 요청, Bad Request): 서버가 요청의 구문을 인식하지 못했다.</p>
    </c:if>
    <c:if test="${requestScope['javax.servlet.error.status_code'] == 404}">
        <p>404(찾을 수 없음): 서버가 요청한 페이지를 찾을 수 없습니다.</p>
        <p>예를 들어 서버에 존재하지 않는 페이지에 대한 요청이 있을 경우 서버는 이 코드를 제공합니다.</p>
    </c:if>
    <c:if test="${requestScope['javax.servlet.error.status_code'] == 500}">
        <p>500(내부 서버 오류): 서버에 오류가 발생하여 요청을 수행할 수 없습니다.</p>
    </c:if>
    <a href="/bbs/list.bbs?pageNum=1">돌아가기</a>
</body>
</html>
cs



[「자바 웹 개발 완벽가이드, 위키북스」 교재 참고]

[Spring bean이란? 참고1]

[Spring Framework: annotation 정리 참고2]

[DAO랑 service랑 차이가 없던데 무슨차이죠? 참고3]

[[Spring] css, js 파일 Include 하는 법 참고4]