Spring Boot + Thymeleaf에서 JTE로 마이그레이션할 때의 판단 기준: 타입 안전 SSR과 AI 구동 개발 관점에서
요약
본 기사는 Spring Boot + Thymeleaf 기반 업무 애플리케이션을 JTE(Java Template Engine)로 마이그레이션할 때의 판단 기준을 제시합니다. 특히 JTE는 템플릿을 빌드 시점에 Java 클래스로 변환하여 타입 안전성을 확보하고, 변경된 필드의 영향 범위를 컴파일 단계에서 미리 감지할 수 있다는 장점을 강조합니다.
핵심 포인트
- JTE는 템플릿을 빌드 시점(Compile Time)에 처리하여 타입 안정성이 높다.
- 필드명 변경 등 구조적 변화가 발생하면, CI/CD 과정에서 모든 참조 단절을 미리 감지 가능하다.
- Thymeleaf와 달리 JTE는 문맥별 이스케이프 규칙을 명시적으로 관리할 수 있다.
서론
Spring Boot로 화면을 갖춘 업무 애플리케이션을 만들 때, 템플릿 엔진은 사실상 Thymeleaf 하나만 선택지가 되어 왔습니다. 본 기사에서는 시스템 아키텍트의 시점에서 Spring Boot + Thymeleaf 구성을 JTE (Java Template Engine)로 대체할 가치를 정리합니다.
나아가 코딩 에이전트를 개발 플로우에 통합하는 경우, AI가 발생시킨 오류를 빌드 시점에 감지할 수 있다는 점은 마이그레이션의 추가적인 근거가 됩니다.
먼저 입장을 명확히 밝힙니다.
- 대상은 서버 사이드 렌더링(SSR)을 전제로 하는 업무 애플리케이션입니다. SPA와의 비교는 본론에서 제외합니다.
- 'JTE가 우수하다'고 결론 내리는 글이 아닙니다.
무엇이 개선되고, 무엇을 잃게 되며, 무엇을 자체적으로 갖게 되는지를 작성하여 채택 판단의 자료로 삼고자 합니다.
평가 축
템플릿 엔진 선정은 기능의 우열이 아닌 다음 축으로 평가합니다.
| 축 | 질문 |
|---|---|
| 유지보수성 | 10년 운영 시, 변경의 영향 범위를 기계적으로 파악할 수 있는가 |
| ... |
구조적 차이
Thymeleaf는 템플릿을 실행 시점에 해석하고, 표현식은 SpEL의 문자열로 평가합니다. JTE는 템플릿을 빌드 시점에 Java 클래스로 변환합니다. 이후 언급되는 장점들은 거의 모두 이 한 지점에서 파생됩니다. 템플릿이 타입(Type)과 시그니처(Signature), 컴파일러의 관리 하에 놓인다는 것입니다.
JTE로 마이그레이션하는 메리트
1. 템플릿의 타입 계약이 명시됨
JTE에서는 템플릿 시작 부분에서 받을 타입을 선언합니다.
@param com.example.users.UserListView view
템플릿 내부의 ${row.name()}는 Java 표현식으로 해결됩니다. 존재하지 않는 메서드의 참조나 타입 불일치는 화면을 열었을 때가 아니라, 빌드 시점에 실패합니다.
주의할 점이 있습니다. Spring MVC의 Model을 통해 템플릿을 호출하는 경우, 컨트롤러와 템플릿은 **이름(Name)**으로 연결됩니다. 따라서 타입 안전성이 보장되는 것은 '템플릿 내부'이며, '컨트롤러 → 템플릿' 경계는 별도로 지켜야 합니다.
실무에서는 다음 운영 방식으로 경계를 확실히 합니다.
- 화면별로 ViewModel (
record)을 하나 정의하고,Model에는 단일 속성만 전달 -
MockMvc 등으로 주요 화면을 렌더링까지 통과시키는 테스트를 배치합니다.
public record UserListView(List<Row> rows, boolean canCreate) {
public record Row(String id, String name, String email, String createdAt) {}
}
2. 변경의 영향 범위를 빌드가 알려줌
ViewModel의 필드를 이름 변경하면, 참조하는 템플릿이 컴파일 오류를 일으킵니다. JTE에는 템플릿을 사전에 컴파일하는 메커니즘이 있기 때문에, CI에서 모든 템플릿을 컴파일하면, 도달하기 어려운 화면의 참조 단절까지 리리즈 전에 감지할 수 있습니다.
Thymeleaf에서는 해당 화면을 실제로 표시할 때까지 SpEL의 참조 단절을 인지하지 못하는 경우가 있습니다. 업무 애플리케이션은 화면 수가 많고, 오류 화면이나 권한별 화면 등 테스트로 통과하기 어려운 경로가 반드시 남아있습니다. 이 차이는 큽니다.
3. 출력 이스케이프 규칙과 예외 추적 용이성
JTE는 HTML 구조를 이해하고, 문맥(요소 본문, 속성, script 등)별로 빌드 시점에 이스케이프 방식을 결정합니다. 이스케이프를 해제하고 싶을 때는 $unsafe{...}를 사용하기 때문에, 위험한 출력 위치를 grep으로 목록화할 수 있습니다.
<p>${comment}</p> <!-- 보통은 자동 이스케이프 -->
<p>$unsafe{trustedHtml}</p> <!-- 예외는 여기만. 리뷰 대상이 쉬움 -->
공평하게 작성하자면, Thymeleaf 역시 기본적으로 이스케이프합니다. 차이점은 '할 수 있는지 여부'가 아니라, 규칙의 일관성과 예외의 가시성입니다. 또한, 엔진 자체에도 취약점이 발생합니다. JTE에서도 script 내부의 JavaScript 템플릿 문자열에 기인하는 XSS(CVE-2025-23026)이 보고되었고, 3.1.16에서 수정되었습니다. 종속성 업데이트 관리는 어떤 엔진에서도 필요합니다.
4. 컴포넌트 경계가 함수 시그니처가 된다
JTE의 레이아웃이나 컴포넌트는 인수가 있는 호출입니다.
@param gg.jte.Content content
@param String title
<!DOCTYPE html>
...
@param com.example.users.UserListView view
@template.layout.page(
title = "사용자 목록",
...
Thymeleaf의 th:fragment / th:replace는 문자열로 프래그먼트를 참조하며, 호출하는 쪽의 변수 스코프를 암묵적으로 상속합니다. JTE에서는 전달된 것만 보입니다. 인수가 늘어나는 대신, 컴포넌트의 의존 관계가 읽을 수 있게 됩니다.
5. 템플릿을 Spring 없이 단독 테스트할 수 있다
JTE의 렌더링에는 TemplateEngine만 있으면 되기 때문에, Spring 컨텍스트를 구동하지 않고 HTML을 검증할 수 있습니다.
@Test
void 일람에 성명이 출력되는() {
var engine = TemplateEngine.create(
...
6. 운영 측면과 성능
JTE는 운영 환경에서 사전 컴파일된 템플릿을 사용할 수 있어, JRE만 있는 환경에서도 구동할 수 있습니다. 성능은 설계상 빠르지만, Thymeleaf 역시 운영 환경에서는 템플릿을 캐싱합니다. 따라서, 성능 차이는 자체 환경에서 측정하여 평가에 반영해야 합니다. 업무 애플리케이션의 지배적인 병목 현상은 보통 DB와 외부 연계입니다.
AI 구동 개발과의 적합성은 후속 전용 장에서 다루겠습니다.
AI 구동 개발에서의 우위성 ― '제대로 작성하게 하는 것'보다 '오류를 저렴하게 감지하는 것'
주장을 좁히기
먼저 결론을 말씀드립니다. JTE의 AI 협업상의 우위성은 AI가 제대로 작성할 수 있는 것이 아닙니다. AI의 오류가 빌드 시에, 위치를 지정하여, 기계가 읽을 수 있는 형태로 감지되는 것입니다.
이 루프가 사람의 손을 거치지 않고 돌아갈수록, 인간의 검토 대상은 '빌드와 테스트를 통과한 차이점'으로 좁혀집니다.
구조적인 근거
| 관점 | Thymeleaf | JTE |
|---|---|---|
| 오류의 현시화 | Thymeleaf의 감지 능력을 과소평가하고 있다 | JTE에서는 Java 컴파일로 감지할 수 있는 불일치가 증가한다 |
| ... | ||
| 요점은 3가지입니다. |
사양이 기계가 읽을 수 있게 된다: @param과 ViewModel의 record이 에이전트에게는 사양서가 됩니다. Thymeleaf에서는 Model 속성의 이름과 타입이 컨트롤러 측 문자열에 분산되어 있습니다. -
검증에 필요한 문맥이 작다: 화면 변경 단위가 'ViewModel + 하나의 .jte (+ 공통 레이아웃)'으로 수렴하기 쉬워, 에이전트에게 전달할 정보를 좁힐 수 있습니다. 이는 설계상의 속성이며, 효과의 크기는 다음 절의 측정에서 확인해야 할 가설입니다. -
위험 조작이 검토 대상으로 고정된다: AI가 생성한 차이점에 $unsafe{}가 포함되어 있다면 사람이 반드시 확인할 것이라는 규칙을 기계적으로 운영할 수 있습니다.
한계 ― JTE는 AI의 학습 데이터가 적다
불리한 점도 명시합니다. Thymeleaf는 정보량이 압도적으로 많고, AI도 익숙합니다. JTE는 새롭고 사용자도 적기 때문에, 구문 오류가 발생하기 쉽습니다.
필자가 다른 생성 AI와 설계 벽치기를 했을 때도 다음과 같은 오류가 나왔습니다.
- 콘텐츠 블록에 존재하지 않는
@eval{...}를 사용함 (정확히는@...``) - 생 HTML 출력에
!{...}을 사용함 (정확히는$unsafe{...}) - record의 필드를
${user.name}으로 참조함 (record에서는${user.name()})
이 모든 것은 JTE에서는 빌드가 실패하거나, 리뷰에서 감지할 수 있는 오류입니다. 그렇기 때문에 가치 있는 것은 '제대로 작성하게 하는 것'이 아니라, '틀리더라도 피해가 발생하기 전에 멈추는 것'입니다. Thymeleaf의 오류는 렌더링 테스트가 없으면 조용히 지나갈 때가 있습니다.
운영 — 에이전트에게 전달할 규칙
오류를 줄이려면, 에이전트를 위한 지시 파일(AGENTS.md나 CLAUDE.md 등)에 JTE의 요점을 적어두는 것이 효과적입니다.
## JTE 규칙
- 템플릿은 `src/main/jte`입니다. 맨 앞에서 `@param`을 통해 타입을 선언합니다.
- 콘텐츠 블록은 `` @`...` `` 입니다. `@eval`은 존재하지 않습니다.
...
CI 게이트
사람이 매번 확인할 필요가 없도록, 기계적인 게이트를 설정합니다.
- 모든 템플릿의 사전 컴파일을 CI에서 필수화합니다.
$unsafe{를 포함하는 변경분은 추가 리뷰를 필수화합니다(grep 기반으로 충분함) - 주요 화면의 렌더링 테스트(Spring이 빠진TemplateEngine버전 포함)를 통과시킵니다.
결론
- JTE의 AI 협업상의 우위성은, 오류의 조기 감지 및 기계가 읽을 수 있는 명세에 있습니다. - 학습 데이터 부족으로 인한 오류는, 규칙 정비와 CI 게이트로 흡수합니다. - 이 우위성은, AI를 사용하지 않을 경우에도 성립하는 타입 안전성의 장점의 추가분이며, 단독적인 채택 이유로 삼지 않습니다.
잃게 되는 것 및 자체적으로 보유할 것
아키텍트로서 중요하게 생각하는 부분입니다. JTE는 Thymeleaf처럼 Spring과 깊이 통합되어 있지 않기 때문에, 암묵적 기능이 명시적인 설계 대상이 됩니다.
| Thymeleaf에서 암묵적으로 얻던 것 | JTE에서의 처리 |
|---|---|
폼 바인딩 및 오류 표시(th:field / th:errors) | 스타터는 BindingResult 연동을 표준으로 제공하지 않습니다. 변환 계층을 자체 설계해야 합니다 |
CSRF 토큰 자동 부여(th:action) | 토큰을 명시적으로 출력합니다 |
i18n(#{...}) | MessageSource를 감싼 헬퍼를 전달합니다 |
URL 표현(@{...})의 컨텍스트 경로 대응 | URL 생성 헬퍼를 준비합니다 |
sec:authorize에 의한 표시 제어 | ViewModel에 표시 가능 여부(canEdit 등)를 사전에 계산하여 담아둡니다 |
| 정적 HTML 그대로 브라우저에서 확인할 수 있는 '자연스러운 템플릿' | 사라집니다. 디자이너 분업 전제를 확인해 주십시오 |
| Layout Dialect 같은 주변 자산 및 일본어 정보량 | 줄어듭니다. 공식 문서 중심의 운영이 됩니다 |
제 추천은, 암묵적인 메커니즘(ThreadLocal 등)에 의존하지 않고, 교차적인 관심사를 하나의 PageContext에 모아서 명시적으로 전달하는 것입니다.
public record PageContext(
String contextPath,
String csrfParam,
...
@ControllerAdvice
class ViewAdvice {
private final MessageResolver msg;
...
폼의 오류도, 컨트롤러에서 BindingResult를 화면용 FormErrors(항목명 → 메시지)로 명시적으로 변환하여 ViewModel에 담습니다. 이로 인해 템플릿은 Spring의 타입을 알 필요가 없어지고, 이전 장의 단위 테스트도 그대로 작성할 수 있습니다.
마이그레이션 설계
변환 대응표
Thymeleaf와 JTE 비교
| Thymeleaf | JTE |
|---|---|
th:text="${x}" | ${x} |
th:utext | $unsafe{x} |
th:each | @for(...) … @endfor |
th:if / th:unless | @if(...) … @endif |
th:fragment / th:replace | @template.xxx(...) |
@{/path} | URL 헬퍼 |
#{key} | ctx.msg().get("key") |
${#temporals.format(...)} | ViewModel에서 정형화된 값을 전달 |
핵심은 두 가지
① SpEL → ViewModel
SpEL로 표시 로직(정형, 조건, Bean 호출)을 작성했던 부분은 JTE에 직역하지 않고, Java 측의 ViewModel에서 완결시킵니다. 마이그레이션을 '템플릿으로부터 비즈니스 로직과 정형 처리를 끄집어내는 기회'로 위치지으면, 템플릿의 책임이 작아지고 단위 테스트도 쉬워집니다.
② 프래그먼트 → 명시 인수의 함수
Thymeleaf의 프래그먼트가 암묵적으로 참조하던 변수를 파악하여 인수로 격상시킵니다. 인수가 많아지는 부품은 작은 record에 모읍니다.
진행 방법
- 공통 레이아웃 → 부품 → 화면(최종) 순서로, 화면 단위로 단계적 마이그레이션을 합니다.
- 마이그레이션 전후의 렌더링 결과 HTML을 정규화하여 차분 비교합니다 (회귀 감지). - CI에서 모든 템플릿의 컴파일을 필수화합니다. 운영 환경에서는 사전 컴파일된 템플릿을 사용합니다.
- 병행 운영 중에는 뷰 이름 충돌과 ViewResolver의 우선순위를 검증합니다.
기계적인 변환은 AI에게 맡기기 쉬운 작업이지만, 정확성의 판별은 차분 비교와 테스트로 진행하는 것을 전제로 합니다.
최소 구성
Spring Boot 3 계열의 경우, 의존성은 gg.jte:jte-spring-boot-starter-3입니다.
(Spring Boot 4 계열은 -4)
빌드 시에는 공식 Maven / Gradle 플러그인으로 템플릿을 사전 컴파일합니다.
gg.jte.templateLocation=src/main/jte
gg.jte.developmentMode=true # 개발 시: 변경을 감지하여 재컴파일
# gg.jte.usePrecompiledTemplates=true # 운영 환경: 사전 컴파일된 것을 사용
버전은 CVE-2025-23026 수정(3.1.16) 이후 버전을 사용하고, 작성 시점의 최신 버전은 공식적으로 확인해 주십시오.
채택 판단
| 적합한 경우 | 재고를 고려할 경우 |
|---|---|
| 개발자가 HTML까지 작성하는 체제 | 디자이너가 정적 HTML을 직접 편집하는 분업 |
| ... | |
| 판단의 근거는 ADR(Architecture Decision Record)로 남기는 것을 권장합니다. 평가 축, 기각한 선택지, 자체적으로 보유하기로 결정한 것을 작성해 두면 수년 후의 검토에 사용할 수 있습니다. |
요약
JTE로의 마이그레이션은 Thymeleaf의 구문을 Java에 대체하는 작업만 아닙니다. 템플릿을 Java의 타입 검사 및 컴파일 대상화하고, 화면의 입력 사양이나 컴포넌트의 의존 관계를 더욱 명시적으로 관리하기 위한 설계 변경입니다.
채택함으로써 기대할 수 있는 주요 효과는 다음 3가지입니다.
- 변경 영향의 조기 감지: ViewModel의 변경으로 인한 템플릿의 불일치를 컴파일 시점에 탐지할 수 있는 범위가 넓어집니다. -
의존 관계의 명확화: 화면 부품이 받는 인수를 명시하여, 템플릿 간의 암묵적인 의존을 줄일 수 있습니다. -
검증의 자동화: 템플릿의 컴파일과 렌더링 테스트를 CI에 통합하여, 변경의 회귀를 감지하기 쉬워집니다.
반면, Thymeleaf와 Spring의 통합으로 얻고 있던 폼 처리나 CSRF 토큰 출력, i18n, URL 생성 등은 마이그레이션 시 설계 재검토가 필요합니다. 또한, JTE의 타입 안전성은 화면의 표시 사양이나 권한, 보안상의 정확성까지 보장하는 것은 아닙니다.
AI 구동 개발에서도 JTE의 가치는 AI가 올바른 코드를 생성하는 것 자체가 아니라, 생성된 코드의 불일치를 컴파일러나 테스트에서 조기에 감지할 수 있다는 점에 있습니다. 다만, 그 효과의 크기는 기존 테스트나 CI 구성에도 좌우됩니다.
따라서 JTE의 채택은 AI와의 궁합만으로 결정해서는 안 됩니다. 타입 안전성(Type Safety), 유지보수성(Maintainability), 테스트 용이성(Testability) 개선을 기대할 수 있는지, 그리고 Spring과의 통합 기능을 명시적으로 설계하는 비용을 감당할 수 있는지를 평가해야 합니다.
궁극적으로는 대표적인 화면을 활용한 파일럿 마이그레이션을 통해, 마이그레이션 공수와 검증 효과를 확인하고 그 결과를 ADR(Architecture Decision Record)로 남기는 것을 권장합니다.
참고
논의 (Discussion)

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기