Spring4에 tiles3 레이아웃 적용하기.

들어가기

이 글은 Spring 프레임워크로 개인프로젝트를 만들기위해 tiles3를 적용하는 과정을 정리한 글이다.

개발환경
windows10
spring4
java1.8
tomcat9
tiles3
jsp + bootstrap

사실 tiles3 글을 쓰고 있지만, 난 tiles3를 포기하고 Handlebars 나 Thymeleaf를 좀 파보고 적당한 것을 골라 이번 스프링 프로젝트의 레이아웃으로 적용할 예정이다.

tiles3를 적용하고 내가 경험한 문제점은 특정 tile의 갱신, 즉 메뉴를 클릭했을때 컨텐츠 부분만 갱신하게 만들기 위해서 불필요한 자바스크립트 로직이 들어가게 된다.

더 큰 문제는 자바스크립트 함수를 이용하여 콘텐츠 영역만 갱신하게 되었을때, 내가 적용한 bootstrap 테마가 정상 동작하지 않는다.

아마 렌더링 될때 뷰포트 등에서 문제가 발생하는 듯하다.(내가 공부가 부족해서 스스로 해결하기 포기했다.)

다만 나중에 tiles를 쓸일이 생길수도 있기 때문에 이렇게 기록한다. (bootstrap을 쓰지 않던가, 컨텐츠 영역만 갱신되게 할 필요가 없다면 충분히 쓸만하다고 생각하다.)

tiles 레이아웃 적용전 유의점 tiles는 레이아웃을 나눌수 있게 해주지만, 컨트롤러가 타일즈 뷰를 렌더링할 때 실질적으로 바뀌는 부분과 바뀌지 않는 부분 모두 새로 랜드링 한다. 따라서 레이아웃의 특정 부분만, 예를 들어 body나 content 부분만 갱신하기를 원한다면 tiles레이아웃을 피하길 바란다.


Spring mvc 프로젝트에 tiles3 적용하기

1.pom.xml에 tiles3 라이브러리 추가하기.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- Tiles Core -->
<!-- http://mvnrepository.com/artifact/org.apache.tiles/tiles-core -->
<dependency>
<groupId>org.apache.tiles</groupId>
<artifactId>tiles-core</artifactId>
<version>3.0.7</version>
</dependency>

<!-- Tiles Servlet -->
<!-- http://mvnrepository.com/artifact/org.apache.tiles/tiles-servlet -->
<dependency>
<groupId>org.apache.tiles</groupId>
<artifactId>tiles-servlet</artifactId>
<version>3.0.7</version>
</dependency>

<!-- Tiles JSP -->
<!-- http://mvnrepository.com/artifact/org.apache.tiles/tiles-jsp -->
<dependency>
<groupId>org.apache.tiles</groupId>
<artifactId>tiles-jsp</artifactId>
<version>3.0.7</version>
</dependency>

2.servlet-context.xml 파일 수정하기.

일단 기존에 사용하던 InternalResourceViewResolver의 order를 2로 변경한다. 그리고 tielsViewResolver, tilesConfigurer를 추가한다. tiles.xml 파일은 tiles 템플릿을 설정하는 설정 파일이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 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:property name="order" value="2" /> <!-- tiles3 설정 -->
</beans:bean>


<!-- Tiles 뷰 리졸버 -->
<beans:bean id="tielsViewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<beans:property name="viewClass" value="org.springframework.web.servlet.view.tiles3.TilesView" />
<beans:property name="order" value="1" />
</beans:bean>

<!-- Tiles 설정 파일 -->
<beans:bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<beans:property name="definitions">
<beans:list>
<beans:value>/WEB-INF/tiles/tiles.xml</beans:value>
</beans:list>
</beans:property>
</beans:bean>

3. /WEB-INF/tiles/tiles.xml 을 생성하자.

그냥 참고용 내 파일트리

위에서 말했듯이 tiles의 레이아웃을 정의하는 설정 파일이다. 내 경우에는 아래 처럼 설정했다. biz-template 와 sample-template 라는 두 종류의 tiles템플릿을 동시에 사용하게 설정했다. 사실 템플리 jsp는 biz_template.jsp로 동일하지만 메뉴정보를 달리 하기 위해서 템플릿을 두개로 나눠놨다.

