스프링MVC-1.동작원리

1. 스프링 MVC 동작 원리

1. 스프링 MVC 소개

  • MVC란?

    • M : Model
      • 도메인 객체 또는 DTO로 view에 전달할 혹은 view에서 전달 받은 데이터를
        가지고 있는 객체
    • V : View
      • 데이터를 보여주는 화면 역할, 다양한 형태 가능. HTML, JSON, XML 등
    • C : Controller
      • 사용자 입력을 받아 모델 객체의 데이터 변경 혹은 모델 객체를 뷰에 전달
      • 역할
        • 입력값 검증
        • 입력 받은 데이터로 모델 객체 변경
        • 변경된 모델 객체를 뷰에 전달
  • MVC 패턴의 장점

    • 동시 다발성(Simultaneous) 개발
      • 백엔드, 프론트 엔드가 독립적으로 개발 진행이 가능
    • 높은 결합도
      • 논리적으로 관련있는 기능을 하나의 컨트롤러로 묶거나,
        특정 모델과 관련있는 뷰를 그룹화 할 수 있다
    • 낮은 의존도
      • 뷰, 모델, 컨트롤러는 각각 독립적이다
    • 개발 용이성
      • 책임이 구분되어 있어 코드 수정이 간편하다
    • 한모델에 대한 여러 형태의 뷰를 가질 수 있다
  • MVC 패턴의 단점

    • 코드 네비게이션이 복잡하다
    • 코드 일관성 유지에 노력이 필요하다
    • 높은 학습 곡선
1
2
3
4
5
6
7
8
9
@Controller
public class EventController {
@RequestMapping(value = "/event",method = RequestMethod.GET)
//이것을 줄여서 다음과 같이 가능하다
// @GetMapping("/event") //스프링 4.3부터 사용 가능
public String events(Model model) {
return "events";
}
}
  • @GetMapping 정의를 살펴보면 @RequestMapping이 붙어있다
    모델
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import lombok.*;

    import java.time.LocalDateTime;
    @Getter @Setter
    @Builder @NoArgsConstructor @AllArgsConstructor

    public class Event {
    private String name;
    private int limitOfEnrollment;
    private LocalDateTime startDateTime;
    private LocalDateTime endDateTime;
    }
  • 롬복 사용
  • 컴파일 시점에 롬복 어노테이션으로 세팅한 값들이 들어간다
  • @Data를 해서 간단하게 toString, Hash까지 하게 하고 builder만 붙여도 된다
  • @Builder 애노테이션을 붙여주면 이펙티브 자바 스타일과 비슷한 빌더 패턴 코드가 빌드된다.
2개의 이벤트를 제공하는 서비스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class EventService {
public List<Event> getEvents() {
Event event1 = Event.builder()
.name("Spring Web MVC 1차")
.limitOfEnrollment(5)
.startDateTime(LocalDateTime.of(2019,1,10,10,0))
.endDateTime(LocalDateTime.of(2019,1,10,12,0))
.build();

Event event2 = Event.builder()
.name("Spring Web MVC 2차")
.limitOfEnrollment(5)
.startDateTime(LocalDateTime.of(2019,1,17,10,0))
.endDateTime(LocalDateTime.of(2019,1,17,12,0))
.build();

return List.of(event1, event2);
}
}

이제 컨트롤러에서 이 서비스를 이용해서 evnet를 얻을 수 있다

서비스를 이용하도록 고친 컨트롤러. 리턴값은 뷰의 이름
1
2
3
4
5
6
7
8
9
10
11
@Controller
public class EventController {
@Autowired
EventService eventService;

@GetMapping("/event")
public String events(Model model) {
model.addAttribute("events",eventService.getEvents());
return "events";
}
}
  • 리턴되는 문자열은 뷰의 힌트다.
  • 기본적으로 resource/templates서 찾는다
  • events.html을 만들어 본다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>테이블 목록</h1>
<table>
<tr>
<th>이름</th>
<th>참가 인원</th>
<th>시작</th>
<th>종료</th>
</tr>
<tr th:each="event: ${events}">
<td th:text="${event.name}">이벤트 이름</td>
<td th:text="${event.limitOfEnrollment}">100</td>
<td th:text="${event.startDateTime}">2019-01-01 오전 10시: 10분</td>
<td th:text="${event.endDateTime}">2019-01-01 오전 10시: 10분</td>
</tr>
</table>
</body>
</html>
  • 타임리프를 쓰려면 네임스페이스를 먼저 추가한다
  • 자동완성안되거나 하면 의존성에 타임리프 추가 했는지 살펴보자

2. legacy 정리

예전 레거시 코드를 생각해보자 서블릿..
maven을 사용했다면 maven-archetype-webapp구조를 사용해서 만들었을 것이다
기본 maven폴더구조에 main에서 webapp이라는 새로운 폴더가 있었을 것이다
안에는 샘플로 index.jsp가 들어있고 WEB-INF라는 폴더가 있는데 WEB-INF안에는
정말 중요한 web.xml이 들어 있었을 것이다
이렇게만 만들면 jsp의존성이 없으므로 pom.xml에 servlet-api 의존성을 추가해
주어야한다

pom.xml
1
2
3
4
5
6
7
8
9
10

<dependencies>
<!-- 새로 추가한 servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
  • provided는 코딩 시점에는 사용이 되지만 패키징이나 런타임에선 빠지게 된다
    보통은 서블릿 컨테이너에서 제공이 되기에 provided로 scope 지정한다
  • main에 java디렉토리를 만들고 이를 ide에서 소스 폴더로 지정한다
    인텔리J에서 소스폴더 지정한다

이제 예전 기억을 되살리면서 패키지를 만든후 서블릿을 만들어 보자

서블릿은 HttpServlet을 extend한후 필요한 메소드를 구현해서 쓴다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.servlet.http.HttpServlet;
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("servlet init"); //서블릿 초기화시 출력
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("Servlet-doGET");
PrintWriter pr = resp.getWriter();
pr.println("<html><head></head><body>");
pr.println("<h1>hello</h1>");
pr.println("</body></html>");
}

@Override
public void destroy() {
System.out.println("servlet destroy"); //서블릿 내릴떄 출력
}
}

  • HttpServlet은 Servlet 인터페이스를 어느정도 구현한 클래스로 개발자가 실제로 서블릿 사용시 상속받아 사용하는 클래스다
  • Servlet이나 GenericServlet인터페이스를 구현한다면 서블릿의 스펙의 init,service, destroy등의 모든 메소드를 전부 구현해야한다. HttpServlet을 상속받으면 필요한 메소드만 오버라이딩해서 사용하면 된다
  • 내부적으로 service(req, res)에서 http에서 get,put,post등의 프로토콜을 보고 해당하는 doGet, doPut, doPost메소드를 호출한다

