Controller에서 Thymeleaf 화면까지: Spring MVC View 처리 이해

최정윤·2026년 6월 30일

Spring

목록 보기
6/38

웹 어플리케이션은 사용자의 요청을 받은 뒤, 그 결과를 다시 사용자가 이해할 수 있는 형태로 응답해야 한다. 이때 응답 형태는 크게 두 가지로 나눌 수 있다.

하나는 서버가 HTML 화면까지 완성해서 브라우저에 보내는 방식이고, 다른 하나는 서버가 JSON 데이터만 보내고 화면 구성은 프론트엔드가 담당하는 방식이다.

이번 실습은 첫번째 방식에 해당한다. 백엔드 서버가 Controller에서 데이터를 준비하고, Model에 담은뒤, Thymleaf를 통해 HTML화면을 완성해서 브라우저에 응답하는 구조를 확인했다.

핵심 흐름:

GET /movies 요청
→ Controller 실행
→ 랜덤 영화 선택
→ Model에 데이터 저장
→ return "movies"
→ ViewResolver가 movies.html 탐색
→ Thymeleaf가 HTML 완성
→ 브라우저에 화면 응답

이 흐름을 보면 return "movies"가 단순 문자열이 아니라 View 이름이라는 점, Model이 Controller와 View 사이에서 데이터를 전달한다는 점, Thymeleaf가 서버에서 HTML을 완성한다는 점을 연결해서 이해할 수 있다. ViewResolver는 Controller가 반환한 논리적인 View 이름을 실제 View 파일로 바꿔주는 역할을 한다.


웹 요청을 화면으로 바꾸는 문제

브라우저에서 다음 주소로 접속한다고 가정한다:

localhost:8080/movies

브라우저 입장에서는 /movies 라는 URL로 요청을 보낸 것이다.
하지만 서버 입장에서는 이 요청을 어느 Java 메서드가 처리해야 하는지, 어떤 데이터를 준비해야 하는지, 어떤 HTML 파일을 사용해 화면을 만들어야 하는지 결정해야 한다.

Spring MVC는 이 문제를 다음 구조로 해결한다.

역할담당하는 일
DispatcherServlet들어온 HTTP 요청을 Spring MVC 흐름으로 전달
HandlerMapping요청 URL과 실행할 Controller 메서드 연결
Controller요청 처리, 데이터 준비, View 이름 반환
ModelController에서 View로 보낼 데이터 저장
ViewResolver논리 View 이름을 실제 HTML 파일 경로로 변환
ThymeleafModel 데이터를 HTML에 반영
Browser완성된 HTML을 화면에 표시

즉, Spring MVC는 요청을 받은 뒤 바로 HTML을 보여주는 것이 아니라, 여러 단계를 거쳐 화면을 완성한다.


논리 View 이름과 실제 HTML 파일의 연결

Controller 메서드의 마지막에는 다음 코드가 있다.

return "movies";

처음 보면 "movies"라는 문자열이 브라우저에 그대로 출력될 것처럼 보일 수 있다. 하지만 @Controller가 붙은 클래스에서 문자열을 반환하면, Spring MVC는 이 값을 논리적인 View 이름으로 해석한다.

실제 연결은 다음과 같다:

논리 View 이름: "movies"
        ↓ 
        
ViewResolver
        
        ↓
실제 View 파일: resources/templates/movies.html

Spring Boot에서 Thymeleaf 의존성이 추가되어 있으면 ThymeleafViewResolver가 자동으로 설정된다. 이 ViewResolver는 기본적으로 resources/templates 아래에서 .html 파일을 찾는다. 그래서 Controller는 전체 경로를 쓰지 않고 "movies"라는 View 이름만 반환하면 된다.

정리하면:

구분작성 방식
실제 파일 이름movies.html
실제 파일 위치src/main/resources/templates/movies.html
Controller 반환값return "movies";
ViewResolver가 찾는 결과templates/movies.html

