입문 Spring

정예진·2026년 4월 9일

Spring

목록 보기
3/19

2026.04.09

2-1 MVC 패턴 기초와 역할

📚 오늘 배운 것

MVC 패턴

MVC 패턴은 하나의 Servlet이 혼자 모든 것을 처리하던 문제를 해결하기 위해 애플리케이션의 코드를 3가지 역할로 명확하게 나누는 설계 방식임.

  • Model : 데이터와 비즈니스 로직을 담당
  • View : 사용자에게 보여지는 화명을 담당
  • Controller : 사용자의 요청을 받아 Model과 View를 연결해주는 중간 다리 역할

Thymeleaf

타임리프는 서버에서 HTML을 동적으로 만들어주는 자바 기반의 도구, 즉 템플릿 엔진임.

주된 역할

자바 코드(스프링 컨트롤러)가 보내주는 데이터를 실제 HTML 코드와 결합해서 내용이 채워진 완성된 웹 페이지를 만드는 것.

SSR 과 CSR (면접에서 자주 물어봄!)

SSR(Server Side Rendering)은 서버가 웹 페이지에 필요한 모든 데이터를 채워 넣어 완벽하게 조립된 HTML 페이지를 브라우저에게 전달하는 방식. (타임리프가 SSR 방식임.)

사용자는 거의 즉시 콘텐츠를 볼 수 있어 초기 로딩 속도가 빠르고, 모든 내용이 HTML에 포함되어 있어 검색 엔진 최적화(SEO)에 유리함.

CSR(Client Side Rendering)은 서버가 거의 텅 빈 HTML 뼈대와 자바스크립트 파일만 보내주는 방식. 그래서 사용자 브라우저(클라이언트)가 자바스크립트를 실행해 필요한 데이터를 서버에 다시 요청하고, 브라우저가 직접 페이지를 조립하여 화면에 그림.

CSR은 초기 로딩은 느릴 수 있지만, 이후에는 앱처럼 부드럽고 빠른 화면 전환이 가능함.


2-2 프론트 컨트롤러 패턴과 어댑터 패턴, 그리고 DispatcherServlet

📚 오늘 배운 것

프론트 컨트롤러 패턴(Front Controller Pattern)

프론트 컨트롤러 패턴은 모든 클라이언트 요청을 단일 진입점(Single Point of Entry)에서 처리하는 디자인 패턴임.

요청에 대한 공통 처리(보안, 로깅, 인코딩 등)를 중앙에서 효율적으로 관리할 수 있고, 개별 요청을 처리할 핸들러(Controller)로 작업을 위임하는 역할을 함.

어댑터 패턴(Adapter Pattern)

어댑터 패턴은 서로 다른 인터페이스를 가진 클래스들을 연결해주는 패턴임.

즉 다형성을 잘 활용한 패턴이라고 볼 수 있음.

// HandlerAdapter 인터페이스 (공통 인터페이스)
public interface HandlerAdapter {
    boolean supports(Object handler); // 처리 가능 여부 판단(boolean)
    
    ModelAndView handle( // 어떻게 처리할 것인지에 대한 로직
      HttpServletRequest request,
      HttpServletResponse response, 
      Object handler) throws Exception;
}

// Controller 구현 1 (HandlerAdapter를 implements)
public class RequestMappingHandlerAdapter implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return handler instanceof HandlerMethod;  // @RequestMapping 메서드인지 확인
    }
    
    @Override
    public ModelAndView handle(HttpServletRequest request, 
                             HttpServletResponse response, 
                             Object handler) {
        // @Controller 방식으로 처리
        return invokeHandlerMethod((HandlerMethod) handler, request, response);
    }
}

// Controller 구현 2 (HandlerAdapter를 implements)
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return handler instanceof Controller;
    }
    
    @Override
    public ModelAndView handle(HttpServletRequest request, 
                             HttpServletResponse response, 
                             Object handler) {
        // Controller 인터페이스 방식으로 처리
        return ((Controller) handler).handleRequest(request, response);
    }
}

DispatcherServlet

DispatcherServlet은 프론트 컨트롤러 패턴(Front Controller Pattern)을 구현한 스프링 MVC의 핵심적인 프론트 컨트롤러임.

① 요청

  • 클라이언트가 웹 애플리케이션에 요청(Request)을 보내고, 이 요청은 가장 먼저 DispatcherServlet에 도달합니다.