이 서블릿을 독자적으로 실행하는 방법은 없다.
서블릿스펙에 맞게 구현된 서블릿을 실행하기 위해서는
서블릿 컨테이너 스펙을 구현한 서블릿 컨테이너가 필요하다

이클립스에서는 run configuration에서 세팅하던지 view-server에서 세팅하면 된다
인텔리J에서도 run configuration에서 apache tomcat을 추가해주면 된다

tomcat추가후 톰캣 위치까지 세팅했을떄 화면. 밑에 fix 누르자
fix눌러서 나오는 화면에서 war exploded를 선택한다
최종적으로 선택되는 화면

  • 톰캣 COnfigure가 되어있다면 선택하면 되고 아니면 Configure눌러서 Base Home을 지정해준다
  • 서블릿 컨테이너에 war로 배포하는 방법과 압축을 해제한 상태로 하는 방법이 있는데 exploded를 선택하면 2번쨰 방법으로 배포된다
  • 실행시 로그를 보면 war exploded 아티팩트가 서버가 올라갈떄까지 대기하다가 올라가면 배포하는 것을 확인 할 수 있다
  • 현재 실행시 보이는 localhost:8080/mavenSimpleJavaWebApp_war_exploded/
    화면은 webapp밑의 index.jsp가 보이는 화면이다

위에서 만든 서블릿은 아직 실행이 불가능하며 실행하려면 서블릿을 등록해야한다
가장 기본적인 방법은 web.xml에 등록하는 방법이다

web.xml에 서블릿을 등록한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>

<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>me.rkaehdaos.HelloServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>

</web-app>
  • 이제 실행후 localhost:8080/mavenSimpleJavaWebApp_war_exploded/hello
    라고 입력하면 서블릿 실행을 볼 수 있다
  • 로그에서 print된 문자열을 확인할 수 있다
  • 길게 느껴지면 run Configure가서 Context Root를 /로 바꿔주면 localhost:8080/hello로 서블릿을 확인할 수 있다

서블릿 2.5까지는 web.xml 파일에 서블릿으로 등록해주어야 서블릿 클래스를 사용할 수 있었는데, 서블릿 3.0 버전부터는 web.xml 파일에 따로 등록하지 않아도 @WebServlet 애노테이션을 사용하면 서블릿으로 자동으로 등록된다. 톰캣7 버전등의 서블릿 3.0을 지원하는 웹 컨테이너는 @WebServlet이 적용된 서블릿 클래스를 검색해서 사용할 서블릿으로 자동으로 등록해주는 기능을 제공하고 있다.
따라서, web.xml 파일에 따로 등록하지 않더라고 해당 클래스를 서블릿으로 사용할 수 있게 된다.

어노테이션을 이용한 서블릿 등록
1
2
@WebServlet(name = "hello", urlPatterns = "/hello" )
public class HelloServlet extends HttpServlet {
  • @WebServlet(“/hello”)처럼 urlPatterns대신 value에 값을 주어도 같은 결과
  • 다만 필터에서 필요한 서블릿 이름떄문에 위처럼 하였다

서블릿 기본 프로그래밍을 다시 리뷰하였다
공부하고있는 스프링 MVC도 결국 HttpServlet을 사용한다는 것은 변함이 없다

그럼 그 서블릿등이 스프링 컨트롤러의 어노테이션처럼 멋지게 매핑이 되었을까?
또한 스프링에서는왜 web.xml등의 설정을 하지 않는가?

서블릿 리스너라는 것이 있어서 주요 이벤트를 감지하고 필요한 경우 작업을 할 수 있다. 예를 들어 서블릿이 초기화 될때 DB 커넥션을 만들고 각 작업을 할떄 커넥션을 넘겨주며 destory될떄 커넥션을 닫을 수 있다. 서블릿에서는 서블릿 컨텍스트라는 곳에 들어 있는 addAttribute에 접근할 수 있으며 그 addAttribute에 들어 있는 DB커넥션을 사용할 수도 있다.

리스너에도 여러 종류가 있다

  • 서블릿 컨텍스트 수준

    • 컨텍스트 라이프사이클 이벤트
    • 컨텍스트 애트리뷰트 변경 이벤트
  • 세션 수준

    • 세션 라이프사이클 이벤트
    • 세션 애트리뷰트 변경 이벤트
    리스너 예제
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import javax.servlet.ServletContextEvent;
    import javax.servlet.ServletContextListener;
    public class MyListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
    System.out.println("Listener Context Initialized.");
    //서블릿콘텍스트에 attribute 추가
    // 키값과 object 형태이므로 object에 문자열이 아닌 임의의 객체도 가능
    sce.getServletContext().setAttribute("name","ahn");
    }
    }

이렇게 등록한 attribute는 어떻게 사용할 수 있을까?

HelloServlet일부. getServletContext()사용부분을 보자
1
2
3
4
5
6
7
8
9
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("Servlet-doGET");
PrintWriter pr = resp.getWriter();
pr.println("<html><head></head><body>");
pr.println("<h1>hello," + getServletContext().getAttribute("name") + "</h1>");
pr.println("</body></html>");
}

  • 서블릿의 영역(scope)은 ServletContext영역과 ServletConfig로 나눠진다

    • ServletContext(SC)
      • 모든 서블릿이 공유하는 영역
      • 하나의 웹 어플리케이션 당 하나가 만들어진다
    • ServletConfig
      • 서블릿 하나마다 할당되는 영역
  • ServletContext(SC) 얻는 방법

    • SC는 ServletConfig의 getServletContext()로 얻는다
    • 서블릿은 HttpServlet을 상속하고 HttpServlet은 ServletConfig를 구현하기에
      getServletContext()를 바로 사용할 수 있다

이렇게 만든 리스너를 등록하는 방법의 원초적인 방법은 역시 web.xml이다

web.xml에 리스너를 등록
1
2
3
4
5
<web-app>
<listener>
<listener-class>me.rkaehdaos.MyListener</listener-class>
</listener>
</web-app>

servlet과 마찬가지로 3.0부터는 어노테이션으로 가능하다

어노테이션으로 리스너 등록
1
2
3
4
import javax.servlet.annotation.WebListener;

@WebListener
public class MyListener implements ServletContextListener {

또한 서블릿 필터라는 것이 있다.
서블릿 컨테이너와 서블릿 사이에 존재하며 요청이 들어왔을떄 서블릿으로 보낼때. 그리고
서블릿이 작성한 응답을 클라이언트에 보내기전에 특별한 처리등이 가능하다. 필터를 이용해 여러 서블릿이나 특정한 url에 대한 작업이 가능하다 필터는 체인형태로 필터를 추가하면 순서대로 적용이 된다. 순서는 web.xml에 기록한 순서대로 적용된다

  • 리스너는 서블릿 컨텍스트 위쪽의 개념이다

