Spring MVC/02.Spring @MVC

01. Spring MVC 개요

  • -

이번 시간에는 Spring MVC에 대해 살펴보자.  Spring MVC는 Front Controller 패턴을 적용한 웹 애플리케이션 개발 프로젝트이다. 이를 위해 필요한 요소들에 대해 살펴보자.

Spring MVC의 구성 요소

 

DispatcherServlet

Spring MVC는 스프링을 통해서 웹 MVC 애플리케이션을 개발하기 위한 아키텍쳐로 [Front Controller 패턴]을 사용한다. Front Controller 패턴은 모든 클라이언트의 요청을 단일 진입점인 Front Controller가  처리하는 패턴으로 요청 처리 전/후 공통 모듈을 처리하기 용이하다.

front controller pattern

Spring MVC가 프론트 컨트롤러 역할을 담당하는 Servlet은  DispatcherServlet 이다. 이 서블릿은 spring-webmvc에 이미 잘 작성되어 포함되어 있기 때문에 우리가 개발할 필요는 없다. 다만 DispatcherServlet의 동작을 이해해두는 것은 매우 중요하다. (면접에도 자주 나온다 ㅎ)

MVC에서 Controller는 길잡이 역할을 수행하는데 DispatcherServlet도 마찬가지이다. 클라이언트의 모든 요청을 접수하고 실제 동작은 하위 컨트롤러에게 위임해서 처리한다. 다만 하위 컨트롤러의 동작 방식이나 작성 형태가 상이할 수 있기 때문에 교체를 위한 유연성을 확보하기 위해 DispatcherServlet에서 하위 컨트롤러를 부르는 과정이 상당히 복잡하다. 이를 위해 여러 개의 인프라 구성요소(Infrastructure Component)들을 사용해서 하위 컨트롤러들을 느슨하게 관리한다.

 

인프라 구성요소(Infrastructure Components)

Infrastructure Component는 Spring MVC에서 애플리케이션의 다양한 기능을 지원하고 요청 처리를 효율적으로 관리하기 위해 사용되는 객체들이다. 일단 DispatcherServlet이 요청을 처리하기 위해 사용하는 3대장으로는 HandlerMapping, HandlerAdapter, ViewResolver가 있다. Handler라는 것은 서브 컨트롤러들로 웹과 관련된 여러 업무를 실제 처리하는 아주 중요한 녀석이고 Controller라고도 불린다.

  • HandlerMapping: 요청을 처리할 Handler가 누구인지를 DispatcherServlet에게 알려준다. HandlerMapping 덕분에 다양한 매핑 전략을 지원하는 유연한 URL 구성이 가능하다. 
    • RequestMappingHandlerMapping: @RequestMapping에 의해 빈 연결
    • BeanNameUrlHandlerMapping: 빈의 이름에 있는 URL을 요청의 URL과 비교해서 빈 연결
  • HandlerAdapter: 요청을 처리해줄 Handler를 연결하고 결과를 반환한다.
    • RequestMappingHandlerAdapter: @RequestMapping이 적용된 메서드를 호출해서 처리
    • SimpleControllerHandlerAdapter: Controller 인터페이스를 구현한 컨트롤러를 이용해서 처리
더보기
// HandlerMapping과 HandlerAdapter의 예

// RequestMappingHandlerMapping + RequestMappingHandlerAdapter 
@RequestMapping("/hello")               // RequestMappingHandlerMapping: path 속성이 url
public String helloControllerMethod(){...}

// BeanNameUrlHandlerMapping + SimpleControllerHandlerAdapter
@Bean(name = "/hello")                   // BeanNameUrlHandlerMapping: 빈 이름이 url
public Controller helloController() {
  return new Controller() {
    @Override
    public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse res){
      return new ModelAndView("helloView"); // 반환할 뷰 이름
    }
  };
}
  • ViewResolver사용자의 요청에 적합한 View를 반환해서 결과를 보여줄 수 있게 한다.
    • InternalResourceViewResolver: JSP와 같은 서버 측 뷰 기술을 사용할 때 사용. prefix와 suffix로 파일 경로 지정
    • BeanNameViewResolver: 빈 이름을 뷰의 이름으로 사용하여 직접 매핑
    • ContentNegotiatingViewResolver: 다양한 컨텐트 타입(excel, pdf, text 등)으로 데이터를 서비스 할 때