주의할 점은 실제 파일에는 .html을 붙여야 하지만, Controller의 반환값에는 보통 .html을 붙이지 않는다는 것이다.

return "movies";      // 적절한 방식
return "movies.html"; // 기본 설정에서는 권장하지 않음

기본 설정에서는 ViewResolver가 prefix와 suffix를 조합해서 파일을 찾는다.

templates/ + movies + .html

따라서 return "movies"는 templates/movies.html을 찾으라는 신호로 이해하면 된다.

MovieController.java, movies.html, Spring Boot 실행 로그가 함께 보인다. @GetMapping("/movies"), model.addAttribute(...), return "movies"가 한 화면에 있어 Controller가 View에 데이터를 넘기고 View 이름을 반환하는 구조를 확인할 수 있다.


Controller에서 View로 데이터를 넘기는 방식

화면에 보여줄 데이터는 Controller에서 직접 HTML에 넣는 것이 아니다. Controller는 데이터를 Model이라는 전달 상자에 담고, View는 그 데이터를 꺼내 사용한다.

핵심 코드는 다음과 같다:

@GetMapping("/movies")
public String getMovieRecommendation(Model model) {
    String recommendedMovie = getMovieRecommendation();

    model.addAttribute("title", "🎬 영화 추천 결과");
    model.addAttribute("recommendedMovie", recommendedMovie);
    model.addAttribute("totalMovies", recommendedMovies.size());
    model.addAttribute("allMovies", recommendedMovies);

    return "movies";
}

이 코드는 URL 요청 처리, Model 데이터 저장, View 이름 반환이라는 핵심 흐름을 모두 포함하고 있다.

각 코드의 의미를 살짝 분석해보았다.

코드의미
@GetMapping("/movies")/movies로 GET 요청이 오면 이 메서드를 실행
Model modelView로 전달할 데이터를 담는 상자
model.addAttribute("title", 값)HTML에서 ${title}로 꺼내 쓸 데이터 저장
model.addAttribute("recommendedMovie", 값)추천 영화 제목을 View에 전달
return "movies"templates/movies.html을 찾기 위한 논리 View 이름 반환

model.addAttribute("키", 값)에서 첫 번째 값은 HTML에서 사용할 이름이고, 두 번째 값은 실제 데이터다. 예를 들어 다음 코드가 실행되면,

model.addAttribute("recommendedMovie", "기생충");

HTML에서는 다음과 같이 값을 꺼낼 수 있다.

<p th:text="${recommendedMovie}">영화 제목</p>

Thymeleaf가 서버에서 이 코드를 처리하면 브라우저는 최종적으로 다음 HTML을 받는다.

<p>기생충</p>

즉, 브라우저가 th:text를 직접 실행하는 것이 아니다. Thymeleaf 문법은 서버에서 먼저 처리되고, 브라우저는 처리 완료된 HTML만 받는다.


버튼을 누를 때 마다 영화가 바뀌는 이유

실행 화면에서는 다른 영화 추천받기 버튼이 있다.
이 버튼을 누르면 기존 화면의 텍스트만 바뀌는 것이 아니라, 브라우저가 서버에 다시 요청을 보낸다.

<a th:href="@{/movies}">다른 영화 추천받기</a>

여기서 @{/movies}는 Thymeleaf의 URL 표현식이다. 서버의 컨텍스트 경로를 고려해 /movies 요청 경로를 만들어준다.

다른 영화 추천받기 버튼을 클릭 후의 흐름은 다음과 같다:

사용자가 버튼 클릭
        ↓
브라우저가 GET /movies 요청 전송
        ↓
Spring이 @GetMapping("/movies") 메서드 실행
        ↓
랜덤 영화 선택 메서드 호출
        ↓
Model에 새 영화 제목 저장
        ↓
return "movies"
        ↓
ViewResolver가 movies.html 탐색
        ↓
Thymeleaf가 새 데이터를 HTML에 반영
        ↓
브라우저에 새 화면 표시

