Spring Thymeleaf(2버전) 적용해보기 + 레이아웃구성(thymeleaf-layout-dialect 1버전)

실습환경

개발환경
windows10
spring4
java1.8
tomcat9

들어가기

개인 스프링 프로젝트에서 사용할 템플릿 엔진을 찾고있다. 저번엔 JSP + tiles3를 사용해 보려했으나, 영 마음에 안들어 접었었다. 조금 찾아보니 Spring boot에서 공식 지원(민다고?)하는 Thymeleaf(타임리프?)라는 텍스트 템플릿 엔진이라는 것을 알게되었다.


JSP의 한계

순수한 HTML이 아닌것이 jsp의 한계이다. JSP는 java server page 로 서버에서 렌더링 해주는 서버 페이지이다. 즉 HTML에 JSP의 고유한 문법인 스크립틀릿 등의 JSP 태그 라이브러리가 침투적으로 존재 한다. JSP의 기술은 서블릿과 더불어 강력한 기술이지만, 아니 강력한 기술이었다.

JSP는 HTML에 서버의 기능적 관련 기술들이 침투함으로써 단독으로 프론트 엔드 개발을 할수 있는 기회를 잃게 된다. 요즘같이 강력한 프론트엔드 프레임워크가 있는 시기에 점차 JSP는 점차 입지를 잃어 가는듯 하다.


Thymeleaf란?

타임리프(Thymeleaf)는 Freemarker, Velocity와 같은 View Template Engine 입니다. 하지만 타임리프는 다른 템플릿 엔진들과는 차별화되는 장점을 지니고 있습니다.

  • 서버상에서 동작하지 않아도 된다.
  • 전체적인 마크업 구조를 흐트려트리지 않는다.

Thymeleaf 템플릿은 형태가 자연스럽고 태그 라이브러리에 의존하지 않는다. 또한 HTML이 사용 가능한 곳에서는 언제든 편집하거나 렌더링 할 수 있다. 그리고 서블릿 스펙과 관계가 없으므로 Thymeleaf 템플릿은 JSP가 감히 사용될 수 없는 곳에서도 사용된다.


Thymeleaf 장점

tag library를 사용한 JSP의 예

1
<form:inputText name="userName" value="${user.name}" />

thymeleaf standard dialect를 사용하는 예

1
<input type="text " name="userName" value="James Carrot" th:value="${user.name}" />

요즘 웹 개발에서 필수적이라 생각되는 것은 프론트엔드와 백엔드의 독립된 개발이다. 위에 코드를 잘 보면 jsp tag library를 사용하였을 때 만약 서버에서 처리해주지 않는다면 해당 태그가 제대로 표시되지 않는다. 반면 아래 thymeleaf standard dialect를 사용 할 경우를 보면 th: 로 시작하는 부분이 처리되지 않더라도 완벽하게 html input 태그의 형태를 가지고 있다.

thymeleaf도 jsp처럼 html에 침투적인 기술이다. BUT HTML의 태그를 손대는 것이 아니라 HTML태그 내부의 어트리뷰트를 이용하여 기술을 구현하고 있다. 이 점이 thymeleaf의 장점이다. 브라우저는 HTML 태그내부의 어트리뷰트에 관대하기 때문이다. 내 기억이 맞는지 모르겠으나, HTML5에서는 커스텀 어트리뷰트를 공식 지원하는것으로 기억하고 있다. 따라서 디자이너는 was의 상태와 관계없이 단독으로 html을 개발할수 있게 된다.


Spring4 mvc lagacy 프로젝트에 Thymeleaf 적용하고 동작확인하기

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

현재 3버전이 나와있지만, 나도 처음 접하고 공부하는 중이라 2버전을 기준으로 실습했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- Template Engine 2 버전 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>

<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring4</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>

<!-- thymeleaf의 레이아웃 추가 라이브러리 -->
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
<version>1.3.1</version>
</dependency>

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

일단 기존의 jsp에서 사용하던 InternalResourceViewResolver를 주석처리한다.

1
2
3
4
5
<!-- 
<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>
-->

스프링에서 Thymeleaf를 사용하기 위해서 Thymeleaf-Spring 통합을 활성화 시키는 세 개의 빈을 설정해야한다. 1.논리적 뷰 이름으로 Thymeleaf 템플릿 뷰를 결정하는 ThymeleafViewResolver 2.템플릿을 처리하고 결과를 렌더링 하는 SpringTemplateEngine 3.Thymeleaf 템플릿을 불러오는 TemplateResolver

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
<!-- thymeleaf View 설정 -->
<beans:bean id="templateResolver" class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".html" />
<beans:property name="templateMode" value="HTML5" />
<beans:property name="characterEncoding" value="UTF-8" />
<beans:property name="cacheable" value="false" />
</beans:bean>

<!-- thymeleaf laytout을 쓰기위해 3rd Party 추가 -->
<beans:bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine">
<beans:property name="templateResolver" ref="templateResolver" />
<beans:property name="additionalDialects">
<beans:set>
<beans:bean class="nz.net.ultraq.thymeleaf.LayoutDialect" />
</beans:set>
</beans:property>
</beans:bean>

