안녕하세요, 여러분의 든든한 IT 길잡이 데브프리입니다.
스프링 부트(Spring Boot)로 웹 페이지를 만들거나, API를 개발하고 나서 브라우저로 접속을 해봅니다. "좋아, 이제 내가 만든 예쁜 화면(또는 데이터)이 뜨겠지?"
하지만 기대와 달리 브라우저에는 'Whitelabel Error Page'와 함께 500 에러가 뜨고, 스프링 부트 콘솔 창에는 아래와 같은 길고 짜증 나는 에러가 출력됩니다.
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [index], template might not exist or might not be accessible
"분명히 코드에 빨간 줄도 없고 로직도 완벽한데, 템플릿(Template)을 못 찾는다니?"
프론트엔드 화면(HTML)을 연결하거나 처음 API를 만들 때 정말 자주 겪는 에러입니다.
오늘은 이 '템플릿 경로 인식 실패 에러(500)'의 원인을 아주 직관적인 레스토랑 비유로 알아보고, 단 1분 만에 허탈할 정도로 쉽게 해결하는 3가지 방법을 정리해 드리겠습니다!
1. Error resolving template, 도대체 왜 뜨는 걸까? (유령 접시 비유)
에러 메시지를 직역하면 "네가 말한 'index'라는 이름의 템플릿(HTML 화면)을 찾을 수 없거나 접근할 수 없어!"라는 뜻입니다.

스프링 부트에서 일반 @Controller를 사용할 때, 메서드 마지막에 return "index"; 라고 적으면 이는 "글자 index를 그대로 출력해라"라는 뜻이 아닙니다.
스프링 부트의 템플릿 엔진(Thymeleaf 등)에게 "내부에 저장된 수많은 HTML 파일 중, 이름이 index.html인 파일을 찾아서 그 화면을 사용자에게 보여줘!"라는 뜻입니다.
그런데 스프링 부트가 폴더를 아무리 샅샅이 뒤져봐도 index.html이라는 파일이 없거나, 엉뚱한 곳에 놓여 있으면 멘붕에 빠지며 이 에러를 뱉어내는 것입니다.
2. 이 에러가 발생하는 3가지 흔한 실수와 해결법
주방장이 접시를 찾지 못하는 가장 대표적인 3가지 상황을 확인해 봅시다.
실수 ①: 데이터를 반환해야 하는데 @Controller를 썼을 때 (API 개발 시 가장 흔함!)
"저는 HTML 화면을 보여주려는 게 아니라, 그냥 'success'라는 글자나 JSON 데이터를 보내주고 싶었던 건데요?" 만약 그렇다면 어노테이션을 잘못 사용한 것입니다.
- 🚨 에러 발생 코드:
Java @Controller // ⬅️ HTML 화면을 찾으려고 시도하는 웨이터 public class UserController { @GetMapping("/api/test") public String test() { return "success"; // "success.html 이라는 접시를 찾아라!" (에러 발생) } } - ✅ 해결법: 순수한 글자나 JSON '데이터'만 응답하고 싶다면 클래스 위에 붙은 @Controller를 @RestController로 바꾸거나, 메서드에 @ResponseBody를 붙여주어야 합니다.
Java @RestController // 💡 "접시 찾지 말고, 이 글자(데이터) 자체를 손님에게 바로 줘!" public class UserController { @GetMapping("/api/test") public String test() { return "success"; // 브라우저에 화면 대신 'success' 글자만 예쁘게 출력됨 } }