    서블릿 필터 예제. servlet의 filter를 import하는 것이다. 햇갈리지 말자
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class MyFilter implements Filter {

    @Override public void init(FilterConfig filterConfig) throws ServletException {System.out.println("filter init"); }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
    System.out.println("filter work");
    //필터에서 그냥 메세지만 출력하면 다음 필터로 요청 응답이 전달이 안된다
    //따라서 chaining을 해주어야한다
    filterChain.doFilter(request,response);
    }

    @Override public void destroy() { System.out.println("filter destroy");}
    }

    서블릿 필터도 리스너처럼 등록이 필요하다

    web.xml에 필터 등록. 서블릿 하는 것과 과정이 비슷하다
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <web-app>

    <filter>
    <filter-name>myFilter</filter-name>
    <filter-class>me.rkaehdaos.MyFilter</filter-class>
    </filter>

    <filter-mapping>
    <filter-name>myFilter</filter-name>
    <servlet-name>hello</servlet-name>
    <!--여러 서블릿 적용하려면 servlet-name대신 <url-pattern>을 사용 -->
    </filter-mapping>

    </web-app>

    마찬가지로 어노테이션 등록이 가능하다

    어노테이션을 이용한 필터 등록
    1
    2
    @WebFilter(servletNames = "hello") //여러 서블릿: (urlPatterns = "/*")
    public class MyFilter implements Filter {
  • 실행하면 ServletContextListener를 구현한 리스너 덕분에
    Context Initialized가 가장먼저 출력된다

  • 아무것도 안해도 Filter init이 출력되는 것을 보아서 필터 초기화가 되는 것을 볼 수 있다

  • 그다음에 서블릿 초기화 떄문에 init이 출력이 되고 요청이 처리되기전에 필터를 타기 때문에 Filter work가 출력되고 필터의 chain을 타고 서블릿이 실행된다

  • 종료할 떄는 서블릿 종료되서 destroy출력되고 그다음에 필터가 종료되서 filter ddestroy가 출력되고 마지막으로 리스너가 종료되서 context destroy가 출력됨을 볼 수 있다

3. spring IOC 적용

위의 legacy 코드에는 스프링 코드가 전혀 들어있지 않다
이제 스프링을 사용해보자

서블릿 어플리케이션에 스프링을 연동하겠다는 말은 다음의 2가지 뜻을 가진다

  1. 서블릿에서 스프링 프레임워크가 제공하는 IoC 컨테이너를 사용하겠다
  2. 스프링이 제공하는 DispatcherServlet을 사용하겠다

먼저 1번을 해보자

1) 스프링 프레임워크가 제공하는 IoC 컨테이너를 사용하기

가장 먼저 필요한 것은 의존성이다.
요새 스프링 부트를 사용하면서 아예 손놓고 있지만 그래도 까먹진 말자
위의 2가지를 한꺼번에 처리하기 위해 spring-webmvc를 추가한다

spring-webmvc의존성 추가. 스프링 부트가 아니니 버전 명시
1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>

그리고 원래 등록했었던 MyListener대신에 다음 리스너를 등록한다

web.xml
1
2
3
4
5
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
ContextLoaderListener 선언 부분
1
2
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
}

자 이제 예제를 떠나서 일단 몇가지 기초를 알고 가자

  • ApplicationContext(AC)

    • 스프링 Ioc 컨테이너로 스프링이 관리하는 빈들의 컨테이너
    • 스프링 안에는 여러 구현체가 존재하는데 ApplicationContext 인터페이스 구현체임
  • 웹 어플리케이션 컨텍스트(Web Application Context = WAC)

    • ApplicationContext를 확장한 WebApplicationContext 인터페이스의 구현체
    • WAC는 AC에 getServletContext() 메서드가 추가 추가됨
    • 요약하면 스프링 어플리케이션 컨텍스트의 변종이며 서블릿 컨텍스트와 연관관계가 있으며 getServletContext() 메서드 추가됨으로 인해 서블릿 컨텍스트를 이용한 빈 라이프사이클 스코프가 추가되기도 한다
  • 스프링MVC 컨텍스트 계층 관계

    서블릿(자식) 컨텍스트가 루트(부모)컨텍스트를 잠조한다

    • 하나의 스프링 어플리케이션에는 2개의 어플리케이션 컨텍스트가 만들어진다
    • Root WebApplicationContext(Root WAC)
      • ContextLoaderListener에 의해 생성됨
      • 보통 서비스 계층과 도메인, DAO를 포함한, 웹환경에 독립적인 빈들을 관리
      • 왜? 또다른 DS들에서도 사용할 수 있도록
    • WebApplicationContext
      • DispatcherServlet(DS)에 의해 생성됨
      • DS이 직접 사용하는 컨트롤러를 포함한 웹 관련 UI성 빈 관리
    • 둘 사이는 부모-자식 관계로 맺어지며 부모쪽을 Root WAC라고 부른다
    • 자식 컨텍스트의 빈들은 부모인 Root Wac의 빈들을 주입 받을 수 있으며
      그 역은 불가능하다
    • 분리 하는 이유?
      • Web기술과 도메인 분리
      • 의존방향의 역전(도메인이 웹에 의존하는 상황)이나 상호 의존을 방지
      • AOP 적용 범위 제한
      • 그밖에 등등()
  • DS(DispatcherServlet)은 한개? 여러개?

    • DS은 일반적인 경우 하나만 쓰이지만 절대적인 것은 아니며
      필요에 따라 여러개 등록 될 수도 있다

    • 이때 각각 DS은 독자적인 WAC를 가지며 동일한 Root WAC를 공유한다

    • 스프링의 AC는 ClassLoader와 비슷하게 자신과 상위 AC에서만 빈을 찾는다
      따라서 같은 레벨의 형제 AC의 빈에는 접근하지 못하고 독립적이다

      각 DispatcherServlet은 별도의 WAC를 생성하며 서로의 빈을 사용할 수 없다
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      <servlet>
      <servlet-name>a-Controller</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/a-servlet.xml</param-value>
      </init-param>
      <load-on-startup>1</load-on-startup>
      </servlet>

      <servlet>
      <servlet-name>b-Controller</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/b-servlet.xml</param-value>
      </init-param>
      <load-on-startup>1</load-on-startup>
      </servlet>
    • 한개만 사용할 경우?

      • Root WAC는 강제되지 않으므로 여러 DS을 사용할 것이 아니면
        만들지 않고 서블릿레벨의 WAC에 전부 때려박으면 된다
      • ContextLoaderListener가 필요없으며 , 결국 하나의 WAC만 만들어지고
        생성된 서블릿 레벨의 WAC가 Root역할을 하게 된다
      • WebApplicationContextUtils.getWebApplicationContext() 그냥 사용불가
        • 스프링이 아닌 Struts같은 곳에서 스프링빈 접근이 힘들어진다
        • 이건 고려대상하지말자 ㅋㅋ
  • Root WebApplicationContext(Root WAC)

    web.xml에서ContextLoaderListener 선언
    1
    2
    3
    <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    • MyListener처럼 ServletContextListener를 구현
      • 따라서, SC 인스턴스 생성 시(톰켓으로 어플리케이션이 로드된 때)에 호출된다
      • 따라서 DS class 로드보다 먼저 동작한다
      • Root WAC 를 시작하고 종료하기 위한 부트스트랩 리스너
    • delegator 기능
      • ContextLoaderListener는 ContextLoader에 모든 기능을 위임하고 있다
      • ContextLoader는 Root WAC의 초기화 작업을 실제로 수행한다