biz_template.jsp는 레이아웃 구조를 잡고 있는 중심 jsp 파일이다. head, left-side, footer는 기본적으로 변하지 않는 고정된 부분이고, content 부분이 변경되는 부분이다.

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN" "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<definition name="biz-template" template="/WEB-INF/tiles/layouts/biz_template.jsp">
<put-attribute name="title" value="Mokaboard 메인페이지" />
<put-attribute name="header" value="/WEB-INF/tiles/components/biz/header.jsp" />
<put-attribute name="left-side" value="/WEB-INF/tiles/components/biz/left_side.jsp" />
<put-attribute name="footer" value="/WEB-INF/tiles/components/biz/footer.jsp" />
</definition>
<definition name="biz/*" extends="biz-template">
<put-attribute name="content" value="/WEB-INF/views/{1}.jsp" />
</definition>
<definition name="biz/*/*" extends="biz-template">
<put-attribute name="content" value="/WEB-INF/views/{1}/{2}.jsp" />
</definition>

<definition name="sample-template" template="/WEB-INF/tiles/layouts/biz_template.jsp">
<put-attribute name="title" value="sample 메인페이지" />
<put-attribute name="header" value="/WEB-INF/tiles/components/biz/header.jsp" />
<put-attribute name="left-side" value="/WEB-INF/tiles/components/sample/left_side.jsp" />
<put-attribute name="footer" value="/WEB-INF/tiles/components/biz/footer.jsp" />
</definition>
<definition name="sample-t/*" extends="sample-template">
<put-attribute name="content" value="/WEB-INF/views/sample/{1}.jsp" cascade="true" />
</definition>
<definition name="sample-t/*/*" extends="sample-template">
<put-attribute name="content" value="/WEB-INF/views/sample/{1}/{2}.jsp" cascade="true" />
</definition>
</tiles-definitions>

4.tiles에서 사용하는 template.jsp 를 만들자.

아래 내 biz_template.jsp 템플릿 파일을 보면 bootstrap 테마를 적용해서 상당히 길다. 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
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
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>

<!DOCTYPE html>
<!--
This is a starter template page. Use this page to start your new project from
scratch. This page gets rid of all links and provides the needed markup only.
-->
<html lang="kr">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title><tiles:getAsString name="title" /></title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">

<link rel="stylesheet" href="<c:url value='/resources/bower_components/bootstrap/dist/css/bootstrap.min.css' />">
<!-- Font Awesome -->
<link rel="stylesheet" href="<c:url value='/resources/bower_components/font-awesome/css/font-awesome.min.css' />">
<!-- Ionicons -->
<link rel="stylesheet" href="<c:url value='/resources/bower_components/Ionicons/css/ionicons.min.css' />">
<!-- Theme style -->
<link rel="stylesheet" href="<c:url value='/resources/dist/css/AdminLTE.min.css' />">
<!-- AdminLTE Skins. We have chosen the skin-blue for this starter
page. However, you can choose any other skin. Make sure you
apply the skin class to the body tag so the changes take effect. -->
<link rel="stylesheet" href="<c:url value='/resources/dist/css/skins/skin-blue.min.css' />">

<!-- Google Font -->
<link rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
</head>
<!--
BODY TAG OPTIONS:
=================
Apply one or more of the following classes to get the
desired effect
|---------------------------------------------------------|
| SKINS | skin-blue |
| | skin-black |
| | skin-purple |
| | skin-yellow |
| | skin-red |
| | skin-green |
|---------------------------------------------------------|
|LAYOUT OPTIONS | fixed |
| | layout-boxed |
| | layout-top-nav |
| | sidebar-collapse |
| | sidebar-mini |
|---------------------------------------------------------|
-->
<body class="hold-transition skin-blue sidebar-mini">
<div class="wrapper">

<tiles:insertAttribute name="header" />

<tiles:insertAttribute name="left-side" />

<!-- content 위치 -->
<div id="bodyTile"><tiles:insertAttribute name="content" /></div>