<!-- thymeleaf laytout ViewResolver 설정 -->
<beans:bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
<beans:property name="templateEngine" ref="templateEngine" />
<beans:property name="characterEncoding" value="UTF-8" />
<beans:property name="order" value="1" />
</beans:bean>

사실 아래 additionalDialects는 현재는 필요가 없다. 아래 설정은 thymeleaf-layout-dialect 라는 이름의 Thymeleaf의 레이아웃 관련 라이브러리다. 일단 나중에 레이아웃을 설정하기 위해서 사용할 거라서 미리 셋팅했다.

1
2
3
4
5
<beans:property name="additionalDialects">
<beans:set>
<beans:bean class="nz.net.ultraq.thymeleaf.LayoutDialect" />
</beans:set>
</beans:property>

3.WEB-INF/views/home.html 파일 만들기.

이 html파일은 컨트롤러가 리턴해서 클라이언트에게 보여줄 파일이다. thymeleaf 텍스트 템플릿 엔진이 스프링에게 jsp파일이 아니라 html파일을 렌더링 해서 클라이언트에게 전달하게 도와준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Insert title here</title>
</head>
<body>

Hello World..!!

<br />

<th:block th:text="${serverTime}"></th:block>

</body>
</html>

4.HomeController.java

컨트롤러는 jsp를 사용할때와 별반 다르지 않다.

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

Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

String formattedDate = dateFormat.format(date);

model.addAttribute("serverTime", formattedDate );

return "home";
}

5.동작 확인

tomcat을 시작하고 스프링 컨트롤러가 html을 잘 전달하는지 확인하자.

동작을 확인했다.


ERROR - org.thymeleaf.exceptions.TemplateInputException: Error resolving template "index", template might not exist or might not be accessible by any of the configured Template Resolvers

Thymeleaf를 적용하고 tomcat을 실행할때 발생하는 독특한 에러이다.

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
정보: Server startup in 22985 ms
ERROR: org.thymeleaf.TemplateEngine - [THYMELEAF][http-nio-8080-exec-1] Exception processing template "index": Error resolving template "index", template might not exist or might not be accessible by any of the configured Template Resolvers
8월 01, 2018 5:24:18 오전 org.apache.catalina.core.StandardWrapperValve invoke
심각: Servlet.service() for servlet [appServlet] in context with path [] threw exception [Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateInputException: Error resolving template "index", template might not exist or might not be accessible by any of the configured Template Resolvers] with root cause
org.thymeleaf.exceptions.TemplateInputException: Error resolving template "index", template might not exist or might not be accessible by any of the configured Template Resolvers
at org.thymeleaf.TemplateRepository.getTemplate(TemplateRepository.java:246)
at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1104)
at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1060)
at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1011)
at org.thymeleaf.spring4.view.ThymeleafView.renderFragment(ThymeleafView.java:335)
at org.thymeleaf.spring4.view.ThymeleafView.render(ThymeleafView.java:190)
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1282)
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1037)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:980)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:651)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:417)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:754)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1376)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Unknown Source)

내 경우는 아래처럼 컨트롤러에서 index를 리턴하는 메소드가 설정되어 있는데 정작 index.html이 없다. (index.jsp파일은 있다. jsp로 개발중인 프로젝트를 지금 Thymeleaf를 변경중이기 때문이다.)

위 에러 해결 방법은 아래 index 컨트롤러의 함수를 제거 하거나, 위의 home.html을 복사해서 index.html을 만들어 주면 해결이 된다.

즉 위 에러는 thymeleaf가 선언된 컨트롤러에서 리졸버할 뷰가 존재하지 않을때 발생하는듯 하다.

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET)
public String index(Locale locale, Model model) {

Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

String formattedDate = dateFormat.format(date);

model.addAttribute("serverTime", formattedDate );

return "index";
}


Thymeleaf의 thymeleaf-layout-dialect 이용하여 레이아웃을 구성해보자.

Thymeleaf에서 fragment라는 것을 이용하여 기본적으로 레이아웃을 구성할 수가 있다. th:include를 사용하는 방법이 기본적인 방법인데, 여기서는 thymeleaf-layout-dialect라는 추가 레이아웃 라이브러리를 사용하여 Thymeleaf 레이아웃을 구성해 보겠다.

1.pom.xml 파일에 thymeleaf-layout-dialect 추가하기

위에 미리 설정했지만, 다시 확인차 정리한다.

1
2
3
4
5
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
<version>1.3.1</version>
</dependency>

pom.xml

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

additionalDialects 빈 프로퍼티를 추가하자.

1
2
3
4
5
6
7
8
9
10
11
<!-- thymeleaf laytout을 쓰기위해 3rd Party 추가 -->
<beans:bean id="templateEngine"
class="org.thymeleaf.spring4.SpringTemplateEngine">
<beans:property name="templateResolver"
ref="templateResolver" />
<beans:property name="additionalDialects">
<beans:set>
<beans:bean class="nz.net.ultraq.thymeleaf.LayoutDialect" />
</beans:set>
</beans:property>
</beans:bean>