② 핸들러 조회

  • DispatcherServlet은 HandlerMapping에게 요청을 처리할 Handler(=Controller)를 찾아달라고 요청합니다.

③ 핸들러 실행

  • DispatcherServlet은 HandlerMapping으로부터 받은 정보를 이용해 해당 Controller에게 요청 처리를 위임합니다.

④ ModelAndView 반환

  • Controller는 비즈니스 로직을 수행한 후, 결과 데이터(Model)와 뷰의 논리적 이름(View Name)을 담은 ModelAndView 객체를 DispatcherServlet에 반환합니다.

⑤ 뷰 해석

  • DispatcherServlet은 ModelAndView에서 뷰 이름을 추출하여 ViewResolver에게 전달하고, 해당하는 실제 View 객체를 찾아달라고 요청합니다.

⑥ 뷰 렌더링

  • DispatcherServlet은 ViewResolver로부터 받은 View 객체에게 모델 데이터를 전달하여 뷰를 렌더링하도록 요청합니다.

⑦ 응답

  • 렌더링된 View의 결과물이 DispatcherServlet을 통해 클라이언트에게 최종적으로 응답(Response)으로 전달됩니다.

/hello 예제

hello 프로젝트를 생성하는데 롬복, 스프링 웹, 타임리프를 의존성으로 넣어주고 만들어줌.

HelloController 클래스를 만들고 아래 코드를 채워줌. 필요한 임포트는 해줘야함!

@Controller
public class HelloController {
    
    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("message", "Hello, Spring!");
        return "hello";  // view
    }
}

그리고 templates 경로에 hello.html 파일을 하나 만들어주고 아래 코드를 채워줌.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello Page</title>
</head>
<body>
    <h1 th:text="${message}">(메시지가 보여지는 곳)</h1>
</body>
</html>

스프링을 실행 시키고 아래 링크로 들어가보면

http://localhost:8080/hello

이런게 나옴…! 귀엽 ㅎ


2-3 ViewResolver와 View 처리

📚 오늘 배운 것

ViewResolver의 역할

ViewResolver는 컨트롤러가 반환한 논리적인 뷰 이름실제 뷰로 바꿔주는 번역기 역할을 함.

이전 강의에서 실습했던 HelloController의 코드로 보면

@Controller
public class HelloController {
    
    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("message", "Hello, Spring!");
        return "hello";  // view
    }
}

위 코드에서 return "hello"; 코드는 ViewResolver에 의해 ”/templates/hello.html”이라는 실제 뷰 파일 경로와 매핑됨.

여기서 “hello”는 논리적인 뷰 이름이 되는 것이며, “/templates/hello.html”는 실제 뷰가 되는 것임. (사실 뭔 말인지 잘 모르겠음.)

Model 객체를 통한 데이터 전달

Model은 Controller에서 View로 데이터를 전달하기 위한 Spring MVC의 핵심 인터페이스임.

위 코드에서 model.addAttribute("message", "Hello, Spring!"); 부분이 바로 Model 객체를 활용하는 코드임.

model.addAttribute("키_이름",);

이걸 보면 우선은 첫 번째 파라미터 (키, Key) : View에서 사용할 변수명, 두 번째 파라미터 (값, Value) : 전달할 데이터

즉, model.addAttribute("message", "Hello, Spring!"); 이 코드에서는 Key가 “message”이고, Value가 “Hello, Spring!” 인 Model 객체를 세팅한 것임.

그럼 프론트에서 보여지는 View에서 “message” 키로부터 “Hello, Spring!” 이라는 값을 불러와서 유저에게 데이터를 보여줄 수 있게 되는 것.