DispatcherServlet이 하위 컨트롤러를 호출하고 결과를 보여줄 떄 직접 컨트롤러를 호출하지 않고 중간에 여러 컴포넌트들을 끼워 넣는 이유는 무엇일까? 역시 loose coupling 하게 관계를 유지하면서 다양한 형태의 호출을 지원하기 위해서이다. 이를 통해 여러 접근 방식을 허용하는 유연성과 나중에 추가될 수 있는 접근 방식에 대한 확장성을 유지할 수 있다. 다시 한번 스프링의 큰 그림에 감탄하게 된다.

라고 적긴 했지만 이런 유연함은 일반 프로젝트를 진행하면서는 잘 느끼지 못한다. 대부분 일관된 방식으로 프로젝트를 진행하기 때문일것 같다. 억지로 느끼지 말고 열린 결말이라고 정리해두자.

위 3대장 이외에도 LocaleResolver, ThemeResolver, Interceptors 등 다양한 인프라스트럭쳐 컴포넌트들이 존재한다.

 

그래서 어떻게 동작한다고?

아래 그림은 Spring MVC의 동작 방식이다. 매우 복잡해보이고 할게 많아 보이지만 다행스럽게도 보라색으로 표시된 부분은 이미 스프링이 만들어 놓은 부분이고 개발자는 오렌지색 즉 Service(Model),  Handler(Controller), View(화면)만 만들면 된다.

Spring MVC의 동작: 복잡한건 이유가 있다.

  1. 브라우저에서 요청이 발생한다. 그 요청은 모조리 DispatcherServlet(이하 D.S)이 접수한다.
  2. D.S은 그냥 길잡이만 하니까 그 요청을 처리할 Handler가 누구인지 HandlerMapping에게 물어보면 HandlerMapping이 적절한 Handler를 알려준다. 
  3. Handler마다 동작 방식이 다르므로 D.S은 직접 Handler를 호출하지 못하고 HandlerAdapter에게 요청을 전달한다.
  4. HandlerAdapter가 드디어 Handler를 호출하면서 request 처리를 요청한다.
  5. Handler는 요청을 분석 후 필요한 서비스(모델영역)를 호출해서 비지니스 로직을 처리한다.
  6. 서비스에서 반환된 데이터는 View에서 사용되는 경우가 많은데  View에서 쉽게 접근할 수 있도록 HttpServletRequest에 저장해 두는 것이 좋다. 이를 위해 스프링에서는 Model 타입의 객체를 사용한다. 
  7. 데이터 저장까지 끝났다면 그 데이터를 확인 할 View의 이름이 HandlerAdapter를 통해 D.S에 전달한다.
  8. D.S은 다시 해당 이름의 View를 어떻게 호출할 수 있는지 ViewResplver에게 문의 후 결과를 받는다.
  9. 실제 해당 View를 호출하면 템플릿 엔진이 동작해서 HTML을 구성하기 시작한다.
  10. HTML을 구성하면서 서비스의 동작 결과를 참조하기 위해 (6)에서 만들어 놓은 Model의 데이터를 사용한다.
  11. 마지막으로 View의 내용이 클라이언트에 전달되면 처리가 종료된다.
더보기

식당에서의 역할놀이를 한번 해보자. 

먼저 DispatcherServlet이라는 메니저가 있었다. 주문을 받는 역할이다. 처음에는 한식 요리사(Contorller)들이 맛난 요리를 만들던 곳이었다. 그런데 시간이 흘러 사람들이 다양한 유리들을 주문하기 시작했다. 이런 필요에 따라 여러 요리사들이 추가되는데 이태리, 중식, 일식 요리사가 각 국에서 고용되었다. 이 요리사들이 할 수 있는 요리도 다양하고 필요한 재료들도 한식과는 매우 상이했다. 설상가상 메니저는 한국어만 할 수 있어서 효율적으로 요리사들을 관리할 수 없었다. 