실수 ②: HTML 파일의 오타나 대소문자를 틀렸을 때
정말로 화면을 띄워주려는 목적이 맞다면, 컨트롤러에서 리턴한 문자열과 실제 파일 이름이 100% 똑같은지 확인해야 합니다.
- return "main"; 이라고 적었는데 실제 생성한 파일 이름이 Main.html 이거나 mian.html 이라면 스프링 부트는 절대 찾지 못합니다. 컴퓨터는 대소문자 하나만 달라도 남남으로 취급합니다.
실수 ③: HTML 파일이 엉뚱한 폴더에 들어가 있을 때 (경로 이탈)
타임리프(Thymeleaf) 같은 템플릿 엔진은 아주 융통성이 없습니다. 오직 자기가 정해둔 특정 골방(폴더) 안에서만 접시를 찾습니다.
- 타임리프의 고정 탐색 경로: src/main/resources/templates/
- 🚨 초보자 실수: HTML 파일을 src/main/resources/static/ 폴더에 넣었거나, 아예 java 폴더 안에 넣어두는 경우가 많습니다.
- ✅ 해결법: 화면을 구성하는 HTML 파일은 무조건 resources/templates/ 폴더 바로 아래(혹은 그 안의 하위 폴더)에 위치해야 합니다. 만약 하위 폴더에 넣었다면 return "user/index"; 처럼 폴더 경로까지 꼼꼼하게 명시해 주어야 합니다.
💡 [데브프리의 실무 꿀팁: return을 안 적었는데(void) 왜 에러가 날까요? (ViewNameTranslator)]
정말 화면을 띄울 생각이 없고 단순 로직만 실행하려고, 컨트롤러 메서드를 public void test() 처럼 리턴 값 없이(void) 만들었는데 이 에러가 터져서 억울해하는 초보자들이 꽤 많습니다.
스프링 부트의 @Controller는 리턴 값이 void이면, '요청받은 URL 주소'를 템플릿 이름으로 찰떡같이(혹은 눈치 없게) 유추해 버립니다!
즉, @GetMapping("/api/test")로 들어왔는데 리턴이 없으면, 스프링은 "아! api/test.html이라는 화면을 보여달라는 뜻이구나!" 하고 템플릿 폴더를 뒤지다가 에러를 뱉어냅니다.
데이터를 안 주더라도 단순히 처리만 하고 끝나는 API라면, 반드시 @RestController를 달아주거나 빈 상태 코드(ResponseEntity.ok().build())를 반환하도록 설계해야 이 황당한 유령 접시 에러를 피할 수 있습니다!
3. 요약: 트러블슈팅 체크리스트
TemplateInputException (Error resolving template) 에러가 떴다면 당황하지 말고 아래 3가지를 순서대로 점검하세요.
- 데이터만 보낼 것인가? 화면을 띄울 것인가? ➡️ 데이터(문자열, JSON)만 보내는 API라면 @Controller 대신 @RestController를 사용했는지 확인합니다.
- 리턴 값과 파일명이 정확히 일치하는가? ➡️ return "파일명" 의 글자와 실제 HTML 파일의 이름(대소문자 포함)이 완벽하게 똑같은지 확인합니다.
- 파일이 올바른 폴더에 있는가? ➡️ HTML 파일이 resources/static이 아닌 resources/templates 폴더 안에 정확히 들어가 있는지 확인합니다.
마무리
스프링 부트는 개발자를 편하게 해주기 위해 뒤에서 수많은 '자동화' 작업을 대신 해줍니다. return "문자열"만 쳐도 알아서 HTML 파일을 뒤져서 화면을 만들어주는 마법 같은 기능도 그중 하나죠.
하지만 그 자동화 기능이 '어떤 규칙'으로 움직이는지 모른다면, 오늘처럼 "왜 내 글자를 안 띄워주고 없는 파일을 찾는 거야?" 라며 당황하게 됩니다.
오늘 배운 @Controller와 @RestController의 차이, 그리고 타임리프의 템플릿 경로 탐색 원리를 이해하셨다면, 앞으로 화면 연동에서 나오는 500 에러는 1분 만에 웃으며 넘기실 수 있을 겁니다.
오늘 준비한 가이드가 여러분의 붉은 에러 창을 깨끗하게 지워주었기를 바랍니다. 막히는 부분이 있다면 언제든 댓글로 남겨주세요.
지금까지 데브프리였습니다. 감사합니다!