Spring controller와 thymeleaf뷰간 데이터 전달방식 정리(게시판 crud)

실습환경

개발환경
windows10(64)
spring4
java1.8
tomcat9

들어가기

이 글은 spring 개인 프로젝트에 thymeleaf를 적용함에 있어서, spring controller와 thymeleaf간의 데이터 전달 방식을 정리하기 위한 글이다. 게시판 crud 예제를 통해서 설명할 것이다.

게시판 글 리스트 조회

게시판 글 리스트 조회 화면

1.controller

1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/listAll", method = RequestMethod.GET)
public String listAll(Locale locale, Model model) throws Exception {

logger.info("show all list........");

model.addAttribute("list", service.listAll());

return "/samples/board/list";
}

컨트롤러에서는 게시판글을 조회해서 글 vo를 List로 담아 model에 담는다.

2.list.html

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
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
data-layout-decorate="~{samples/layout/sampleLayout}"
>

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
</th:block>


<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
<script th:inline="javascript">
/*<![CDATA[*/
$(function () {

var result =/*[[${msg}]]*/ 'default';

if(result == 'success'){
alert("처리가 완료되었습니다.");
}

$('#example1').DataTable({
'ordering' : false, /* 자동으로 정렬되는 것을 막자 */
});

/* $('#example2').DataTable({
'paging' : true,
'lengthChange': false,
'searching' : false,
'ordering' : true,
'info' : true,
'autoWidth' : false
}); */

});

/*]]>*/
</script>
</th:block>



<div layout:fragment="content">

<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
게시판 리스트
<small>advanced tables</small>
</h1>
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="#">Tables</a></li>
<li class="active">Data tables</li>
</ol>
</section>
<!-- Main content -->
<section class="content">
<div class="row">
<div class="col-xs-12">

<div class="box">
<div class="box-header">
<h3 class="box-title">List All Page</h3>
</div>
<!-- /.box-header -->
<div class="box-body">
<table id="example1" class="table table-bordered table-striped">
<thead>
<tr>
<th>BNO</th>
<th>TITLE</th>
<th>WRITER</th>
<th>REGDATE</th>
<th>VIEWCNT</th>
</tr>
</thead>
<tbody>
<tr th:each="boardVO : ${list}">
<td th:text="${boardVO.bno}">BNO</td>
<td><a th:href="@{/samplehome/board/read(bno=${boardVO.bno})}" th:text="${boardVO.title}">TITLE</a></td>
<td th:text="${boardVO.writer}">WRITER</td>
<td th:text="${#dates.format(boardVO.regdate, 'yyyy-MM-dd HH:mm')}">REGDATE</td>
<td th:text="${boardVO.viewcnt}">VIEWCNT</td>
</tr>
</tbody>
<tfoot>
<tr>
<th>BNO</th>
<th>TITLE</th>
<th>WRITER</th>
<th>REGDATE</th>
<th>VIEWCNT</th>
</tr>
</tfoot>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->

</div>


</html>

위 html은 부트스트랩 테마때문에 좀 길긴 하다. 중요한 부분을 추리면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<tbody>
<!-- th:each 는 thymeleaf의 반복문이다. list라는 spring controller부터 전달 받은 객체를 boardVO라는 이름으로 접근한다. -->
<tr th:each="boardVO : ${list}">
<td th:text="${boardVO.bno}">BNO</td>
<!-- 글의 제목을 눌렀을때 글 보기페이지로 이동하기 위한 링크 -->
<!-- /samplehome/board/read(bno=${boardVO.bno}) 는 /samplehome/board/read?bno=${boardVO.bno} 와 같은 의미이다. -->
<td><a th:href="@{/samplehome/board/read(bno=${boardVO.bno})}" th:text="${boardVO.title}">TITLE</a></td>
<td th:text="${boardVO.writer}">WRITER</td>
<!-- thymeleaf는 dataes.format을 통해 데이터 포멧 처리를 도와준다. -->
<td th:text="${#dates.format(boardVO.regdate, 'yyyy-MM-dd HH:mm')}">REGDATE</td>
<td th:text="${boardVO.viewcnt}">VIEWCNT</td>
</tr>
</tbody>