먼저 손님들의 주문서를 읽을 수 없었다. 이건 어떤 메뉴를 요청하는걸까? 그래서 주문을 읽어서 처리할 수 있는 요리사를 파악하는 일이 필요했다. 이를 위해서 HandlerMapping이라는 알바생을 고용했다. 이 DispatcherServlet은 주문서를 HandlerMapping에게 넘겨주면 이 친구는 어떤 요리사가 처리하면 되는지만 알려준다. 담당 요리사가 누군지 찾아낸 D.S는 요리사랑 직접 소통하기 어렵기 때문에 또 한명의 알바생을 고용하는데 이 친구는 HandlerAdapter이다. 이 친구는 D.S의 요청을 받아서 요리사를 지원하면서 요리를 하게 한다. 요리사가 요리를 만들어(Service 호출) 특정 위치(Model)에 두면 HandlerAdapter가 요리의 위치를 알려준다. 요리별로 특별한 서빙 방법이 필요하다. 이런 다양한 방법을 모르는 D.S는 또다른 알바생 ViewResolver를 고용한다. 이 친구는 위치만 받으면 그것에 가장 적합한 서빙 방법을 알고 있다. 최종적으로 서빙 방법(View)을 이용해서 손님에게 서빙하게되면 D.S에게도 행복만이 남는다.

그럼 Spring MVC기반의 프로젝트를 만들어보면서 필요한 요소들과 그것들의 배치에 대해 알아보자.

 

프로젝트 구성

 

프로젝트 생성과 dependency 설정

간단히 웹과 관련된 내용으로만 프로젝트를 구성해보자.

프로젝트 구성

프로젝트 생성 첫 화면에서는 특별한 내용이 없다. 단 하나 집고 넘어갈 것은 Packaging 타입이 Jar라는 점이다. "웹 애플리케이션을 만들면 일반적으로 웹 서버에 배포해서 실행해게 되고 그럴 경우 War를 쓸텐데 왜 Jar일까?" 라는 정도의 궁금증을 남기고 [Next]로 넘어가자.

다음 화면에서 의존성을 선택한다. 필요한 의존성은 Lombok, Spring Boot DevTools, Spring Web, Thymeleaf이다. 웹과 관련해서 새롭게 추가된 의존성은 Spring Web과 Thymeleaf이다.

  • spring web(spring-boot-starter-web)에는 Spring MVC를 사용하기 위한 여러가지 인프라스트럭처 구성요소들이 포함되어있다. 재밋는 점은 spring-boot-starter-tomcat을 포함하고 있다는 점이다. 즉 이미 WAS를 내장하므로 애플리케이션배포를 위한 별도의 was 환경을 고민할 필요가 없다.(Spring Boot application의 배포 형태가 war가 아닌 jar임을 상기시켜보자. 이미 자체로 다 가지고 있다.) spring boot의 auto configuration의 힘은 참으로 막강하다.
    spring-boot-starter-web을 했더니 tomcat이 따라왔다!
  • Thymeleaf(spring-boot-starter-thymeleaf)는 Template Engines 즉 동적으로 사용자를 위한 View를 만들기 위한 기술 중 하나이다. 스프링 이전에 자바에서 웹을 개발해본 경험이 있다면 당연히 JSP를 찾아보겠지만 SpringBoot에서는 JSP를 권장하지 않고 따라서 기본으로 지원하지도 않는다. 대신 Thymeleaf 같은 녀석을 추천한다. 그 이유는 나중에 차차 살펴보자.

JSP 안써요?

하지만 실무에서는 여전히 JSP에 대한 수요가 있기 때문에 JSP에 대한 설정이 필요한 경우가 더 많다. Spring Boot에서 JSP를 사용하기 위한 방법은 다음에서 살펴보자: https://goodteacher.tistory.com/258

 

