이번에 회사에서 받은 데이터를 가지고 PDF를 만들 일이 있어서 Thymeleaf를 사용하여 적용해 본 방법을 기록하고자 합니다.

의존성

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
	implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
	implementation('org.xhtmlrenderer:flying-saucer-pdf-openpdf:9.1.22')
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
}

Template Engine을 사용하여 렌더링 된 HTML을 String으로 반환받기

@Component
@RequiredArgsConstructor
public class TemplateParser {

	public String parseHtmlFileToString(String templateName, Map<String, Object> variables) {
		// Thymeleaf Resolver 설정
		var templateResolver = new ClassLoaderTemplateResolver();
		templateResolver.setPrefix("templates/");
		templateResolver.setSuffix(".html");
		templateResolver.setTemplateMode(TemplateMode.HTML);

		// Spring Template Engine으로 위에서 설정한 Thymeleaf Resolver를 사용하도록 설정
		var templateEngine = new SpringTemplateEngine();
		templateEngine.setTemplateResolver(templateResolver);

		// Template Engine에서 사용할 변수
		var context = new Context();
		context.setVariables(variables);

		// 렌더링 된 값을 String으로 반환
		return templateEngine.process(templateName, context);
	}
}

Spring Boot에서 Template Engine 사용 시

interface TemplateParser를 만들어 Template Engine마다 상속받아 구현하는 것이 더 좋은 설계이지만, Spring Boot를 이용하면 설정파일로 Template Engine 설정할 수 있습니다. Spring Boot는 기본적으로 Thymeleaf가 설치 되어 있다면 Thymeleaf 설정을 해줍니다.

즉, 위에서 SpringTemplateEngine을 의존주입 받아 사용하면 Template Engine 설정하는 부분 생략할 수 있습니다.

@Component
@RequiredArgsConstructor
public class TemplateParser {

	private final SpringTemplateEngine templateEngine;

	public String parseHtmlFileToString(String templateName, Map<String, Object> variables) {
		var context = new Context();
		context.setVariables(variables);
		return templateEngine.process(templateName, context);
	}
}

Thymeleaf 사용하기

layout기능을 사용하기 위해선 org.springframework.boot:spring-boot-starter-thymeleaf 외에 nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect도 필요합니다.

layout/document.html

<!DOCTYPE html>
<html
	lang="ko"
	xmlns:th="<http://www.thymeleaf.org>"
	xmlns:layout="<http://www.ultraq.net.nz/thymeleaf/layout>"
>
<head>
	<meta charset="UTF-8" />
	<meta
		name="viewport"
		content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
	<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<style th:replace="pdf/approval/document/partials/styles :: commonStyle"></style>
<title th:text="${title}"></title>
</head>
	<body>
		<div id="main-pdf-container">
			<div th:replace="partials/header :: commonHeader"></div>
			<div th:replace="partials/commonField :: commonField"></div>
			<div layout:fragment="content"></div>
			<div th:replace="partials/attachedFiles :: commonApprovalAttachedFiles"></div>
		</div>
	</body>
</html>

저는 공통적으로 사용 될 layout용 html을 하나 만들어서 사용했습니다.

th:replace 부분에 들어갈 파일 작성

partials/header부분은 templates폴더 하위 부터 해당 파일의 절대경로를 나타내며 :: 이후 commonHeader는 해당 파일 내에서 th:fragment로 선언된 이름입니다.

partials/header.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="<http://www.thymeleaf.org>">
	<div th:fragment="commonHeader">...</div>
</html>

layout:fragment 부분에 들어갈 파일 작성