contextConfigLocation 파라미터가 context-param Element를 통해 선언
1
2
3
4
5
6
7
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/conf/root-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
  • contextConfigLocation
    • 로딩할 스프링 파일 정의하는 컨텍스트 파라미터
    • 위처럼 컨텍스트 파라미터를 지정하지 않았다면 기본값으로
      XmlWebApplicationContext에 정의된 “/WEB-INF/applicationContext.xml”를 찾는다
  • contextClass
    • 생성할 AC의 타입을 지정할 수 있는 파라미터
    • 지정하지 않으면 기본값으로 XmlWebApplicationContext
  • context-param Element를 통해 선언 - 전역 사용
  • xml의 빈 정의에 기반해서 WAC객체를 생성하고 웹어플리케이션의 SC에 저장한다
  • 직접 접근하려면?
    Root WAC에 직접 접근하는 방법. 스프링만 사용하면 이 방법을 쓸일이 거의 없다
    1
    WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
  • Servlet WebApplicationContext

    • 사실 그냥 WAC지만 Root WAC와 구분짓기 위해 Servlet WAC 라고 하겠다
      contextConfigLocation 파라미터가 servlet Element를 통해 선언
      1
      2
      3
      4
      5
      6
      7
      8
      9
      <servlet>
      <servlet-name>dispatcherServlet</servlet-name>
      <servler-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring-servlet.xml</param-value>
      </init-param>
      <load-on-startup>1</load-on-startup>
      </servlet>
    • Servlet Element를 통해 선언 -> 해당 서블릿에서만 사용가능한 WAC
  • 계층 관계 컨텍스트

    • 부모 자식 모두 param-name이 contextConfigLocation이다
    • 전역변수와 지역변수처럼 덮어 써지는 것이 아니라 클래스 상속같은 계층 관계이다
    • 따라서 자식 컨텍스트의 빈들은 부모 컨텍스트의 빈들을 주입받을 수 있다
  • 백기선님 설명

    • ContextLoaderListener는 스프링 Ioc 컨테이너, 즉 ApplicationContext를 이 서블릿 어플리케이션 생명주기에 맞춰서 바인딩해준다
    • 따라서 웹 어플리케이션에 등록된 서블릿들이 사용할 수 있도록 AC를 만들어서 그 AC를 서블릿 컨텍스트에 등록해준다
    • 그리고 서블릿 이 종료될 시점에 어플리케이션 컨텍스트를 제거해준다
    • 이게 정확히 서블릿 리스너가 할 수 있는 일
    • 서블릿 컨텍스트의 라이프 사이클에 맞춰서 스프링이 제공해주는 AC를 연동해주는 가장 핵심적인 리스너
    • 이 컨텍스트 리스너는 AC를 만들어야 하기에 스프링 설정파일을 필요로 한다
    • 이 리스너의 소스를 열어서 파라미터들을 보면
      컨텍스트설정파일 위치라던지 생성할 ApplicationContext의 타입등을 지정할 수 있다
    • 기본은 xml이었으나 요새는 자바 설정파일을 하는 추세

다시 예제로 돌아간다

ContextLoaderListener에 contextClass,contextConfigLocation 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 기본 XmlWebApplicationContext 대신
AnnotationConfigWebApplicationContext를
사용하는 ContextLoaderListener를 설정한다 -->
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<!-- 설정 위치는 반드시 콤마나 공백을 구분자로 사용하는
하나 이상의 정규화된 @Configuration 클래스들로 구성되어야 한다.
정규화된 팩키지는 컴포넌트 스캔으로 지정될 수도 있다. -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>me.rkaehdaos.AppConfig</param-value>
</context-param>
  • AC 인스턴스 방법

    • 스프링의 @Configuration 클래스 지원은 스프링 XML 100% 대체하지 못한다
    • 스프링 XML namespace같은 기능등은 여전히 컨테이너를 설정하는 이상적인 방법
    • ClassPathXmlApplicationContext를 이용한 XML중심적인 방법
    • AnnotationConfigApplicationContext를 이용한 자바 어노테이션 중심적인 방법
  • AnnotationConfigApplicationContext

    • 스프링 3.0때부터 추가
    • 인풋으로 @Configuration 클래스뿐만 아니라 평범한 @Component 클래스와 JSR-330 메타데이터로 어노테이션이 붙은 클래스들도 받아들일 수 있다
    • @Configuration클래스를 받았을 때 @Configuration 클래스 자체가 빈 정의로 등록되고 해당 클래스내의 선언된 모든 @Bean 메서드들도 빈 정의로 등록된다.
    • Component와 JSR-330 클래스들이 제공되었을 때 이 클래스들은 빈 정의로 등록되고 해당 클래스내에서 필요한 곳에 @Autowired나 @Inject 같은 DI 메타데이터가 사용되었다고 가정한다
  • AnnotationConfigWebApplicationContext

    • AnnotationConfigApplicationContext의 WebApplicationContext 변형
    • ContextLoaderListener, MVC DispatcherServlet 등 설정할 때 사용할 수 있다
  • 위 예제 작성 부분

    • contextClass 미설정시 디폴트 값은 XmlWebApplicationContext
    • contextClass 파라미터에 AnnotationConfigWebApplicationContext를 설정하였다
    • 이제 ContextLoaderListener는 AnnotationConfigWebApplicationContext를 사용
    • 이제 설정의 위치는 자바 설정파일 클래스로 지정.
    • 설정 위치는 반드시 콤마나 공백을 구분자로 사용하는 하나 이상의 정규화된
      @Configuration 클래스들로 구성되어야 한다
    • 정규화된 패키지는 컴포넌트 스캔으로 지정될 수도 있다
  • 요약

    • 이제 위에서 ContextLoaderListener가 contextConfigLocation로 지정된 me.rkaehdaos.AppConfig 자바 설정 클래스를 참고하여 contextClass로 지정된 AnnotationConfigWebApplicationContext를 만들고 ServletContext에 등록한다
      이과정에서 AppConfig, HelloService가 빈으로 등록이 된다
    • ContextLoaderListener의 초기화 메서드인 contextInitialized()는 contextLoader클래스의 initWebApplicationContext(ServletContext)에 위임하고 있으며 여기를 보면 다음과 같은 이름으로 서블릿 클래스에 AC를 저장하고 있음을 알 수 있다
      contextLoader클래스의 initWebApplicationContext메서드의 일부분
      1
      2
      3
      4
      5
      6
      7
      8
      servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
      ```

      ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE의 정의는 다음에서 찾아볼 수 있다
      ```java ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
      public interface WebApplicationContext extends ApplicationContext {
      String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
      }