웹과 관련된 리소스의 배치

자동으로 WAS도 추가되었으니 간단한 HelloWorld 예를 작성해보자. 먼저 웹과 관련된 리소스들을 배치해보자.

웹과 관련된 리소스들의 배치에는 src/main/resources 아래 static과 tempaltes 경로가 사용된다.

  • src/main/resources/static
    • 동적으로 생성하지 않는 CSS, JavaScript, Image등 정적 리소스들이 위치한다. 이 요소들은 D.S가 처리할 필요가 없기 때문에 별도로 관리하는 해준다.
  • src/main/resources/templates
    • 템플릿 엔진에 의해 동적으로 변경되는 파일들이 위치한다.

웹과 관련된 리소스의 배치

 

Thymeleaf와 index.html

간단히 Thymeleaf 기반의 템플릿 페이지인 index.html를 만들어보자. Thymeleaf는 파일의 확장자로 .html을 사용한다. 파일을 작성할 위치는 당연히 src/main/resources/templates/ 이다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>

<!-- / 는 context root를 나타낸다. -->
<link rel="stylesheet" href="../static/css/common.css" th:href="@{/css/common.css}">

</head>
<body>
	<h1 th:text="${message}" id="button">메시지 표시 영역</h1>
</body>
<script src="../static/js/common.js" th:src="@{/js/common.js}" ></script>
</html>

 

  • xmlns:th="http://www.thymeleaf.org"
    • th라는 namespace를 선언한다. 이제 html 내에 th:를 사용할 수 있고 이 내용은 Thymeleaf에 의해 해석된다.
  • href vs th:href
    • 일반 href는 템플릿 엔진을 통하지 않고 파일을 브라우저로 직접 실행했을 때 동작한다.
    • th:href는 템플릿 엔진을 사용했을 때만 Thymeleaf에 의해 해석되며 일반 속성을 덮어쓰게 된다.
    • src와 th:src도 동일한 개념이다.
    • th:href의 속성값 인 @내부에서 사용된 / 는 context root를 나타낸다.
  • th:text
    • th:text 내용은 템플릿 엔진에서 해석된 후 html 태그의 inner text를 대체한다.
    • ${message} 내용은 EL과 유사하게 HttpServletRequest 영역에 message라는 이름으로 저장된 값으로 대체된다.

 

Service와 Controller

다음으로 Java 영역의 코드들을 만들어볼 차례이다.

간단한 Service를 작성해보자. 이 부분은 이제까지 만들었던 것과 다를바가 없다.

package com.doding.hellomvc.model.service;

import org.springframework.stereotype.Service;

@Service
public class HelloService {

    public String sayHello() {
        return "Hello Spring@MVC";
    }
}

 

다음은 컨트롤러 부분이다. 컨트롤러는 DispatcherServlet에서 사용자의 요청을 처리하기 위한 서브 컨트롤러라고 생각하면 되고 @Controller를 선언해서 작성한다.

package com.doding.hellomvc.controller;

import com.doding.hellomvc.model.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

// 이 클래스는 컨트롤러입니다.
@Controller
public class HelloController {
    // 모델 연동 처리
    @Autowired
    HelloService service;
    
    // RequestMappingHandlerMapping + RequestmappingHandlerAdapter
    @RequestMapping("/")                    // 사용자의 request 경로가 / 라면.
    public String welcome(Model model) {
        // model은 MVC의 M이 아니라 M에서 나온 결과를 저장할 객체(request scope에 해당)
        model.addAttribute("message", service.sayHello());
        // 결과를 보여줄 template 페이지의 이름
        return "index";
    }
}

 