<!-- mainfooter 위치 -->
<tiles:insertAttribute name="footer" />

<!-- Control Sidebar 모바일 모드일때 사이즈 변경을 위한 바 -->
<aside class="control-sidebar control-sidebar-dark">
<!-- Create the tabs -->
<ul class="nav nav-tabs nav-justified control-sidebar-tabs">
<li class="active"><a href="#control-sidebar-home-tab" data-toggle="tab"><i class="fa fa-home"></i></a></li>
<li><a href="#control-sidebar-settings-tab" data-toggle="tab"><i class="fa fa-gears"></i></a></li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<!-- Home tab content -->
<div class="tab-pane active" id="control-sidebar-home-tab">
<h3 class="control-sidebar-heading">Recent Activity</h3>
<ul class="control-sidebar-menu">
<li>
<a href="javascript:;">
<i class="menu-icon fa fa-birthday-cake bg-red"></i>

<div class="menu-info">
<h4 class="control-sidebar-subheading">Langdon's Birthday</h4>

<p>Will be 23 on April 24th</p>
</div>
</a>
</li>
</ul>
<!-- /.control-sidebar-menu -->

<h3 class="control-sidebar-heading">Tasks Progress</h3>
<ul class="control-sidebar-menu">
<li>
<a href="javascript:;">
<h4 class="control-sidebar-subheading">
Custom Template Design
<span class="pull-right-container">
<span class="label label-danger pull-right">70%</span>
</span>
</h4>

<div class="progress progress-xxs">
<div class="progress-bar progress-bar-danger" style="width: 70%"></div>
</div>
</a>
</li>
</ul>
<!-- /.control-sidebar-menu -->

</div>
<!-- /.tab-pane -->
<!-- Stats tab content -->
<div class="tab-pane" id="control-sidebar-stats-tab">Stats Tab Content</div>
<!-- /.tab-pane -->
<!-- Settings tab content -->
<div class="tab-pane" id="control-sidebar-settings-tab">
<form method="post">
<h3 class="control-sidebar-heading">General Settings</h3>

<div class="form-group">
<label class="control-sidebar-subheading">
Report panel usage
<input type="checkbox" class="pull-right" checked>
</label>

<p>
Some information about this general settings option
</p>
</div>
<!-- /.form-group -->
</form>
</div>
<!-- /.tab-pane -->
</div>
</aside>
<!-- /.control-sidebar -->
<!-- Add the sidebar's background. This div must be placed
immediately after the control sidebar -->
<div class="control-sidebar-bg"></div>
</div>
<!-- ./wrapper -->

<!-- REQUIRED JS SCRIPTS -->

<!-- jQuery 3 -->
<script src="<c:url value='/resources/bower_components/jquery/dist/jquery.min.js' />"></script>
<!-- Bootstrap 3.3.7 -->
<script src="<c:url value='/resources/bower_components/bootstrap/dist/js/bootstrap.min.js' />"></script>
<!-- AdminLTE App -->
<script src="<c:url value='/resources/dist/js/adminlte.min.js' />"></script>

<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->


<!-- Optionally, you can add Slimscroll and FastClick plugins.
Both of these plugins are recommended to enhance the
user experience. -->
</body>
</html>

그래서 tiles 관련된것만 정리해 보자.

template.jsp 파일 최 상단에는 아래 내용이 추가 되어야 한다.

1
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>

아래는 내용은 tiles를 통해 template.jsp의 title내용을 변경하는 부분이다. tiles는 동적으로 페이지의 구조를 바꾼다. 따라서 때에 따라 페이지의 title내용을 변경해줄 필요가 있다.

1
<title><tiles:getAsString name="title" /></title>

그리고 실제로 tiles 조각들이 채워질 공간은 아래이다. header, left-side, content, footer 총 4부분이다. left-side에 메뉴가 존재하며 메뉴를 클릭했을때 content 부분이 교체되는 형태이다. 주의할 점은 content tiles부분이 bodyTile이라는 div 태그로 감싸여 있는데, 이것은 left-side에서 메뉴를 클릭했을때 header, left-side, content, footer 모든 타일들이 refresh가 되는 것이 아니라 content 부분만 갱신하기 위한 태그이다.