이제 설정파일 클래스와 빈을 만들어보자

AppConfig.java
1
2
3
4
5
6
7
8
package me.rkaehdaos;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class AppConfig {}
HelloService.java
1
2
3
4
5
6
7
8
package me.rkaehdaos;

import org.springframework.stereotype.Service;

@Service
public class HelloService {
public String getName(){ return "GeunChang"; }
}
  • 설명
    • AppConfig에 @Configuration을 붙여서 스프링 자바 설정 클래스로 만들었다
    • 필요시엔 @ComponentScan에 basePackage로 스캔을 시작할 부분을 지정할 수 있다
    • 위처럼 하면 HelloService가 빈으로 등록된다

IDE에서 보면 web.xml의 에서 빨간색으로 나온다. web.xml의 설정은 위치가 중요하다. context-param은 필터보다 먼저 등록이 되어야한다 위치를 바꾸면 이제 빨간색이 사라진다

HelloServlet에서 이제 스프링 Ioc를 이용 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import static org.springframework.web.context.WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;


public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException { System.out.println("servlet init"); }

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("Servlet_doGET");

//서블릿 컨텍스트에서 AC를 꺼내온다
ApplicationContext ctx = (ApplicationContext)getServletContext().getAttribute(ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
HelloService helloService = ctx.getBean(HelloService.class);


PrintWriter pr = resp.getWriter();
pr.println("<html><head></head><body>");
pr.println("<h1>hello," + helloService.getName() + "</h1>");
pr.println("</body></html>");
}

@Override
public void destroy() {
System.out.println("servlet destroy");
}
}
  • 설명
    • 무식한 예지만 이제 저장된 서블릿컨텍스트에서 AC를 꺼내온다
    • AC에서 빈을 꺼내서 쓴다 (new를 쓰지 않았다)

여기까지가 legacy 자바 웹 어플리케이션에서 Spring Ioc컨테이너 사용하기..
잘 작동하지만 불편한 점이 있을 것이다

요청할 일이 생길때마다 서블릿을 생성해야하고 URL추가 할때마다 web.xml에 을 해주어야 한다

만약 이렇게 서블릿을 늘리는 중에 중복된 부분이 생긴다던지
아니면 공통적으로 처리하고 싶은 작업이 생긴다면 어떻게 해야할까?
스프링 시큐리티등을 적용할때 많은 서블릿에 일일히 어떻게 적용할 것인가?

이를 효과적으로 처리하기 위해 나온것이
(물론 굳이 필터로 처리할 수도 있겠지만) 프론트컨트롤러 디자인패턴을 적용한 DispatcherServlet(DS) 사용이다

프론트 콘트롤러 패턴은 하나의 대표 컨트롤러(프론트 컨트롤러)를 두고 그곳에서
모든 request를 담당하고 처리하도록 한다. 프론트 컨트롤러는 들어오는 요청을 파악하여 그 요청에 해당하는 다른 컨트롤러에게 위임하게 된다

  • DS 사용시 장점및 특징(프론트 컨트롤러의 장점도 포함된다)

    • 모든 컨트롤러는 프론트 컨트롤러 일부분을 구현하는 형태 -> 규격화된 코드
    • 공통 부분을 제외한 로직을 컨트롤러가 갖게 되므로 코드량이 줄고 목적이 명확
    • DS가 요청을 핸들링하므로 web.xml에 다른 컨트롤러를 등록할 필요가 없어 편하다
    • 제일 처음에 만들었던 스프링 컨트롤러처럼 명확한 컨트롤러 작성 가능
    • 컨트롤러에서 return String만 해도 알아서 응답을 Http Response로 만들어 준다

    ContextLoaderListener가 Root WAC를 만들었다면 DS는 Root WAC를 상속받는 또하나의

WAC를 만들게 된다 (기초에서 보았듯이 Root WAC가 없다면 그냥 WAC가 만들어진다 )

2) 스프링이 제공하는 DispatcherServlet을 사용

지금까지 스프링 Ioc 컨테이너를 사용하게 되었지만 아직 스프링 MVC를 사용하게 된 것은
아니다. HTTPServlet을 상속받아 일일히 서블릿을 구현했었던 레거시에 Ioc 컨테이너만
사용할 수있게 적용했던 것 뿐이다.
이제 스프링 MVC를 적용하기 위해 스프링이 제공하는 서블릿 구현체인 DS를 사용해본다

  • 목표
  • 기존 web.xml에서 MyFilter, HelloServlet 부분 삭제 -> 안쓸꺼니까
  • HeeloServlet을 대체하는 DS에서 위임받을 Controller를 작성한다
  • DS를 서블릿으로 등록한다
  • 서비스는 Root WAC에, 컨트롤러는 DS에서 만드는 WAC에 등록 되게 만들 것이다

web.xml에서 필터랑 서블릿 부분을 삭제하였으면 컨트롤러를 작성한다

기본 HelloServlet 역할을 하는 Controller 작성
1
2
3
4
5
6
7
8
9
10
11
@RestController
public class HelloController {

@Autowired
HelloService helloService;

@GetMapping("/hello")
public String hello() {
return "hello, "+ helloService.getName();
}
}
  • Controller와 RestController의 차이
    • HTTP Response Body가 생성되는 방식의 차이
    • 기존 MVC의 Controller는 View 기술을 사용
    • RestController는 객체 반환시 객체 데이터를 JSON/XML타입의 응답으로 직접 리턴
    • @Controller의 메서드에 @ResponseBody를 선언해서도 객체 리턴이 가능하다
    • 실제 RestController 소스를 보면 위의 방법이 쓰인다

Root WAC에 컨트롤러가 등록이 안되도록 해보자

이전에 작성한 AppConfig에서 필터로 Controller만 걸러낸다
1
2
3
4
5
6
7
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;

@Configuration
@ComponentScan(excludeFilters = @ComponentScan.Filter(Controller.class))
public class AppConfig {}

이렇게 하면 이제 Root WAC에 Controller를 제외한 빈들이 등록된다
이제 하나의 Config를 더 만들어보자

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;

@Configuration
@ComponentScan(useDefaultFilters = false, includeFilters = @ComponentScan.Filter(Controller.class))
public class WebConfig {

//핸들러매핑이나 뷰리졸버 수준에서 DS가 자동 등록을 못하는 빈들은
//여기에서 @Bean으로 등록하는 것도 방법이다
}
  • 설명
    • AppConfig와 같은 자바 설정 파일이다
    • ComponentScan에서 기본 필터를 끈 후 Controller만 등록하도록 하였다

이제 이 설정파일을 web.xml에 등록하면 끝난다

아래는 완성된 web.xml이다

완성된 web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>