Thymeleaf 기본 문법 (프론트 영역이라서 깊게 알 필요는 없음)

  1. 텍스트 출력(th:text)

    th:text 는 HTML 태그의 텍스트 내용을 서버에서 전달된 데이터로 변경하는 데 사용됨.

    <p>기본 텍스트</p>가 있더라도 th:text 안의 값이 그 자리를 완전히 대체함.

    <!-- 기본 사용법 -->
    <h1 th:text="${message}">기본 메시지</h1>
    <!-- 결과: <h1>Hello, Spring MVC!</h1> -->
    
    <!-- 문자열 결합 -->
    <p th:text="'안녕하세요, ' + ${name} + '님!'">안녕하세요, 사용자님!</p>
    
    <!-- 조건부 텍스트 -->
    <span th:text="${user != null ? user.name : '게스트'}">사용자</span>
  2. 속성 설정(th:href, th:src, th:class)

    th:* 문법을 사용하면 href, src, class 등 HTML 태그의 다양한 속성 값을 동적으로 설정할 수 있음.

    <!-- 링크 생성 -->
    <a th:href="@{/user/profile(id=${user.id})}">프로필 보기</a>
    <!-- 결과: <a href="/user/profile?id=123">프로필 보기</a> -->
    
    <!-- 이미지 경로 -->
    <img th:src="@{/images/logo.png}" alt="로고">
    
    <!-- CSS 클래스 조건부 적용 -->
    <div th:class="${user.isActive ? 'active' : 'inactive'}">상태</div>
  3. 반복 처리(th:each)

    th:each는 Java의 for-each 루프처럼 컬렉션(List, Set 등)의 데이터를 반복하여 여러 개의 HTML 요소를 생성할 때 사용함.

    <!-- 리스트 반복 -->
    <ul>
        <li th:each="product : ${products}" th:text="${product.name}">상품명</li>
    </ul>
    
    <!-- 인덱스와 함께 반복 -->
    <tr th:each="user, status : ${users}">
        <td th:text="${status.index + 1}">번호</td>
        <td th:text="${user.name}">이름</td>
    </tr>
  4. 조건 처리(th:if, th:unless)

    th:ifth:unless는 특정 조건이 참이거나 거짓일 때 HTML 블록을 렌더링하거나 렌더링하지 않도록 제어하는 속성임.

    <!-- 조건부 표시 -->
    <div th:if="${user.isAdmin}">관리자 메뉴</div>
    <div th:unless="${user.isAdmin}">일반 사용자 메뉴</div>
    
    <!-- 삼항 연산자 활용 -->
    <span th:class="${user.isOnline ? 'online' : 'offline'}" 
          th:text="${user.isOnline ? '온라인' : '오프라인'}">상태</span>

Thymeleaf 실습

movies 라는 새로운 프로젝트를 만들어주고 Lombok, Spring Web, Thymeleaf 넣어서 세팅해줌.

MovieController 클래스 만들어주고

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;
import java.util.Random;

@Controller
public class MovieController {

    @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";  // templates/movies.html 로 매핑
    }

    private final List<String> recommendedMovies = List.of(
            "쇼생크 탈출",
            "대부",
            "다크 나이트",
            "인생은 아름다워",
            "기생충",
            "타이타닉",
            "아바타",
            "어벤져스: 엔드게임"
    );

    private final Random random = new Random();

    private String getMovieRecommendation() {
        int randomIndex = random.nextInt(recommendedMovies.size());
        return recommendedMovies.get(randomIndex);
    }
}

