Java의 template engine 중 유명한거로 Thymeleaf가 있다. HTML, XML, JS, CSS, TXT 등을 처리하거나 생성할 때 사용되는 엔진이다.
Template Engine이 뭔지는 이 글 참고. 참고로 Thymeleaf는 예상했겠지만 server-side, text template engine이다.
사실 템플릿 엔진을 다루는 것이 Spring에는 Spring MVC template engine이랑 Spring Boot template engine이 있다. 둘의 차니는 사실 Spring과 Spring Boot의 목표성을 고려하면 꽤 직관적이다.
Spring MVC template engine은 그냥 HTML/XML 등의 template을 기반으로 동적 웹페이지를 생성할 수 있는 web application을 만들 수 있는 환경을 제공하는거다. 뭐... 웹 애플리케이션에서 필요하니까...
Spring Boot의 목표는 보통 Spring에서 하는 것을 편하게 하는 것이 목표다. Spring Boot template engine도 마찬가지인데 Spring MVC template engine에서 지원하는 template engine들을 위한 기본 설정을 해주는 것이다. starter dependency로 이를 제공한다. 이 auto configuration의 설정을 변경하는 것도 가능하다는 점 참고.
여하튼, 이번에 이글에서 소개하는 것은
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
글 작성 기준 spring은 6이 최신이기 때문에 최신 것을 쓰고 싶으면 baeldung과 다르게
thymeleaf-spring6
를 쓰는게 맞다는 점 유의
이제 spring MVC template engine측에 thymeleaf tempalte engine을 위한 configuration을 제공해줘야 한다.
그게 무슨 말인가? 간단하다. Spring의 ApplicationContext
에다가 thymeleaf template engine관련 configuration을 bean의 형태로 제공해야 한다는 것이다. 그럼 무슨 configuration을 줘야 하는가? 바로 ServletContextTemplateResolver
과 SpringTemplateEngine
이다.
@Bean
@Description("Thymeleaf Template Resolver")
public ServletContextTemplateResolver templateResolver() {
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver();
templateResolver.setPrefix("/WEB-INF/views/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("HTML5");
return templateResolver;
}
@Bean
@Description("Thymeleaf Template Engine")
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
templateEngine.setTemplateEngineMessageSource(messageSource());
return templateEngine;
}
- 별첨 :
@Description
이것은 Spring 4부터 나온 문법인데, 그냥@Bean
이나@Component
에서 나온 bean에 대한 text description을 나타낸다. 테스트에서 활용하는거 정도로 쓰이고 기능적으로 크게 뭘
ServletContextTemplateResolver
thymeleaf에서 제공하는 class다. 이 class는 ITemplateResolver
이라는 interface의 구현체로 AbstractConfigurableTemplateResolver
을 확장했다.
ITemplateResolver
은 thymeleaf의 모든 Template Resolver들이 기본적으로 implement하는 interface다.
AbstractConfigurableTemplateResolver
도 역시나 ITemplateResolver
의 구현체다. AbstractTemplateResolver
이라고 모든 template resolver들의 가장 기본적인 구현체를 확장한 녀석인데, 여러 종류의 configuration을 설정하는게 가능한 template resolver이다. 대표적으로 설정 가능한 것들은
그럼 결국 이 class는 무엇을 하는 것인가? 일단 나름대로의 환경설정이 가능한 template resolver인건 위 2개를 통해 알 수 있다. 거기에 더불어, template resource들에 대응되는 ServletContextTemplateResource
를 만드는 것이 가능하다. 그러니까 어떤 template에 대응되는, ServletContext
상의 자원을 만들어주는 것이 가능하다는 것을 의미. ServletContext
는 알다시피 servlet container과 관련이 있는 내용이다. 결국 template 이름으로부터 servlet container에 저장되어 있는 template 자원들을 mapping시키는 resolver이라고 생각하면 된다. servlet container 기반으로 운용되는 웹 애플리케이션에서는 필수적으로 필요하겠지요.
이때 servlet container 상에서의 template 자원의 위치 (setPrefix
), 사용하는 확장자(setSuffix
), 그리고 사용하는 형식(HTML5
)이 무엇인지를 전부 세팅하는 것이 가능하다. 이는 얘가 AbstractConfigurableTemplateResolver
을 확장하는 녀석이라서 가능한 것이라는 점 참고.
위 코드에서 유의해야하는건 사실
setTemplateMode
에서HTML5
는 더이상 지원이 안 될 예정이고HTML
만 사용이 가능하다는 것이다. 관련 내용이 담긴 javadoc
SpringTemplateEngine
앞이 간단히 말해 template 이름에 따른 (HTML 확장자의) thymeleaf template 자원을 찾아주는 녀석이라면, 이 녀석은 발견한 thymeleaf template 자원을 해석 및 처리해 실제로 유효한 HTMl 파일을 만들고, 이를 기반으로 Spring MVC에서 사용할 수 있는 View를 만드는 녀석이라고 생각하면 된다.
이 때문에 먼저 template 이름에 따른 template 자원을 찾을 수 있는 녀석이 필요하다. 그래서 앞에 있는 ServletContextTemplateResolver
을 주입받는다.
ResourceBundleMessageSource
@Bean
@Description("Spring Message Resolver")
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages");
return messageSource;
}
그리고 이 method에서 생성한 녀석을 주입받는 부분도 존재함을 볼 수 있다.
이 class의 용도는 보통 각 나라 언어별로 페이지를 만들 때, 혹은 각 나라에서 사용하는 시간/환율/단위를 제공 해야 할 때 사용되는 녀석이다. 즉 웹 애플리케이션에서 제공하는 페이지가 이를 실제로 사용하는 곳의 위치에 따라 잘 적응할 수 있도록 하기 위해 만든 것이다.
정확한 동작을 설명하자면, 먼저 위와 같이 각 국가에 필요로 하는 자원을 모아놓은 것에 대한 추상화 표현으로 ResourceBundle
이라고 Java SE에서 제공하는 것이 있다. 각 국가에서 사용할 자원들을 모아놓은 파일들은 properties
확장자의 파일로 보통 저장이 되며, 이것을 load해가지고 만드는 객체가 바로 ResourceBundle
인 것이다. 여기에 약간 더 자세한 설명이 있다.
그러면MessageSource
는 무엇인가? 이건 Spring에서 제공하는 인터페이스고 사실 목표는 위의 ResourceBundle
과 유사하다. 바로 다양한 국가에 대해 웹페이지가 적응할 수 있도록 관련 자원들을 뒤져서 적절한 자원들을 보여주는 것이다. 다만 이 구현체가 2가지가 있는데 하나는 위의 JavaSE를 기반으로 한 ResourceBundleMessageSource
고 또 하나는 ReloadableResourceBundleMessageSource
이 있다. 참고로 이 이름이 MessageSource
인 이유는, 국제화 대상이 되는, 그니까 사용자에게 보여지는 객체를 Message
라고 하기 때문.
참고로 setBaseName
은 관련된 resource들을 각 나라에 대해서 모아놓은 파일들의 '기본'이름을 지정한다. 위는 messages
라고 했는데, 이러면 message_en.properties
, message_kr.properties
와 같이 properties
파일을 지정해야 상황에 따라 제대로 인식이 가능..
ViewResolver
, ThymeleafViewResolver
마지막으로 ThymeleafViewResolver
, 정확히는 Spring MVC에 사용할 ViewResolver
을 만들어야 한다. Spring MVC의 ViewResolver
의 역할은 view 이름이 controller로부터 주어지면 실제 대응되는 view를 찾아내는 것을 말한다. 이 view는 우리가 thymeleaf를 사용하다보니 thymeleaf view가 될 것이다.
이것도 당연히 configuration에 들어갈 bean이고, 다음과 같이 만드는 것이 가능하다. 보면 우리가 방금 만든 SpringTemplateEngine
을 주입받는걸 볼 수 있다. 앞에 말했듯 결국 Engine에서 View를 실질적으로 만드는 곳이기 때문에 주입을 받아야 이쪽에서도 Controller에서 준 view name을 기반으로 view를 만들 수 있기 때문.
@Bean
@Description("Thymeleaf View Resolver")
public ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
viewResolver.setOrder(1);
return viewResolver;
}
위와 같은 미친 설정, 너무 피곤하지 않은가! 그리고 실제로 할 필요 없다! Spring Boot만 있으면 말이다~
spring-boot-starter-thymeleaf
를 사용하면 위의 작업을 다 알아서 해준다! 이때 기본 설정에 따르면 HTMl 파일들은 src/main/resources/templates
위치에 존재해야 한다는 점은 유의.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
저 기본 경로를 바꾸려면
application.properties
에다가spring.thymeleaf.prefix
를 사용하면 된다. 밑은/src/main/resources/my-template
위치로 바꾸는 예시.spring.thymeleaf.prefix=classpath:/my-template/
ResourceBundleMessageSource
에 대해서 이미 설명을 했다. 이제 이걸 활용해보고 싶은데... 그럴려면 thymeleaf template 측에서 어떻게 해야하냐면 매우 간단하다.<span th:text="#{welcome.message}" />
welcome.message
는 key다. 그러면 앞의 SpringTemplateEngine
측에서 ResourceBundleMessageSource
bean을 활용해가지고 resolve를 해 알아서 view를 만들 것이다. properties
파일에 다음과 같은 항목이 있다면 그게 그대로 웹페이지에 출력되는 것이다.welcome.message=Welcome Student !!!
model.addAttribute("serverTime", dateFormat.format(new Date()));
Current time is <span th:text="${serverTime}" />
MessageSource
때와 다르게 {}
을 placeholder로 사용할 때 $
이 붙어있다는 점 유의.다만 model attribute에서 가리키는 것이 단일 객체가 아니라 객체들의 모임일수도 있다. 예를들면 List
라든가. 이것들 하나하나를 출력하고 싶으면 어떻게 해야 할까?
간단하다. 바로 th:each
를 사용하면 된다. 예를들어 Student
라는 객체가 있고, 사용자가 모든 학생들의 아이디와 이름을 받고 싶다고 해보자. 그러면 controller의 관련 request handler측에서는 모든 학생들을 담기 위해 List
를 만들고 조작한 다음에 이를 model attribute로 집어넣을 것이다. view에서 활용하라고 말이다.
public class Student implements Serializable {
private Integer id;
private String name;
// standard getters and setters
}
List<Student> students = new ArrayList<Student>();
// logic to build student data
model.addAttribute("students", students);
<tbody>
<tr th:each="student: ${students}">
<td th:text="${student.id}" />
<td th:text="${student.name}" />
</tr>
</tbody>
if
and unless
if
랑 특정 조건이 아니면 출력되는 unless
라는 녀석이 존재한다. 다음과 같은 Student
객체가 있다고 해보자.public class Student implements Serializable {
private Integer id;
private String name;
private Character gender;
// standard getters and setters
}
gender
이 M
인 경우 Male
을, F
인 경우 Female
을 출력하도록 요청을 했다고 해보자. 그러면 일단 controller의 관련 request handler이 학생들을 다 모아놓긴 할 것이다. iterate를 하는 방법은 앞에서 배웠고, 조건에 따라 출력하는걸 다르게 하려면? 밑과 같이 하면 된다.<td>
<span th:if="${student.gender} == 'M'" th:text="Male" />
<span th:unless="${student.gender} == 'M'" th:text="Female" />
</td>
switch
and case
switch
, case
문과 유사한 th:switch
, th:case
라는 녀석도 존재한다. 밑은 위와 같은 역할을 하는 thymeleaf template이다.<td th:switch="${student.gender}">
<span th:case="'M'" th:text="Male" />
<span th:case="'F'" th:text="Female" />
</td>
사용자 입력을 thymeleaf template 형태의 form에서 받은 다음, 이것을 프로그램에서 접근하는 것도 가능하다.
상황은 우리가 Student
정보를 form을 통해 받은 다음에 이를 프로그램에서 처리하는 경우에 대해 알아보도록 하자. 그러면 먼저 form의 경우 다음과 같이 작성하면 된다.
<form action="#" th:action="@{/saveStudent}" th:object="${student}" method="post">
<table border="1">
<tr>
<td><label th:text="#{msg.id}" /></td>
<td><input type="number" th:field="*{id}" /></td>
</tr>
<tr>
<td><label th:text="#{msg.name}" /></td>
<td><input type="text" th:field="*{name}" /></td>
</tr>
<tr>
<td><input type="submit" value="Submit" /></td>
</tr>
</table>
</form>
여기서 th:action
부분에서 어떤 경로로 POST request를 보낼지를 지정하며 th:object
에서는 각 field가 bound된 object가 무엇인지를 지정하는데 사용된다. th:field
부분에 *{...}
문법이 있는것이 보일텐데, 이건 th:object
덕분에 ${object.(...)}
부분으로 해석되는 것이 가능하다. 참고로 이 object는 controller에게 model attribute 형태로 전달되며 그때의 이름도 student
가 된다.
그리고 table
tag는 그냥 input을 받는데 사용되는 간단한 구조물이다. 이 구조물에 Student
를 위한 id
랑 name
을 입력하면 된다. 마지막은 제출용 버튼.
또 th:text
부분에서 #
부분은 resource bundle에서 내용물을 찾아가지고 그 값을 label
에 집어넣으라는 것을 의미한다. #{...}
부분이 thymeleaf에서는 resource bundle을 활용한 국제화에 쓰인다는 것을 앞에서 배웠는데 이를 활용한 것이다.
이것을 controller의 관련 request handler이 활용하려면 다음과 같이 하면 된다. @ModelAttribute
를 활용해 미리 student
에 해당하는 object를 Student
parameter에다가 자동으로 binding하고 있다는 점 참고.
@RequestMapping(value = "/saveStudent", method = RequestMethod.POST)
public String saveStudent(Model model, @ModelAttribute("student") Student student) {
// logic to process input data
}
먼저 field
에 validation error이 있는지 없는지는 #fields.hasErrors()
를 통해 확인하는 것이 가능하다. 만일 존재한다면, 해당 field와 관련된 실제 오류를 출력하는 것은 #fields.errors()
를 활용하면 된다.
예를 들어 밑은 id
랑 name
에 있는 error을 그대로 출력하는 thymeleaf template의 일부분이다.
<ul>
<li th:each="err : ${#fields.errors('id')}" th:text="${err}" />
<li th:each="err : ${#fields.errors('name')}" th:text="${err}" />
</ul>
name
에 error에 있을시 그 오류를 출력하는 thymeleaf template의 일부분이다. th:errors
는 관련 field의 error message를 출력될 때 사용되는 녀석이라는 점 참고. <div th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Name Error</div>
*
이나 all
을 사용해서 모든 field를 지칭하는 것도 가능하다. 밑은 모든 field의 error을 출력하는데 사용될 수 있는 두 구문이다.<ul>
<li th:each="err : ${#fields.errors('*')}" th:text="${err}" />
</ul>
<ul>
<li th:each="err : ${#fields.errors('all')}" th:text="${err}" />
</ul>
global
을 사용하면 Spring에서의 global error들도 확인하는 것이 가능하다.<ul>
<li th:each="err : ${#fields.errors('global')}" th:text="${err}" />
</ul>
{{}}
을 사용하면 display할 data의 formatting이 가능하다.<tr th:each="student: ${students}">
<td th:text="${{student.name}}" />
</tr>
#conversions.convert
를 사용한다.<tr th:each="student: ${students}">
<td th:text="${#conversions.convert(student.percentage, 'Integer')}" />
</tr>
Formatter
을 implement한 class를 ApplicationContext
측에 추가함으로서 가능하다.Java SE에도
Formatter
이 있는데 그Formatter
이랑 다르다.
Formatter
을 WebMvcConfigurerAdapter
을 활용해서 추가했지만, 알다시피! Spring 6부터는 WebMvcConfigurer
을 사용한다. 그래서 이를 가지고 Formatter
을 추가해야 한다. 다만 extend하는게 저거라는것 말고 크게 코드가 달라지지는 않는다. @Override
@Description("Custom Conversion Service")
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new NameFormatter());
}
NameFormatter
class는 다음과 같다.public class NameFormatter implements Formatter<String> {
@Override
public String print(String input, Locale locale) {
return formatName(input, locale);
}
@Override
public String parse(String input, Locale locale) throws ParseException {
return formatName(input, locale);
}
private String formatName(String input, Locale locale) {
return StringUtils.replace(input, " ", ",");
}
}
보다시피 print
랑 parse
라는 method가 있는데, 전자는 Formatter
에 들어간 type parameter의 instance가 주어지면 출력에 사용될 String
을 만들어서 반환하는 것이고 후자는 Formatter
에 들어간 type parameter의 instance가 주어지면 parsing에 사용될 String
을 만들어서 반환하는 것이다. 위의 경우 하필 type parameter도 String
이라서... 조금 요상하다는 점은 유의.
이렇게 만든 Formatter
을 추가한것까진 좋은데, 어떤 Formatter
을 사용해야하는지는 어떻게 아는 것일까? 사실 그냥 변환하려고 하는 녀석의 type을 기반으로, Formatter
의 type parameter이 그거랑 일치하면 그냥 그걸 쓴다. addFormatterForFieldType
으로 어떤 class에 대해 사용할지 지정하는 것도 가능하다. 그것도 결국 type기반이긴 하다만 여튼...