servlet-context.xml

3.레이아웃의 틀이 될 html 생성하자

이제 템플릿을 구성할 html파일들을 생성해야 한다. 일단 폴더트리구조는 다음과 같다. 노란색만 보면된다.

폴더 트리

WEB-INF/views에 layout폴더를 생성하고 default.html파일을 생성하자. default.html파일을 레이아웃의 틀로 지정할 것이다. default.hmlt파일의 내용은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lagn="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

<head th:replace="fragments/config :: configFragment"></head>

<header th:replace="fragments/header :: headerFragment"></header>

<div layout:fragment="content"></div>

<footer th:replace="fragments/footer :: footerFragment"></footer>

</html>

레이아웃의 구성은 고정된 틀인 config.html, header.html, footer.html 이다. fragments/config의 의미는 WEB-INF/views/fragments/config.html을 사용한다는 의미이고, configFragment의 의미는 WEB-INF/views/fragments/config.html 파일의 아래 태그를 잘라서 가져와 사용한다는 의미이다.

1
2
3
<head th:fragment="configFragment">
...
</head>

content 부분이 실제 컨텐츠가 들어갈 부분이다. 대충 실습을 하면 어떤식으로 돌아가는지 감이 올 것이다.

4.config.html, header.html, footer.html을 생성하자.

WEB-INF/views에 fragments 폴더를 생성하고 config.html, header.html, footer.html 파일들을 생성하자.

config.html 파일은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

<head th:fragment="configFragment">
<meta charset="UTF-8" />
<!-- 공통으로 쓰이는 css파일을넣는다.-->
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" />
<!-- 사용자 CSS 영역이 들어감 -->
<th:block layout:fragment="css"></th:block>

<!-- 공통으로 쓰이는 css파일을넣는다.-->
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<!-- 사용자 스크립트 영역이 들어감 -->
<th:block layout:fragment="script"></th:block>
</head>

</html>

header.html 파일은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<header th:fragment="headerFragment"> 헤더 영역입니다.

<input type="text" />

<br/>

<a th:href="@{/index}">index 페이지 이동</a>

</header>

</html>

footer.html 파일은 아래와 같다.

1
2
3
4
5
6
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<footer th:fragment="footerFragment"> Footer영역입니다. </footer>

</html>

5.content가 될 home.html을 생성하자.

위 예제에서 사용한 home.html을 수정할 생각이다. 경로는 WEB-INF/views 이다. home.html 파일은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layout/default">

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

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
</th:block>

<div layout:fragment="content">
Hello World..!! dd <br />
<th:block th:text="${serverTime}"></th:block>
</div>
</html>

여기서 thymeleaf-layout-dialect을 사용한 레이아웃의 독특한 부분이 있다. layout:decorator="layout/default" 이 것의 의미는 home.html이 layout/default.html 이라는 레이아웃 템플릿을 사용하겠다는 의미이다. 즉 thymeleaf-layout-dialect 레이아웃 템플릿은 스프링 컨트롤러가 응답하는 컨텐츠.html파일에 의해서 레이아웃 템플릿이 결정된다.

이부분은 어쩌면 편해보이기도 하지만, 만약 공통의 템플릿.html파일이 변경되었을때 이 템플릿을 사용하는 수많은 컨텐치.html파일들을 수정해야 하는 수고가 발생할것 같기도하다.(요즘 툴이 좋아서 일괄 편집이 되긴 하지만...)

마지막 컨트롤러는 딱시 위의 컨트롤러와 다르지 않으므로 생략 하겠다.

6.thymeleaf-layout-dialect 레이아웃 동작 확인

thymeleaf 레이아웃 동작 확인


마무리와 문제점

이런 머더퍼커! Thymeleaf는 xhtml 기반으로 작성되어야 하나보다. 즉 html 의 모든 태그는 닫혀야 한다.

즉 아래의 br태그를

1
<br>

아래 처럼 꼭 닫아 주어야 한다.

1
<br/>>

닫아주지 않는다면, 심지어 오류를 뱉어내며 was가 기동조차 하지 않는다.!

br태그 때문에 발생하는 에러

html5기반으로 제작된 테마를 빌려쓸려고 하는데 이것은 큰 문제가 아닐 수 없다.

내 생각에는 Thymeleaf를 접고 다른 템플릿 엔진을 찾아보는것이 좋을거 같다.


Related Posts

"spring4 thymeleaf3 버전 설정"

참고한 자료들

http://wgenius.tistory.com/11 https://blog.outsider.ne.kr/1004 https://elfinlas.github.io/2018/02/16/thymeleaf-layout-dialect_exam/ https://elfinlas.github.io/2018/02/17/thymeleaf-layout-dialect/

아래는 Thymeleaf에 대한 사용법이 잘 정리되어 있다. http://cyberx.tistory.com/132