이 코드를 넣어주고, movies.html 파일을 만들어줌. 그 안에는 아래 코드를 넣음.

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
      th:with="
        m=${#strings.trim(recommendedMovie?:'')},
        posterUrl=${
            m=='쇼생크 탈출' ? 'https://github.com/user-attachments/assets/4eea166b-65ef-4360-8ec1-402b70cf8817' :
            m=='대부' ? 'https://github.com/user-attachments/assets/a1910767-2cf6-485e-8816-9aae3c9f5fc2' :
            m=='다크 나이트' ? 'https://github.com/user-attachments/assets/dd172069-78e7-4945-b637-4a8ef10df995' :
            m=='인생은 아름다워' ? 'https://github.com/user-attachments/assets/93ea654a-906b-4516-8d47-91b7a63516cb' :
            m=='기생충' ? 'https://github.com/user-attachments/assets/cb4d5ded-0442-4fde-92d2-341c0a64bbb5' :
            m=='타이타닉' ? 'https://github.com/user-attachments/assets/1e67b339-2137-46f3-8642-24ae7a9be114' :
            m=='아바타' ? 'https://github.com/user-attachments/assets/f30ac710-7103-4fd4-8fd0-010c616e55f4' :
            m=='어벤져스: 엔드게임' ? 'https://github.com/user-attachments/assets/0afe4084-2db7-4465-bf14-1791e746e3c3' :
            null
        }">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title th:text="${title}">영화 추천</title>
    <link rel="dns-prefetch" href="https://github.com"/>
    <link rel="preconnect" href="https://github.com" crossorigin>
    <link rel="dns-prefetch" href="https://private-user-images.githubusercontent.com"/>
    <link rel="preconnect" href="https://private-user-images.githubusercontent.com" crossorigin>
    <link rel="dns-prefetch" href="https://user-images.githubusercontent.com"/>
    <link rel="preconnect" href="https://user-images.githubusercontent.com" crossorigin>
    <link rel="preload" as="image" th:if="${posterUrl}" th:href="${posterUrl}">
    <style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f8f9fa;min-height:100vh;display:flex;align-items:center;justify-content:center;color:#2c3e50}.container{max-width:500px;width:90%;background:#fff;border-radius:16px;box-shadow:0 4px 20px rgba(0,0,0,.08);padding:40px 30px;text-align:center}.title{font-size:1.8em;font-weight:600;margin-bottom:30px;color:#2c3e50}.movie-poster{width:400px;height:600px;margin:0 auto 30px;border-radius:12px;overflow:hidden;box-shadow:0 8px 25px rgba(0,0,0,.15);background:#e9ecef;display:flex;align-items:center;justify-content:center;color:#6c757d;position:relative}.movie-poster img{width:100%;height:100%;object-fit:cover;object-position:center;transition:transform .3s ease}.movie-poster:hover img{transform:scale(1.05)}.recommendation-section{margin-bottom:30px}.recommendation-label{font-size:1.1em;color:#6c757d;margin-bottom:8px}.movie-title{font-size:2em;font-weight:700;color:#2c3e50;margin-bottom:10px}.movie-subtitle{color:#6c757d;font-size:1em}.btn{background:#007bff;color:#fff;border:none;padding:12px 24px;border-radius:8px;font-size:1em;font-weight:500;cursor:pointer;text-decoration:none;display:inline-block;transition:all .2s ease}.btn:hover{background:#0056b3;transform:translateY(-1px)}.btn:active{transform:translateY(0)}</style>
    <script th:inline="javascript">
        (function(){
            var current = /*[[${posterUrl}]]*/ null;
            var urls = [
                'https://github.com/user-attachments/assets/4eea166b-65ef-4360-8ec1-402b70cf8817',
                'https://github.com/user-attachments/assets/a1910767-2cf6-485e-8816-9aae3c9f5fc2',
                'https://github.com/user-attachments/assets/dd172069-78e7-4945-b637-4a8ef10df995',
                'https://github.com/user-attachments/assets/93ea654a-906b-4516-8d47-91b7a63516cb',
                'https://github.com/user-attachments/assets/cb4d5ded-0442-4fde-92d2-341c0a64bbb5',
                'https://github.com/user-attachments/assets/1e67b339-2137-46f3-8642-24ae7a9be114',
                'https://github.com/user-attachments/assets/f30ac710-7103-4fd4-8fd0-010c616e55f4',
                'https://github.com/user-attachments/assets/0afe4084-2db7-4465-bf14-1791e746e3c3'
            ];
            for (var i=0; i<urls.length; i++) {
                var u = urls[i];
                if (!u || u === current) continue;
                var img = new Image();
                try { img.referrerPolicy = 'no-referrer'; } catch(e) {}
                img.decoding = 'async';
                img.src = u;
            }
        })();
    </script>
</head>
<body>
<div class="container">
    <h1 class="title" th:text="${title}">🎬 영화 추천 결과</h1>

    <div class="movie-poster">
        <img
                th:if="${posterUrl}"
                th:src="${posterUrl}"
                th:alt="${recommendedMovie}"
                loading="eager"
                fetchpriority="high"
                decoding="async"
                referrerpolicy="no-referrer"
                onerror="this.style.display='none'; this.parentElement.innerHTML='🎬 영화 포스터';"
        />
        <span th:unless="${posterUrl}">🎬 영화 포스터</span>
    </div>

    <div class="recommendation-section">
        <div class="recommendation-label">🎉 오늘의 추천 영화</div>
        <div class="movie-title" th:text="${recommendedMovie}">영화 제목</div>
        <div class="movie-subtitle">좋은 선택이에요! 즐거운 시청 되세요 🍿</div>
    </div>

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

실행을 시킨 뒤에

localhost:8080/movies

이 링크로 들어가보면

이렇게 영화 추천을 해줌…!


0개의 댓글