게시판 글 한개 조회화면

게시판 글 리스트에서 특정 글을 선택했을때 나오는 화면.

게시판 글 한개 조회화면

1.controller

1
2
3
4
5
6
7
8
@RequestMapping(value = "/read", method = RequestMethod.GET)
public String read(@RequestParam("bno") int bno, Model model) throws Exception {

logger.info("sampleboard read bno:" + bno);

model.addAttribute("boardVO", service.read(bno));
return "/samples/board/read";
}

특정 게시글 하나를 db에서 조회하여 model에 boardVO라는 이름으로 담아 넘긴다.

2.read.html

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
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
data-layout-decorate="~{samples/layout/sampleLayout}"
>

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
</th:block>

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
<script th:inline="javascript">
/*<![CDATA[*/
$(function () {

var formObj = $("form[role='form']");
console.log(formObj);

$(".btn-warning").on("click", function(){
formObj.attr("action", "/samplehome/board/modify");
formObj.attr("method", "get");
formObj.submit();
});

$(".btn-danger").on("click", function(){
formObj.attr("action", "/samplehome/board/remove");
formObj.submit();
});

$(".btn-primary").on("click", function(){
formObj.attr("action", "/samplehome/board/listAll");
formObj.attr("method", "get");
formObj.submit();
});

});

/*]]>*/
</script>
</th:block>

<div layout:fragment="content">
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
게시판 읽기
<small>Optional description</small>
</h1>
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-dashboard"></i> 게시판</a></li>
<li class="active">read</li>
</ol>
</section>

<!-- Main content -->
<section class="content container-fluid">

<!--------------------------
| Your Page Content Here |
-------------------------->

<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">READ BOARD</h3>
</div>
<!-- /.box-header -->
<!-- form start -->
<form role="form" method="post">
<div class="box-body" th:object="${boardVO}">
<input th:field="*{bno}" type="hidden" >
<div class="form-group">
<label for="exampleInputBoardTitle">Title</label>
<input th:value="*{title}" type="text" class="form-control" id="exampleInputBoardTitle" readonly="readonly">
</div>
<div class="form-group">
<label>Content</label>
<textarea th:inline="text" class="form-control" rows="3" readonly="readonly">[[*{content}]]</textarea>
</div>
<div class="form-group">
<label for="exampleInputBoardWriter">Writer</label>
<input th:value="*{writer}" type="text" class="form-control" id="exampleInputBoardWriter" readonly="readonly">
</div>
</div>
<!-- /.box-body -->

<div class="box-footer">
<button type="submit" class="btn btn-warning">Modify</button>
<button type="submit" class="btn btn-danger">Remove</button>
<button type="submit" class="btn btn-primary">ListAll</button>
</div>
</form>
</div>
<!-- /.box -->

</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
</div>
</html>

역시 부트스트랩 테마 때문에 좀 길다. 데이터가 맵핑되는 부분을 추려보겠다. form 태그 내부를 보면 아래처럼 controller으로 부터 전달되는 데이터를 받는 부분이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- th:object="${boardVO}" 이렇게 선언하면 내부 태크에서 프로퍼티로 값 접근이 가능하다. -->
<div class="box-body" th:object="${boardVO}">
<!-- th:field="*{bno}" 는 boardVO.bno값을 꺼내 자동으로 값을 설정하며, 이 form이 get이나 post로 컨트롤러에 전달되면 bno라는 이름으로 값이 전달된다. -->
<input th:field="*{bno}" type="hidden" >
<div class="form-group">
<label for="exampleInputBoardTitle">Title</label>
<!-- th:value도 th:field는 boardVO.title값을 꺼내 자동으로 태그에 값을 설정한다, 그러나 이 form 태그가 get이나 post로 controller에 전달 되었을때 title이라는 이름으로 값전달이 되지 않는다. 만약 input 태그에 name 어트리뷰트가 지정되어 있다면, 전달이 된다. -->
<input th:value="*{title}" type="text" class="form-control" id="exampleInputBoardTitle" readonly="readonly">
</div>
<div class="form-group">
<label>Content</label>
<!-- 컨트롤러에서 전달된 값을 textarea에 값을 채우기 위해서는 아래처럼 설정해야 한다. th:field로 설정이 가능하긴 하나, 이렇게 설정한 경우 앞서 말했듯이 form액션이 발생했을때 아래 texteare 태그에 name 어트리뷰트가 없지만 자동으로 컨트롤러로 값이 전달된다. -->
<textarea th:inline="text" class="form-control" rows="3" readonly="readonly">[[*{content}]]</textarea>
</div>
<div class="form-group">
<label for="exampleInputBoardWriter">Writer</label>
<input th:value="*{writer}" type="text" class="form-control" id="exampleInputBoardWriter" readonly="readonly">
</div>
</div>