1
2
3
4
5
6
7
8
9
<tiles:insertAttribute name="header" />

<tiles:insertAttribute name="left-side" />

<!-- content 위치 -->
<div id="bodyTile"><tiles:insertAttribute name="content" /></div>

<!-- mainfooter 위치 -->
<tiles:insertAttribute name="footer" />

4.tiles 템플릿을 채워줄 공통 타일 header.jsp, footer.jsp, left_side.jsp를 생성하자.

header.jsp, footer.jsp는 생략 하겠다.

실질적인 메뉴가 되는 left-side.jsp 는 아래와 같다.

역시 bootstrap테마가 있어서 소스가 길다.

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
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!-- Left side column. contains the logo and sidebar -->
<aside class="main-sidebar">

<!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar">

<!-- Sidebar user panel (optional) -->
<div class="user-panel">
<div class="pull-left image">
<img src="<c:url value='/resources/dist/img/user2-160x160.jpg' />" class="img-circle" alt="User Image">
</div>
<div class="pull-left info">
<p>Alexander Pierce</p>
<!-- Status -->
<a href="#"><i class="fa fa-circle text-success"></i> Online</a>
</div>
</div>

<!-- search form (Optional) -->
<form action="#" method="get" class="sidebar-form">
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Search...">
<span class="input-group-btn">
<button type="submit" name="search" id="search-btn" class="btn btn-flat"><i class="fa fa-search"></i>
</button>
</span>
</div>
</form>
<!-- /.search form -->

<!-- Sidebar Menu -->
<ul class="sidebar-menu" data-widget="tree">
<li class="header">HEADER</li>
<!-- Optionally, you can add icons to the links -->
<!-- <li class="active"><a href="#"><i class="fa fa-link"></i> <span>Link</span></a></li>
<li><a href="#"><i class="fa fa-link"></i> <span>Another Link</span></a></li> -->

<li class="treeview">
<a href="#">
<i class="fa fa-share"></i> <span>샘플페이지</span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu">
<!-- <li><a href="#"><i class="fa fa-circle-o"></i> Level One</a></li> -->
<li class="treeview">
<a href="#"><i class="fa fa-circle-o"></i> 게시판
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu">
<li class="active">
<a href="#" onClick="switchContent('/sample/board/read2');"><i class="fa fa-circle-o"></i> 조회2</a>
<a href="/sample/board/register" ><i class="fa fa-circle-o"></i> 입력</a>
<a href="/sample/board/mod" ><i class="fa fa-circle-o"></i> 수정</a>
<a href="/sample/board/read" ><i class="fa fa-circle-o"></i> 조회</a>
<a href="/sample/board/list" ><i class="fa fa-circle-o"></i> 리스트</a>
</li>
</ul>
</li>
</ul>
</li>

</ul>
<!-- /.sidebar-menu -->
</section>
<!-- /.sidebar -->
</aside>

<script>
/* 메뉴 선택시 컨텐츠만 갱신되게 하는 함수 */
function switchContent(url){
$('#bodyTile').children().remove();
$('#bodyTile').load(url);

}

</script>

위 소스를 보면

1
2
3
4
5
<a href="#" onClick="switchContent('/sample/board/read2');"><i class="fa fa-circle-o"></i> 조회2</a>
<a href="/sample/board/register" ><i class="fa fa-circle-o"></i> 입력</a>
<a href="/sample/board/mod" ><i class="fa fa-circle-o"></i> 수정</a>
<a href="/sample/board/read" ><i class="fa fa-circle-o"></i> 조회</a>
<a href="/sample/board/list" ><i class="fa fa-circle-o"></i> 리스트</a>

이 부분이 메뉴 링크 부분이다. switchContent 함수는 스프링 컨트롤러에게 content 페이지를 요청할때, 모든 타일 header, footer, left-side, content들을 refresh하는 것이 아니라 content 타일만 갱신하기 위한 함수이다. switchContent 자바스크립트 함수를 보면 bodyTile div 태그의 내부를 지우고 대신 content 페이지를 요청하여 렌더링 결과인 html을 대신 채워준다. 위 처럼 하면 content 타일만 refresh가 된다.