핵심은 버튼 클릭 = 새 GET 요청이라는 점이다. 새 요청이 발생하면 Controller 메서드가 다시 실행되고, 랜덤 영화 선택 로직도 다시 실행된다. 그래서 처음에는 인생은 아름다워가 보였다가, 버튼을 누른 뒤에는 기생충이 보였다.

버튼 클릭부터 GET 요청, Controller 재실행, 랜덤 영화 선택, Model 저장, ViewResolver 처리, Thymeleaf 렌더링, HTML 응답까지의 흐름을 세로형으로 정리한 이미지다.


첫번째 영화 추천 결과 화면

localhost:8080/movies 접속 후 인생은 아름다워가 추천 영화로 표시된 화면이다. Controller에서 Model에 담은 데이터가 Thymeleaf를 거쳐 브라우저 화면에 출력되었음을 보여준다.

버튼 클릭 후 변경된 영화 추천 결과

다른 영화 추천받기 버튼 클릭 후 기생충이 표시된 화면이다. 같은 URL로 다시 GET 요청이 발생하고, Controller가 다시 실행되어 새로운 랜덤 결과가 반영되었음을 보여준다.


직접 Servlet을 작성하지 않는 구조

이전 Servlet 방식에서는 개발자가 요청과 응답을 더 직접적으로 다뤄야 했다. JSON 응답을 만들기 위해 응답 타입을 설정하고, ObjectMapper로 객체를 JSON문자열로 바꾸고, 응답 본문에 직접 작성하는 코드가 필요했다.

Spring MVC에서는 개발자가 직접 HttpServlet을 작성하지 않고도 요청을 Controller 메서드에 연결할 수 있었다.

@GetMapping("/movies")
public String getMovieRecommendation(Model model) {
    ...
    return "movies";
}

이 구조 덕분에 반복적인 요청·응답 처리 코드보다 다음 흐름에 집중할 수 있었다.

구분직접 Servlet 작성 방식Spring MVC Controller 방식
요청 매핑Servlet URL 매핑을 직접 관리@GetMapping으로 메서드에 연결
응답 처리응답 객체에 직접 작성View 이름 또는 데이터를 반환
JSON 변환ObjectMapper 등을 직접 사용API 방식에서는 Spring이 변환 지원
화면 처리직접 HTML 작성 또는 별도 처리 필요ViewResolver와 Thymeleaf 사용
집중할 부분요청·응답 세부 처리요청 흐름, 데이터 준비, View 연결

Spring MVC가 Servlet을 전혀 사용하지 않는 것은 아니다.

내부적으로는 DispatcherServlet이 요청을 받아 Spring MVC 흐름으로 넘긴다. 정확히는 직접 Servlet을 작성하지 않아도 되도록 Spring MVC가 Servlet 기반 요청 처리를 감싸준다고 이해해야 한다.


Java 문법과 Spring MVC 코드의 연결

이번 실습은 Spring MVC 흐름뿐 아니라 Java 문법이 실제 코드에서 어떻게 쓰이는지 확인하기에도 적절하다. 특히 같은 이름의 메서드가 두 개 등장한다.

public String getMovieRecommendation(Model model)
private String getMovieRecommendation()

이 두 메서드는 이름이 같지만 매개변수가 다르다. Java에서는 메서드 이름이 같아도 매개변수 목록이 다르면 서로 다른 메서드로 구분한다. 이를 오버로딩이라고 한다.

메서드매개변수역할
getMovieRecommendation(Model model)Model model 있음/movies 요청을 처리하는 Controller 메서드
getMovieRecommendation()없음랜덤 영화 제목 하나를 반환하는 내부 helper 메서드

여기서 중요한 점은 Java가 메서드를 구분할 때 반환 타입이 아니라 메서드 이름과 매개변수 목록을 본다는 것이다.

영화 목록과 랜덤 선택 로직에서는 다음 Java 문법이 사용되었다.

