[SpringBoot] Thymeleaf와 ViewResolver

hwhyeons·2025년 8월 18일

Controller는 요청을 받아서 Model을 만들고, 적절한 View를 선택해 반환하는 역할을 한다.

컨트롤러는 return타입에 따라 View이름이 될 수도 있고,
@RestController면 HTTP ResponseBody일 수도 있다.

이번에 작성할 글은 그 중에서 View 이름인 경우다.

@Controller
public class PageController {
    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("msg", "안녕하세요");
        return "hello";
    }
}

이런 코드가 있을 때, 나는 위와 같이 리턴하면 "hello"가 templates/hello.html을 가리킨다고 알고 있었다.

틀린말은 아니지만, 문제는 중요한 전제를 나는 모르고 있었다.

해당 이름을 실제 템플릿으로 바꿔줄 ViewResolver가 있어야 된다는 것이다.

대표적으로 ThymeleafViewResolver가 있다.



문제 발생 원인

웹소켓을 사용하는 페이지를 타임리프로 간단하게 테스트 해보려고 하고 있었다.
타임리프 파싱을 위해서 templates폴더에 test_socket.html파일을 만들고
사용을 위해서 아래 테스트용 컨트롤러를 만들었다.

@GetMapping("/test-socket") // 접속할 URL 주소를 "/test-socket"으로 지정
public String socketTestPage() {
    return "test_socket"; // "templates/" 폴더 아래의 test_socket.html을 의미
}

이렇게 했고, 저 페이지에 접속하기 위해
http://127.0.0.1:8080/test-socket
으로 접속하였으나, 계속 404 오류가 발생했다.

어 왜 안되지?라고 생각했다.
내가 예전에 스프링부트를 공부하면서 정리해놨던 내용을 봤는데도
resources/templates폴더에 있는 html 파일을 따라가는게 맞다고 정리해놨었다.

아무리 봐도 오타도 없고 다 맞는데 자꾸 안되니, GPT에게 질문을 했다.

GPT는 여러 원인 후보를 알려주었다.
내가 오타를 내거나 경로가 src/resoruces/templates폴더가 맞는지 등등이랑,
타임리프 의존성이 추가 되어있는지를 보라는 것이였다.

이 답변을 보고 처음에 했던 생각은

타임리프가 의존성 없으면 타임리프 문법을 파싱을 못하긴 하겠지만
애초에 지금 내가 원하는 html파일의 경로를 인지를 전혀 못하는데 의존성이랑 무슨상관이지?

라고 생각했다.

즉, 타임리프 의존성 문제였으면 그건 html경로는 일단 제대로 잡고 그 이후에 파싱에 문제가 생기겠지,
지금처럼 파일 자체를 인식하지 못하는건 관련이 없다고 마음대로 판단을 했다.

특히, 거의 대부분의 스프링부트를 공부하고 간단한 프로젝트를 해보면서
타임리프는 그냥 기본으로 의존성에 추가하고 프로젝트를 만들었기 때문에 더욱 생각하지 못했다.

build.gradle파일을 제대로 확인도 안해보고 GPT에게 의존성 추가 제대로 되어있다고
원인을 제대로 찾아달라고 요청하고 있었다..

그러니 GPT는 답변으로 다른 원인 후보를 주는데 그게 될리가 없다.


결국 GPT말고 제미나이에게도 요청했더니,
역시 여러 원인이 될 수 있는 리스트를 주는데, 그 중에 역시 “타임리프 의존성 추가되어 있는지 확인”하라는
말이 있었다.

GPT뿐만 아니라 제미나이까지 이렇게 타임리프 의존성을 원인 중에 하나로 말하니,
설마 의존성이 없었나?싶어서 build.gradle을 확인해보니 진짜로 없었다..

이때까지만 해도 방금 생각했듯이,

타임리프가 의존성 없으면 타임리프 문법을 파싱을 못하긴 하겠지만
애초에 지금 내가 원하는 html파일의 경로를 인지를 전혀 못하는데 의존성이랑 무슨상관이지?

이렇게 생각하고 있었기 때문에 설마이게 원인이겠어?하는 마음으로 의존성 추가하고
다시 컨트롤러 url로 접속해보니 잘 되는 것이였다.



Thymeleaf와 ViewResolver

문제가 해결되었으니, 원인을 찾아보았다.

그 원인은, View이름으로 View를 만들어주는게 ViewResolver인데,
타임리프가 없으면 현재 내 프로젝트에서는

return "test_socket";

을 templates/test_socket.html로 연결해줄 뷰 리졸버가 없던 것이다.


그래서 타임리프 의존성을 제거한 상태와 다시 추가한 상태에서 ViewResolver목록들을 모두 출력해봤다

// ctx는 필드에 추가
//    @Autowired
//    private ApplicationContext ctx;

String[] names = ctx.getBeanNamesForType(ViewResolver.class);
System.out.println("=== Registered ViewResolvers ===");
for (String n : names) {
    ViewResolver vr = ctx.getBean(n, ViewResolver.class);
    System.out.println(n + " -> " + vr.getClass().getName());
}

이렇게 확인해봤다.

타임리프 의존성이 없을 때는

beanNameViewResolver
mvcViewResolver
defaultViewResolver
viewResolver

이렇게 4개만,
의존성에 있으면 thymeleafViewResolver가 추가된다

바로 이 thymeleafViewResolver가 return "test_socket"을
templates/test_socket.html로 연결해주고 있었던 것이다



참고 : 정적 리소스의 경우

참고로, 정적 리소스의 경우에는 ViewResolver을 거치지 않는다.

즉, static폴더에 있는 정적 HTML파일인 test.html을 보여주고 싶을 때는

return "test"

가 아니라

return "test.html"

로 해야한다.

static 폴더에 있는 html파일을 찾아가는건 정적 리소스 핸들러(ResourceHttpRequestHandler) 를 등록해서 처리한다. 즉, ViewResolver단계를 거치지 않는다.

0개의 댓글