<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>me.rkaehdaos.AppConfig</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- 새로 추가된 부분-->
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>me.rkaehdaos.WebConfig</param-value>
</init-param>
</servlet>

<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>

</web-app>
  • 설명
    • DispatcherServlet은 서블릿이므로 을 사용해서 등록한다
    • 파라미터를 보면 ContextLoaderListener와 거의 흡사함을 알 수 있다
    • 새로 지정한 WebConfig를 참조하여 WAC를 만들게 된다
    • 서블릿 매핑을 /app/*하여서 /app/*으로 들어오는 모든 요청은 DS가 처리한다
    • 실행후 localhost:8080/app/hello 해보면 정상 작동하는 것을 볼 수 있다

계층구조로 이루어진 어플리케이션을 작성해 보았다. 사실 꼭 이런 구현이 필요한 것은 아니다. 기초에서 보았던 내용 상기하자. DS를 여럿 쓰지 않을거면 그냥 DS에서 만든 WAC에 모든 빈을 등록해도 된다

  • DS만 쓰는 웹 어플리케이션 만드는 순서
    • web.xml에서 ContextLoaderListener와 context-param 부분을 삭제한다
    • AppConfig 삭제
    • WebConfig에서 콤포넌트 스캔의 ()부분 전부 삭제 -> 전체 빈 등록

이게 사실 가장 흔한 형태의 자바 웹 어플리케이션이다
반대로 Root WAC만 만들고 DS의 WAC를 비울 수도 있지만 좋은 구조는 아니다

다른 legacy 서블릿이 있고 그 서블릿에서 HelloServlet처럼 AC의 빈을 사용해야할
경우라면 상속구조로 하는게 맞지만 최근에는 legacy 서블릿이 없기에 DS의 WAC에 다 등록하는 식으로 많이 쓴다

스프링 부트와 많이 틀린점도 상기한다
현재 작성한 부분은 서블릿 컨테이너가 먼저 올라가고 그 컨테이너에 올라가는 서블릿 웹 어플리케이션에 스프링 연동하는 방법이다.
반대로 스프링 부트는 스프링 부트 어플리케이션인 자바 어플리케이션이 먼저 뜨 그안에서
내장 서버로 뜨게 된 임베디드 톰캣에 DS을 코드로 등록한다
구글 검색하면 그냥 스프링에서도 DS를 코드로 등록하는 방법이 보이긴 했지만 개념 자체가 다르다는 것을 생각하자

4. DispatcherServlet(DS) 동작 원리

디버거 탐험

  • DS의 doService()에 디버거를 걸고 디버그 모드로 실행후 브라우저에서 리퀘스트

  • doService안에서는 req에 여러 Attribute를 set한후 doDisPatch()를 실행한다

  • doDispatch를 보 요청을 분석해서 멀티파트 요청인지 로케일은 뭐고 테마는 어떤 테마인지 분석한다 (지금 요청은 멀티파트 아니니 그냥 넘어간다)

  • doDisPatch는 getHandler() 메서드를 사용한다

  • getHandler()에서 여러 핸들러 매핑
    java DS의 getHandler부분. size=2, 디버거에서 핸들러2개가 보인다

  • 아무런 설정없어도 DS가 자동으로 2개의 핸들러 구현체를 기본으로 등록하였다

    • BeanNameUrlHandlerMapping
      • 요청 URI와 동일한 이름을 갖는 Controller빈을 매핑한다
      • 빈 설정에서 name에 URI와 동일하게 주면 된다
    • SimpleUrlHandlerMapping
      • Ant 스타일의 경로 매핑 방식을 사용
      • mappings 프로퍼티를 통해 매핑 설정 목록을 입력받는다
    • RequestMappingHandlerMapping
      • @MVC 개발 필수조건
      • 기존 DefaultAnnotationHandlerMapping이 deprecated되고 대체됨
      • 모든 @Controller빈의 @RequestMapping 어노테이션을 검색
  • 위의 경우 반복문을 돌면서 처음 BeanNameUrlHandlerMapping에서 null이 나온다

  • 다시 반복문을 돌면서 RequestMappingHandlerMapping에서 핸들러를 찾는다

  • 핸들러를 찾았으니 반복문을 벗어나서 이제 HandlerAdapter를 찾게 된다
    java DS의 getHandlerAdapter부분. size=3, 디버거에서 핸들러아답터 3개가 보인다

  • 아무런 설정없이도 DS가 자동으로 3개의 핸들러어댑터 구현체를 등록하였다

  • 아래의 3개의 핸들러아답터중 현재 핸들러를 처리할 수 있는 아답터를 찾는다

    • HttpRequestHandlerAdapter
    • SimpleControllerHandlerAdapter
      • 어노테이션 기반이 아닌 Controller 인터페이스 구현으로 만든
        클래식 컨트롤러를 처리 가능한 핸들러어댑터
    • RequestMappingHandlerAdapter
      • RequestMappingHandlerMapping 에 대응되는 아답터 - 어노테이션 기반
      • 컨트롤러 타입을 결정해야하므로 파라미터를 알아야한다
      • 따라서 HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler
        인터페이스를 가지고 있다
  • 핸들러 아답터까지 받아오게 되면 아답터의.handle()을 이용해 invoke한다

  • adapter인터페이스의 handle()은 handleInternal()를 호출하고
    handleInternal()안에서 invokeHandlerMethod()로 invoke를 하게 된다

  • 핸들러에 메소드까지 들어있으므로 자바 리플렉션을 이용해서 해당 메소드를 실행한다
    즉 컨트롤러의 hello()가 실행되서 문자열이 반환된다

  • 위에서 말했듯이 @RestController는 @Controller에 메소드(여기선 hello())에 @ResponseBody를 붙인것과 동일한 결과이다.

  • .이 리턴값은 문자열인지 model일지 responeEntity일지 View이름인지 모르므로 이를 처리하기위한 Valuehandler가 존재한다
    15개의 ValueHandler.. 여기선 11번째가 선택된다

  • returnValueHandler는 Converter를 사용해서 리턴값을 Http 본문에 넣어주는 처리를 한다

  • @ResponseBody를 사용한경우 mv(ModelAndView)는 null이다

  • 따라서 view에 렌더링 하는 과정없이 응답을 바로 보여주게 된다

4-1. DispatcherServlet 더 살펴보기

이젠 뷰가 있는 경우를 살펴보자.
뷰를 처리하기 위해선 RestController를 일반 Controller로 바꿔줘야한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
public class HelloController {

@Autowired
HelloService helloService;

@GetMapping("/hello")
public @ResponseBody String hello() {
return "hello, "+ helloService.getName();
}

@GetMapping("/sample")
public String sample() {
return "WEB-INF/sample.jsp";
}
}
  • 설명
    • @RestController를 @Controller로 바꿈
    • 이전에 사용했던 부분은 그대로 동작하도록 하기 위해 @ResponseBody를 메소드에 붙임
      • 리턴타입에 주는 어노테이션은 존재하지 않는다
      • 리턴타입과 관련이 있다고 해서 접근지시자 뒤에 어노테이션을 붙이는 것인데
        이 경우 마치 리턴 타입에 어노테이션을 붙이는 모양이지만 이 경우에도
        메소드에 적용이 되는 것이지 리턴타입에 적용되지 않음을 꼭 알아두자
    • 새로운 매핑을 가지는 sample()을 만든다. 반환되는 리턴값은 역시 문자열이다
    • 이 두 메소드의 리턴값이 가지는 의미가 다르다
      • hello() : @ResponseBody를 가지므로 문자열 자체가 내용으로 반환된다
        위에서 디버거로 잘 살펴보았던 부분을 상기하자
      • sample() :
        • hello의 경우와 처음부터 리턴값에 대한 returnValueHandler 검색까지는 동일
        • sample의 경우 12번째인 ViewNameMethodReturnValueHandler가 선택된다
          (즉 View 이름으로 인식을 한 것)
        • invokeHandlerMethod를 거친후 @ResponseBody가 아니기때문에 mv값이 null이
          아니며 객체의 view값에는 sample의 리턴된 문자열이 들어가 있다
        • 만약 model객체가 있다면 mv객체의 모델에 바인딩하고 뷰를 렌더링한다
        • 뷰 렌더링 -> 문자열에 해당한 jsp를 response응답에 추가

DispatcherServlet 정리

  • DispatcherServlet 초기화
    • 다음의 특별한 타입의 빈들을 찾거나, 기본 전력에 해당하는 빈들을 등록
      • (대부분 Strategy 패턴 적용)
      • HandlerMapping : 핸들러를 찾아주는 인터페이스
      • HandlerAdapter : 핸들러를 실행하는 인터페이스
      • HandlerExeceptionResolver
      • ViewResolver
  • DispatcherServlet 동작 순서
    1. 요청 분석 - Locale, Theme, Multipart 등
    2. (HandlerMapping에 위임하여)요청을 처리할 핸들러를 찾는다
    3. (등록된 HandlerAdapter중에) 해단 핸들러를 실행할 수 있는 핸들러 아답터를 찾는다
    4. 찾아낸 핸들러 아답터를 사용해 핸들러의 응답을 처리한다
    5. (부가적으로)예외 발생시, 예외처리 핸들러에 요청처리를 위임한다
    6. 핸들러의 리턴값을 보고 어떻게 처리할지 확인한다
    • 뷰이름에 해당하는 뷰를 찾아 모델 데이터를 렌더링한다
    • @ResponseEntity가 있다면 Converter를 사용해서 응답 본문을 만든다
    1. response를 보낸다

스프링 Ioc와 DS의 연결 및 커스텀 뷰 리졸버

DispatcherServlet의 initStrategies(). 여러 전략이 init되고 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
  • 동작하는 방식은 다 비슷해서 하나만 따라가봐도 이해가 된다
initViewResolvers부분
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void initViewResolvers(ApplicationContext context) {
this.viewResolvers = null;

if (this.detectAllViewResolvers) {
//부모 컨텍스트를 포함한 AC에서 모든 ViewResolver를 찾아 matchingBeans에 보관
Map<String, ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.viewResolvers = new ArrayList<>(matchingBeans.values());
// 뷰 리졸버들의 정렬을 유지한다
AnnotationAwareOrderComparator.sort(this.viewResolvers);
}
}
else {
try {
ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
this.viewResolvers = Collections.singletonList(vr);
}
catch (NoSuchBeanDefinitionException ex) {
// 디폴트 리졸버 투가 할것이기에 여기서 특별히 할건 없다
}
}

// 적어도 한개 이상의 ViewResolver가 있어야 한다
// 다른 검색된 리졸버가 없다면 default ViewResolver가 등록된다
if (this.viewResolvers == null) {
this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No ViewResolvers declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}

  • 다른 뷰 리졸버 초기화도 비슷하다
  • 예를 들어 initHandlerMappings의 경우에는 detectAllHandlerMappings라는 플래그에
    따라서 true면 모든 핸들러를 매핑하고 그렇지 않으면 한개의 핸들러만 매핑하며 따로 선언하지 않았다면 기본 핸들러매핑을 등록한다
    (기본값은 true이며 어지간하면 유지. 높은 최적화가 필요할떄는 false로 한개의
    핸들러매핑만 등록해서 루프를 덜타게 하는 것도 방법이다)
  • 기본전략은 하나씩 추적해가면 결국 spring-webmvc-5.1.4.RELEASE.jar쪽에 있는
    dispatcherServlet.properties에 기본 전략이 정의되어있다
dispatcherServlet.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
```

아무것도 등록하지 않아도 위의 전략에따라 자동으로 viewResolver 빈들이 등록된다
예제는 아무것도 등록하지 않았으므로 핸들러매핑에 BeanNameUrlHandlerMapping, RequestMappingHandlerMapping을 등록했고 뷰리졸버에도 기본으로 InternalResourceViewResolver라는 기본 뷰 리졸버가 등록되어 사용되었다
이제 커스텀한 뷰 리졸버를 등록해본다

```java 기존 자바 설정파일에 커스텀한 뷰리졸버를 추가한다
@Configuration
@ComponentScan
public class WebConfig {

@Bean
public ViewResolver viewResolver() {
//기본 뷰 리졸버에
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
//prefix, suffix를 set하고
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
//그 뷰 리졸버를 등록
return viewResolver;
}
}
  • 이제 기존 컨트롤러의 sample()에서 리턴을 다음처럼 view 이름만 해도 된다
    HelloController의 sample()
    1
    2
    3
    4
    5
    @GetMapping("/sample")
    public String sample() {
    //return "/WEB-INF/sample.jsp";
    return "sample";
    }

이제 initViewResolvers부분에서 기본 리졸버가 등록되지 않는다
자동으로 prefix와 suffix를 합해서 뷰 이름이 생성이 된다

spring mvc 구성 요소

위에서 보았듯이 DS의 기본 전략은 dispatcherServlet.properties에 정의되어 있고
DS의 initStrategies()에서 여러 전략 인터페이스를 초기화 하는 부분을 보았다

위에 있지만 다시보는 initStrategies
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}

이제 DS이 사용하는 여러가지 인터페이스에 대해 살펴본다
* 이 붙은 인터페이스는 여러개의 빈들을 순회한다

  • MultipartResolver

    • 파일 업로드 요청 처리에 필요한 인터페이스
    • http의 contents의 multipart에 바이너리 데이터를 부분부분 쪼개서 보내는데
      이를 처리할 수 있는 로직이 필요하다
    • HttpServletRequest를 MultipartHttpServletRequest로 변환해주어서
      요청이 담고 있는 File을 꺼낼 수 있는 API를 제공한다
    • 이 인터페이스 구현체가 bean으로 등록이 되어 있어야 DS가 사용해서 파일 업로드를
      처리할 수 있다
    • 구현체는 2가지가 존재한다
      • CommonsMultiPartResolver
        • 먼저 만들어짐
        • Apache Commons를 사용
      • StandardServletMultipartResolver
        • 나중에 만들어짐 (스프링 MVC가 나온 다음)
        • Servlet 3.0 기반의 구현체
    • 스프링 기본 DS는 이 구현체들을 등록하지 않는다
    • 스프링 부트를 사용하는 경우에는 기본적으로 StandardServletMultipartResolver를
      등록 시키기 때문에 아무런 bean 설정 없이 파일 업로드 처리하는 핸들러를 손쉽게
      만들 수 있다
  • LocaleResolver

    • 클라이언트의 위치(Locale)정보를 파악하는 인터페이스
    • 요청이 DS에 들어왔을때 분석하는 단계에서 사용된다
    • DS만 있고 다른 빈이 없는 경우 기본 Strategy가 등록된다. 여러 구현체들 중에서
      기본 Strategy로 쓰이는 구현체는 AcceptHeaderLocaleResolver다
    • 기본 Strategy는 request의 accept-language 헤더를 보고 판단한다
  • ThemeResolver

    • 어플리케이션에 설정된 테마를 파악하고 변경할 수 있는 인터페이스
    • 기본 Strategy는 FixedThemeResolver로 사실상 theme변경을 사용하지 않는 것이다
  • HandlerMapping

    • 앞에서 많이 봤던 그것!
    • 요청이 들어왔을 때 그 요청을 처리하는 핸들러를 찾아주는 인터페이스
    • 앞에서 봤듯이 2개가 기본값으로 등록
  • HandlerAdapter

    • HandlerMapping이 찾아낸 “핸들러”를 처리하는 인터페이스
    • 스프링 MVC 확장력 의 핵심
    • 앞에서 보았듯이 HttpRequestHandlerAdapter, SimpleControllerHandlerAdapter,
      RequestMappingHandlerAdapter가 기본으로 등록이 된다

핸들러를 Functional Style?
핸들러 매핑으로 그런 핸들러를 찾을 수 있도록 커스텀 핸들러 매핑을 만들고
또 그런 핸들러를 실행 가능한 핸들러 아답터를 만들어 등록하면 가능하다

  • HanlderExceptionResolvers

    • 예외가 발생하면 해당 ExceptionResolver가 예외를 처리
    • 이것도 기본값으로 3개가 등록 되어 있다
      • ExceptionHandlerExceptionResolver
        • 개발자가 주로 사용하게 될 빈
        • @ExceptionHandler 어노테이션으로 정의
      • ResponseStatusExceptionResolver
      • DefaultHandlerExceptionResolver
  • RequestToViewNameTranslator

    • 핸들러에서 뷰 이름을 명시적으로 리턴하지 않은 경우,
      요청을 기반으로 뷰 이름을 판단하는 인터페이스
    • 구현체는 DefaultRequestToViewNameTranslator 하나이며 기본으로 등록된다
      반환하던 뷰이름을 void로 반환하지 않더라도 정상작동한다
      1
      2
      3
      @GetMapping("/sample")
      // public String sample() { return "sample"; }
      public void sample() { }
    • 위의 경우에도 알아서 요청의 sample로 뷰를 찾아낸다
  • ViewResolver

    • 뷰 이름(String)에 해당하는 뷰를 찾아내는 인터페이스
    • 여러 구현체 중 InternalResourceViewResolver 하나만 기본으로 등록 됨
      (스프링 부트때는 더 많은 구현체 등록)
    • 이 기본 구현체 InternalResourceViewResolver가 jsp를 지원해서 위의 예제에서
      jsp 뷰가 처리될 수 있었다
  • FlashMapManager

    • 3.1부터 존재
    • FlashMap 인스턴스를 가져오고 저장하는 인터페이스
    • FlashMap은 주로 리다이렉션을 사용할 때 요청 매개변수를 사용하지 않고
      데이터를 전달하고 정리할 때 사용한다
    • 리다이렉트를 하는 이유?
      • refresh경우 데이터가 또 넘어온다 (form의 submit의 재발생)
      • 이를 막기 위한 일종의 패턴 - 중복 폼 데이터 전송 방지
      • post요청을 받은다음에는 GET으로 리다이렉션해서 view만 보여준다
      • 만약 GET에서 전달할 데이터가 있을 경우 URL에 다 적어주어야 한다
        (ex redirect:/event?id=2019)
      • 이경우 URLpath 나 URL 파라미터 로 주지 않아도 데이터 전송가능하게 하는 것이
        이 FlashMap이다
    • 구현체는 SessionFlashMapManager가 Default로 쓰인다

더 깊게 공부하려면 앞에서 디버깅할 때 살펴보았던 returnValueHandler나 파라미터에 대한 리졸버들을 보면 된다

코드로 서블릿 등록

지금까지 원래 사용하던 레거시 서블릿 컨테이너에 DS를 등록해서 적용하였다. 이 등록방법은 앞에서 보았던 web.xml에 등록하는 방법이 있지만 코드로도 가능하다
현재 web.xml은 다음과 같이 되어있다

web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Web Application</display-name>

<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>me.rkaehdaos.WebConfig</param-value>
</init-param>
</servlet>

<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>

</web-app>

이 web.xml을 지우고 코드로 고치면 다음과 같다

WebApplication클래스 내용은 위 web.xml내용과 같은 의미이다
1
2
3
4
5
6
7
8
9
10
11
12
13
public class WebApplication implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {

AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(WebConfig.class);
context.refresh();

DispatcherServlet dispatcherServlet = new DispatcherServlet(context);
ServletRegistration.Dynamic app = servletContext.addServlet("app", dispatcherServlet);
app.addMapping("/app/*");
}
}
  • web.xml과 같은 의미이다

  • DS를 생성해야하는데 web.xml에서 init-param으로 주었던 초기화를 한 context를 넘겨준다

  • DS를 서블릿으로 등록하고 매핑을 하였다

  • 이기능은 스프링 3.1 이상, 서블릿 3.0 이상에서 지원하는 기능이다

  • 깊게 내려가면 자바가 제공하는 Service Locator까지 가게 된다
    (나중에 한번 보자)

  • 이처럼 스프링 부트가 없이 사용하는 스프링 MVC의 경우

    • Bean에 무엇이 등록되었는지가 중요하다. 등록이 안되었을 경우 기본 빈들이 등록된다
    • 서블릿 컨테이너에 DS를 등록하는 형태이다
  • 스프링 부트로 사용하는 스프링 MVC의 경우

    • 자바 어플리케이션 안에서 임베디드 톰캣 (혹은 다른 임베디드 서버)가 실핻되는 형태
    • 내장 톰캣을 만들고 그 안에 DS를 등록한다
    • 스프링부트의 AutoConfigure에 의해 자동으로 설정된다

Related POST

공유하기