Java 문법역할
List<String>문자열 영화 제목만 담을 수 있는 리스트
List.of(...)수정 불가능한 리스트 생성
final해당 필드가 다른 객체를 가리키도록 재할당하는 것을 막음
Random.nextInt(size)0 이상 size 미만의 랜덤 정수 생성
recommendedMovies.get(index)리스트에서 해당 인덱스의 값 반환
private클래스 내부에서만 사용하는 보조 로직으로 제한

주의할 부분도 있다. final List라고 해서 무조건 리스트 내부 값이 수정 불가능하다는 뜻은 아니다. final은 변수 재할당을 막는 역할이다. 이 코드에서 영화 목록에 추가나 삭제가 불가능한 직접적인 이유는 List.of(...)가 수정 불가능한 리스트를 만들기 때문이다.

즉, 이 코드에서는 final과 List.of(...)가 함께 사용되어 영화 목록이 고정된 형태가 되었다.


Thymeleaf 문법은 핵심만 이해한다

Thymeleaf는 HTML 안에서 서버 데이터를 사용할 수 있게 해주는 템플릿 엔진이다.

Thymeleaf 문법역할Java로 비유
th:text태그 안의 텍스트를 서버 데이터로 변경값 출력
th:href링크 주소를 동적으로 설정URL 문자열 생성
th:src이미지 주소를 동적으로 설정이미지 경로 대입
th:each컬렉션 데이터를 반복 출력for-each 반복문
th:if조건이 참일 때만 태그 출력if
th:unless조건이 거짓일 때만 태그 출력if (!condition)

Thymeleaf의 기본 문법에는 텍스트 출력, 속성 설정, 반복 처리, 조건 처리 등이 포함된다.

특히 th:if와 th:unless는 단순히 화면에서 숨기는 기능으로만 보면 안 된다. 조건에 맞지 않으면 최종 HTML에 태그 자체가 포함되지 않을 수 있다. CSS의 display: none이 “화면에서 숨김”에 가깝다면, Thymeleaf 조건문은 “HTML 생성 단계에서 포함 여부를 결정”하는 방식에 가깝다.


실습 중 점검 기준

View 처리에서 문제가 생기면 처음부터 복잡하게 접근하기보다 아래 세 가지를 먼저 확인하는 것이 효율적이다.

상황먼저 확인할 것
Whitelabel Error Page브라우저 URL /movies@GetMapping("/movies")가 일치하는가
Template not foundreturn "movies"templates/movies.html 이름이 일치하는가
값이 화면에 안 보임model.addAttribute("key", 값)의 key와 HTML의 ${key}가 일치하는가

Spring MVC View 처리에서 가장 헷갈렸던 부분은 파일 위치, View 이름, Model key 이름이었다.

특히 다음 세 가지는 반드시 맞아야 한다.

URL: /movies
Controller: @GetMapping("/movies")
View file: resources/templates/movies.html

그리고 Model key도 정확히 일치해야 한다.

model.addAttribute("recommendedMovie", recommendedMovie);
th:text="${recommendedMovie}" 

recommendedMovie라는 이름이 한 글자라도 다르면 원하는 값이 출력되지 않는다.


백엔드 개발자가 Thymeleaf에 매몰되면 안 되는 이유

Thymeleaf는 서버에서 HTML을 완성해 브라우저에 보내는 서버 사이드 렌더링 방식이다. 이 방식에서는 백엔드가 화면까지 만들어 응답한다.

하지만 현대적인 웹 서비스에서는 프론트엔드와 백엔드가 분리되는 경우가 많다. React, Vue 같은 SPA 방식이 널리 사용되면서 프론트엔드는 화면 구성, 라우팅, 상태 관리를 담당하고, 백엔드는 HTML을 직접 만드는 대신 JSON 데이터를 제공하는 API 서버 역할에 집중하게 되었다. SPA 방식이 주류가 되면서 백엔드의 역할은 HTML 생성보다 데이터 제공에 가까워졌다고 볼 수 있다.

따라서 Thymeleaf는 깊게 외울 대상이라기보다 Spring MVC의 화면 반환 흐름을 이해하기 위한 도구로 보는 것이 적절하다.