게시글 수정 화면

게시판 글 수정화면

1.controller

게시글 수정화면 (GET)이동 함수와, 실제 게시글 수정(POST)을 해주는 함수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RequestMapping(value = "/modify", method = RequestMethod.GET)
public String modifyGET(@RequestParam("bno") int bno, Model model) throws Exception {

logger.info("sampleboard read bno:" + bno);

model.addAttribute("boardVO", service.read(bno));
return "/samples/board/modify";
}

@RequestMapping(value = "/modify", method = RequestMethod.POST)
public String modifyPOST(BoardVO board, RedirectAttributes rttr) throws Exception {
logger.info("modify post......");
logger.info(board.toString());

service.modify(board);

rttr.addFlashAttribute("msg", "success");

return "redirect:/samplehome/board/listAll";
}

2.modify.html

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
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
data-layout-decorate="~{samples/layout/sampleLayout}"
>

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
</th:block>

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
<script th:inline="javascript">
/*<![CDATA[*/
$(function () {

var formObj = $("form[role='form']");
console.log(formObj);

$(".btn-primary").on("click", function(){
formObj.attr("action", "/samplehome/board/modify");
formObj.attr("method", "post");
formObj.submit();
});

$(".btn-warning").on("click", function(){
formObj.attr("action", "/samplehome/board/listAll");
formObj.attr("method", "get");
formObj.submit();
});


});

/*]]>*/
</script>
</th:block>

<div layout:fragment="content">
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
게시판 수정
<small>Optional description</small>
</h1>
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-dashboard"></i> 게시판</a></li>
<li class="active">modify</li>
</ol>
</section>

<!-- Main content -->
<section class="content container-fluid">

<!--------------------------
| Your Page Content Here |
-------------------------->

<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">MODIFY BOARD</h3>
</div>
<!-- /.box-header -->
<!-- form start -->
<form role="form" method="post">
<div class="box-body" th:object="${boardVO}">
<input th:field="*{bno}" type="hidden" >
<div class="form-group">
<label for="exampleInputBoardTitle">Title</label>
<input th:field="*{title}" type="text" class="form-control" id="exampleInputBoardTitle">
</div>
<div class="form-group">
<label>Content</label>
<textarea th:field="*{content}" class="form-control" rows="3"></textarea>
</div>
<div class="form-group">
<label for="exampleInputBoardWriter">Writer</label>
<input th:field="*{writer}" type="text" class="form-control" id="exampleInputBoardWriter">
</div>
</div>
<!-- /.box-body -->

<div class="box-footer">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="submit" class="btn btn-warning">Cancle</button>
</div>
</form>
</div>
<!-- /.box -->

</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
</div>
</html>

게시글 수정화면은 read와 달리 모든 값을 다 서버로 전달해야 하므로 th:field로 모든 값을 처리한 것을 확인 할수 있다.

마무리 정리

일단 html 태그에 name 어트리뷰트가 불 필요 하다. name 어트리뷰트를 사용할수 있지만, 햇갈리니 안쓰는게 좋아보인다. 서버에서 단순히 thymeleaf 뷰 html 파일에 값을 전달할때 th:value, th:field를 사용 할수 있다. name 어트리뷰트가 없는경우 form 액션발생시 th:value는 컨트롤러에 값을 전달하지 않지만, th:field는 자동으로 값을 컨트롤러에 전달한다.