Baeldung - Introduction to Using Thymeleaf in Spring

sycho·2024년 4월 4일
0

Baeldung - Spring MVC

목록 보기
7/7

Baeldung의 이 글을 정리 및 추가 정보를 넣은 글입니다.

1. Overview

  • 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의 설정을 변경하는 것도 가능하다는 점 참고.

  • 여하튼, 이번에 이글에서 소개하는 것은

    • Spring MVC/Boot의 template engine들을 기반으로 thymeleaf template engine을 사용을 위해 설정하는 방법.
    • MVC의 view layer에서 thymeleaf을 사용할 때 쓸 수 있는 몇몇 thymeleaf 문법.

2. Integrating Thymeleaf With Spring

  • 먼저 MVC에서 thymeleaf를 사용하려면 thymeleaf와 thymeleaf-spring library를 dependency에 추가해야 한다. 이는 maven의 경우 다음과 같이 하면 된다.
<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를 쓰는게 맞다는 점 유의

configuration setting

  • 이제 spring MVC template engine측에 thymeleaf tempalte engine을 위한 configuration을 제공해줘야 한다.

  • 그게 무슨 말인가? 간단하다. Spring의 ApplicationContext에다가 thymeleaf template engine관련 configuration을 bean의 형태로 제공해야 한다는 것이다. 그럼 무슨 configuration을 줘야 하는가? 바로 ServletContextTemplateResolverSpringTemplateEngine이다.

@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

  • javadoc

  • thymeleaf에서 제공하는 class다. 이 class는 ITemplateResolver이라는 interface의 구현체로 AbstractConfigurableTemplateResolver을 확장했다.

  • ITemplateResolver은 thymeleaf의 모든 Template Resolver들이 기본적으로 implement하는 interface다.

  • AbstractConfigurableTemplateResolver도 역시나 ITemplateResolver의 구현체다. AbstractTemplateResolver이라고 모든 template resolver들의 가장 기본적인 구현체를 확장한 녀석인데, 여러 종류의 configuration을 설정하는게 가능한 template resolver이다. 대표적으로 설정 가능한 것들은

    • template 이름 관련 resolving 방식
    • template mode 관련 resolving 방식
    • cache validity
    • character encoding
  • 그럼 결국 이 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

  • javdoc

  • 앞이 간단히 말해 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;
}

3. Thymeleaf in Spring Boot

  • 위와 같은 미친 설정, 너무 피곤하지 않은가! 그리고 실제로 할 필요 없다! 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/

4. Displaying Values From Message Source (Property Files)

  • 앞에서 ResourceBundleMessageSource에 대해서 이미 설명을 했다. 이제 이걸 활용해보고 싶은데... 그럴려면 thymeleaf template 측에서 어떻게 해야하냐면 매우 간단하다.
<span th:text="#{welcome.message}" />
  • 여기서 welcome.message는 key다. 그러면 앞의 SpringTemplateEngine 측에서 ResourceBundleMessageSource bean을 활용해가지고 resolve를 해 알아서 view를 만들 것이다. properties 파일에 다음과 같은 항목이 있다면 그게 그대로 웹페이지에 출력되는 것이다.
welcome.message=Welcome Student !!!

5. Displaying Model Attributes

  • 앞에처럼 locale에 따른 message를 사용하는 것도 가능하지만... model의 attribute를 웹페이지에 사용하는 것도 가능하다.

5.1 Simple Attributes

  • 만약에 사용자가 서버 시간을 요청했다고 해보자. 그래서 이를 해결하기 위해 controller의 관련 request handler에서 request 관련 model에다가 다음 attribute를 넣었다고 해보자.
model.addAttribute("serverTime", dateFormat.format(new Date()));
  • 이걸 렌더링 때 활용하고 싶으면, 활용할 thymeleaf template에서 다음과 같이 template file을 작성하면 된다.
Current time is <span th:text="${serverTime}" />
  • 보면 MessageSource 때와 다르게 {}을 placeholder로 사용할 때 $이 붙어있다는 점 유의.

5.2 Collection Attributes

  • 다만 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);
  • 그러면 관련 thymeleaf template은 다음과 같이 작성하면 된다.
<tbody>
    <tr th:each="student: ${students}">
        <td th:text="${student.id}" />
        <td th:text="${student.name}" />
    </tr>
</tbody>

6. Conditional Evaluation

  • 특정 조건에 따라 출력되는 것을 다르게 하는것도 thymeleaf에서는 가능하다. 관련해서 2가지 방법이 제공된다.

6.1 if and unless

  • 먼저 특정 조건이면 출력되는 if랑 특정 조건이 아니면 출력되는 unless라는 녀석이 존재한다. 다음과 같은 Student 객체가 있다고 해보자.
public class Student implements Serializable {
    private Integer id;
    private String name;
    private Character gender;
    
    // standard getters and setters
}
  • 여기서 한 사용자가 모든 학생들에 대해 genderM인 경우 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>

6.2 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>

7. Handling User Input

  • 사용자 입력을 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를 위한 idname을 입력하면 된다. 마지막은 제출용 버튼.

  • 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
}

8. Displaying Validation Errors

  • 먼저 field에 validation error이 있는지 없는지는 #fields.hasErrors()를 통해 확인하는 것이 가능하다. 만일 존재한다면, 해당 field와 관련된 실제 오류를 출력하는 것은 #fields.errors()를 활용하면 된다.

  • 예를 들어 밑은 idname에 있는 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>
  • 위 두 function의 경우 *이나 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>

9. Using Conversions

  • {{}}을 사용하면 display할 data의 formatting이 가능하다.
<tr th:each="student: ${students}">
    <td th:text="${{student.name}}" />
</tr>
  • 또 display할 data를 기존의 class가 아닌 다른 class로 변형시키는 것도 가능한데 이 때는 #conversions.convert를 사용한다.
<tr th:each="student: ${students}">
    <td th:text="${#conversions.convert(student.percentage, 'Integer')}" />
</tr>

Formatter

  • 이 때 첫번째의 formatting을 실제로 활용하고 싶으면... 관련된 formatter을 직접 추가를 해줘야 한다. 이는 Spring에서 제공하는 Formatter을 implement한 class를 ApplicationContext측에 추가함으로서 가능하다.

Java SE에도 Formatter이 있는데 그 Formatter이랑 다르다.

  • 원본 글에서는 이 FormatterWebMvcConfigurerAdapter을 활용해서 추가했지만, 알다시피! 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, " ", ",");
    }
}
  • 보다시피 printparse라는 method가 있는데, 전자는 Formatter에 들어간 type parameter의 instance가 주어지면 출력에 사용될 String을 만들어서 반환하는 것이고 후자는 Formatter에 들어간 type parameter의 instance가 주어지면 parsing에 사용될 String을 만들어서 반환하는 것이다. 위의 경우 하필 type parameter도 String이라서... 조금 요상하다는 점은 유의.

  • 이렇게 만든 Formatter을 추가한것까진 좋은데, 어떤 Formatter을 사용해야하는지는 어떻게 아는 것일까? 사실 그냥 변환하려고 하는 녀석의 type을 기반으로, Formatter의 type parameter이 그거랑 일치하면 그냥 그걸 쓴다. addFormatterForFieldType으로 어떤 class에 대해 사용할지 지정하는 것도 가능하다. 그것도 결국 type기반이긴 하다만 여튼...

profile
안 흔하고 싶은 개발자. 관심 분야 : 임베디드/컴퓨터 시스템 및 아키텍처/웹/AI

0개의 댓글