구분Thymeleaf 방식JSON API 방식
Controller 역할View 이름 반환JSON 데이터 반환
응답 형태HTMLJSON
화면 처리서버가 HTML 완성프론트엔드가 화면 구성
주로 사용하는 어노테이션@Controller@RestController
데이터 전달 개념ModelDTO
백엔드 실무 방향일부 SSR 프로젝트, 관리자 페이지REST API 서버 중심

여기서 중요한 연결점은 @Controller와 @RestController의 차이다.

구분@Controller@RestController
주 용도HTML 화면 반환JSON 데이터 반환
반환값 해석View 이름응답 본문 데이터
예시 반환값return "movies";return new MovieResponse(...);
연결 개념ViewResolver, ThymeleafDTO, JSON 응답

Spring MVC는 여전히 중요한가

백엔드가 HTML을 직접 만드는 일이 줄었다고 해서 Spring MVC가 의미 없어지는 것은 아니다. View의 상당 부분이 프론트엔드로 이동했더라도, 요청을 받고 적절한 Controller로 연결하며 데이터를 응답하는 구조는 여전히 백엔드의 핵심이다.

다만 역할이 바뀌었다.

MVC 요소전통적인 View 반환 방식현대적인 API 서버 방식
ControllerView 이름 반환API 엔드포인트 역할
ModelView에 전달할 데이터응답 DTO로 확장
View서버가 HTML 생성프론트엔드가 화면 구성

즉, Spring MVC를 배운다는 것은 단순히 Thymeleaf 화면을 만드는 법을 배우는 것이 아니다. 요청이 Controller로 들어오고, 서버가 데이터를 준비하고, 적절한 응답을 만들어 반환하는 백엔드 흐름을 이해하는 것이다.


실습 결과 정리

이번 실습의 결과는 단순히 영화 추천 화면을 띄운 것이 아니다. 핵심 결과는 다음 세 가지다.

첫째, /movies 요청이 @GetMapping("/movies")가 붙은 Controller 메서드로 연결되는 흐름을 확인했다.

둘째, Controller에서 만든 데이터를 Model에 담고, Thymeleaf HTML에서 ${recommendedMovie}로 꺼내 화면에 출력하는 흐름을 확인했다.

셋째, return "movies"가 문자열 응답이 아니라 ViewResolver를 통해 templates/movies.html과 연결되는 논리 View 이름이라는 점을 확인했다.

최종 실행 흐름:


마무리

Spring MVC의 View 처리 방식은 겉으로 보면 return "movies" 한 줄로 화면이 나오는 것처럼 보인다. 하지만 실제 흐름은 브라우저 요청, Controller 실행, Model 데이터 저장, ViewResolver의 View 탐색, Thymeleaf 렌더링, HTML 응답으로 이어진다.

이번 실습에서 가장 중요한 개념은 세 가지다.
1. return "movies"는 문자열 응답이 아니라 논리적인 View 이름이다.
2. Model은 Controller에서 View로 데이터를 전달하는 상자다.
3. Thymeleaf는 서버에서 Model 데이터를 HTML에 반영해 브라우저가 받을 최종 HTML을 완성한다.

또한 직접 Servlet을 작성하지 않아도 Spring MVC가 요청 매핑과 View 연결을 처리해주기 때문에, 개발자는 반복적인 요청·응답 코드보다 요청 흐름과 데이터 전달 구조에 집중할 수 있다. 다만 Spring MVC도 내부적으로는 Servlet 기반 위에서 동작하므로, “Servlet을 사용하지 않는다”가 아니라 “직접 Servlet을 작성하지 않는다”라고 이해하는 것이 정확하다.

백엔드 개발자 관점에서는 HTML과 CSS 디자인에 깊게 매몰되기보다, Controller가 요청을 받고 데이터를 준비한 뒤 어떤 방식으로 응답을 만드는지 이해하는 것이 중요하다.

profile
콩떡

0개의 댓글