처음으로 만들어본 Controller라 생소한 부분이 많다.

  • @Controller는 이 클래스가 컨트롤러로 사용되는 클래스임을 알려주고 있다. Controller 내부에는 @RequestMapping을 갖는 요청 처리 메서드들이 있는데 말 그대로 사용자의 요청(request)에 매핑되는 메서드들을 정의해준다. 위 예에서는 '/' 요청에 대해 동작하는 메서드가 작성되었다.
  • 메서드의 파라미터로 선언된 Model model은 View와의 소통에 매우 중요한 역할을 한다. model은 서비스의 동작 결과를 View에서 참조하기 위해 저장해 놓는 객체이다. model에 저장한 값은 HttpServletRequest 스코프에 저장된다. 앞서 index.html에서 ${message}로 참조하는 부분이 바로 model에 저장해 둔 값이다.
  • 마지막으로 메서드에서 리턴하는 값은 "index"라는 문자열이다. 왜 index.html이 아니라 index일까? 이것은 View 기술과 Controller를 분리시키는 부분이다. 만약 리턴 값이 index.html이었고 template engine이 Thymeleaf에서 JSP로 변경된다면 index.html 부분을 index.jsp로 변경해야만 했을 것이다. 하지만 index라고만 리턴하고 실제 View와의 연결(html인지 jsp인지)은 나타나지 않기 때문에 View 기술이 변경되어도 Controller에는 전혀 영향을 주지 않는다. 이 값은 나중에 "templates/"+index+".html" 형태로 연결된다. 

실행

이제 애플리케이션을 실행시키고 로그를 살펴보자.

. . .
14:23:01 [ INFO] Tomcat initialized with port(s): 8080 (http)
14:23:01 [ INFO] Starting service [Tomcat]
. . .
14:23:01 [ INFO] Tomcat started on port(s): 8080 (http) with context path ''

로그에서 주의깊게 살펴볼 부분은 Tomcat이 8080 포트에서 동작하고 있고 이때 context path는 ""라는 점이다.

물론 이런 값들은 필요하다면 application.properties에서 변경할 수 있다.

server.port=9090
server.servlet.context-path=/mvctest

서버가 성공적으로 싱행되었다면 브라우저를 통해 요청을 보내보자.(http://localhost:8080) 아래와 같이 화면이 출력되면 대 성공이다.!!

반가워~~ Spring @MVC!!

 

단위테스트

여기까지 어렵게 @Controller를 만들고 잘 동작하는지 브라우저로 확인해봤는데 매번 이런 식으로 확인하는 것은 쉽지 않다. 더군다나 테스트해야 할 메서드가 많아진다면 더 힘들어질 것이다. 우리에게 또다시 단위 테스트가 필요한 시점이다.

https://goodteacher.tistory.com/493

 

[spring test] 4. @Controller Test 1

이번 포스트에서는 Spring @MVC의 Controller를 테스트하는 방법에 대해서 알아보자. MockMvc 설정 MockMvc?Controller를 만들고 잘 동작하는지 확인하기 위해서 매번 스프링 애플리케이션을 실행하고 브라

goodteacher.tistory.com

위의 호출은 다음과 같은 단위테스트로 정상 동작을 확인할 수 있다.

package com.doding.hellomvc;

import com.doding.hellomvc.model.service.HelloService;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest
public class HomeControllerTest {
    @Autowired
    MockMvc mock;
    @MockBean
    HelloService service;

    @Test
    public void indexTest() throws Exception {
        // given
        String msg = "Hello Spring@MVC";
        Mockito.when(service.sayHello()).thenReturn(msg);
        String url = "/";
        // when
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get(url);
        ResultActions result = mock.perform(builder);
        // then
        result.andExpect(MockMvcResultMatchers.status().is(200))
                .andExpect(MockMvcResultMatchers.model().attribute("message", msg));
    }
}

'Spring MVC > 02.Spring @MVC' 카테고리의 다른 글

06. 파라미터의 formatting  (0) 2020.07.07
05. Handler Interceptor  (1) 2020.07.03
04. 웹과 관련된 설정  (0) 2020.07.02
03. Controller 작성 2  (2) 2020.07.01
02. Controller 작성 1  (0) 2020.06.30
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.