5.Controller

tiles에서 가장 중요하다고 생각하는 부분은 tiles.xml과 controller이다. 그 이유는 controller가 뷰 리졸버로 전달하는 명칭에 의해 해당 뷰를 타일즈를 통해 렌더링 할 것인가의 여부, 만약 타일즈를 통해 렌더링 한다면 어떤 타일즈 템플릿을 적용하는 지가 결정 되기 때문이다.

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
package com.hanumoka.sample.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/sample/board/*")
public class SampleBoardController {

private static final Logger logger = LoggerFactory.getLogger(SampleBoardController.class);

@RequestMapping(value="/list", method = RequestMethod.GET)
public String list(Model model) throws Exception{
logger.info("샘플 게시판 list...");

return "sample-t/board/list";
}

@RequestMapping(value="/register", method = RequestMethod.GET)
public String register(Model model) throws Exception{
logger.info("샘플 게시판 register...");

return "sample-t/board/register";
}

@RequestMapping(value="/read", method = RequestMethod.GET)
public String read(Model model) throws Exception{
logger.info("샘플 게시판 read...");

return "sample-t/board/read";
}

@RequestMapping(value="/mod", method = RequestMethod.GET)
public String mod(Model model) throws Exception{
logger.info("샘플 게시판 mod...");

return "sample-t/board/mod";
}

//이녀석은 바디이기때문에 템플릿을 거치지 않게 하자.
@RequestMapping(value="/read2", method = RequestMethod.GET)
public String read2(Model model) throws Exception{
logger.info("샘플 게시판 read2...");

return "sample/board/read";
}
}

위 컨트롤러의 대부분 메소드는 보통 sample-t 로 시작하는 뷰를 뷰 리졸버에게 전달하고 있다. 이 경우 tiles.xml에 아래 내용이 동작하게 된다.

1
2
3
4
5
6
<definition name="sample-t/*" extends="sample-template">
<put-attribute name="content" value="/WEB-INF/views/sample/{1}.jsp" cascade="true" />
</definition>
<definition name="sample-t/*/*" extends="sample-template">
<put-attribute name="content" value="/WEB-INF/views/sample/{1}/{2}.jsp" cascade="true" />
</definition>

위 컨트롤러의 마지막 read2를 보면 이 녀석만 독특하게 sample로 시작하는 뷰를 찾는다. 내 tiles.xml 파일을 보면 sample로 시작하는 타일즈 템플릿은 없다. 즉 이 녀석은 타일즈 리졸버를 거치지 않고 스프링의 내장 리졸버에 의해 렌더링 된 뷰가 화면단에 전달 된다.

위 글을 자세히 읽었다면 이 부분이 무엇을 의미 하는지 알 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
//1.left-side.jsp에서 메뉴를 클릭
<a href="#" onClick="switchContent('/sample/board/read2');"><i class="fa fa-circle-o"></i> 조회2</a>

//2.아래 자바스크립트가 대신 컨트롤러에 /sample/board/read2을 요청하여 그 결과물(타일즈가 적용되지 않은 내장 뷰 리졸버 html결과물)을 받아
function switchContent(url){
$('#bodyTile').children().remove();
$('#bodyTile').load(url);

}

//3.template-biz.jsp 파일의 bodyTile태그 내용물을 대체한다.
<!-- content 위치 -->
<div id="bodyTile"><tiles:insertAttribute name="content" /></div>

일바적인 tiles 동작, 재대로 동작하지만 왼쪽 아코디언 메뉴가 갱신된다. 열려있던 메뉴가 접혀버린 것이다.

/read2 tiles 동작, content 부분만 갱신되지만 bootstrap이 동작하지 않는다. 열려있던 메뉴가 접혀지지 않았다. 노란색 부분을 다 채워붜야 하는데 채우다가 만다.

개인적인 생각으로, 만약 레이아웃의 여러 부분중 특정 부분만 갱신되게 해야 한다면 tiles 레이아웃은 비추한다.

필요에 따라 컨텐츠만 갱신하는 로직을 제거해서 사용하면 될듯 하다.

끝!