1. 스프링 MVC 동작 원리
1. 스프링 MVC 소개
MVC란?
- M : Model
- 도메인 객체 또는 DTO로 view에 전달할 혹은 view에서 전달 받은 데이터를
가지고 있는 객체
- 도메인 객체 또는 DTO로 view에 전달할 혹은 view에서 전달 받은 데이터를
- V : View
- 데이터를 보여주는 화면 역할, 다양한 형태 가능. HTML, JSON, XML 등
- C : Controller
- 사용자 입력을 받아 모델 객체의 데이터 변경 혹은 모델 객체를 뷰에 전달
- 역할
- 입력값 검증
- 입력 받은 데이터로 모델 객체 변경
- 변경된 모델 객체를 뷰에 전달
- M : Model
MVC 패턴의 장점
- 동시 다발성(Simultaneous) 개발
- 백엔드, 프론트 엔드가 독립적으로 개발 진행이 가능
- 높은 결합도
- 논리적으로 관련있는 기능을 하나의 컨트롤러로 묶거나,
특정 모델과 관련있는 뷰를 그룹화 할 수 있다
- 논리적으로 관련있는 기능을 하나의 컨트롤러로 묶거나,
- 낮은 의존도
- 뷰, 모델, 컨트롤러는 각각 독립적이다
- 개발 용이성
- 책임이 구분되어 있어 코드 수정이 간편하다
- 한모델에 대한 여러 형태의 뷰를 가질 수 있다
- 동시 다발성(Simultaneous) 개발
MVC 패턴의 단점
- 코드 네비게이션이 복잡하다
- 코드 일관성 유지에 노력이 필요하다
- 높은 학습 곡선
1 |
|
- @GetMapping 정의를 살펴보면 @RequestMapping이 붙어있다
모델 1
2
3
4
5
6
7
8
9
10
11
12import lombok.*;
import java.time.LocalDateTime;
public class Event {
private String name;
private int limitOfEnrollment;
private LocalDateTime startDateTime;
private LocalDateTime endDateTime;
} - 롬복 사용
- 컴파일 시점에 롬복 어노테이션으로 세팅한 값들이 들어간다
- @Data를 해서 간단하게 toString, Hash까지 하게 하고 builder만 붙여도 된다
- @Builder 애노테이션을 붙여주면 이펙티브 자바 스타일과 비슷한 빌더 패턴 코드가 빌드된다.
1 |
|
이제 컨트롤러에서 이 서비스를 이용해서 evnet를 얻을 수 있다
1 |
|
- 리턴되는 문자열은 뷰의 힌트다.
- 기본적으로 resource/templates서 찾는다
- events.html을 만들어 본다
1 |
|
- 타임리프를 쓰려면 네임스페이스를 먼저 추가한다
- 자동완성안되거나 하면 의존성에 타임리프 추가 했는지 살펴보자
2. legacy 정리
예전 레거시 코드를 생각해보자 서블릿..
maven을 사용했다면 maven-archetype-webapp구조를 사용해서 만들었을 것이다
기본 maven폴더구조에 main에서 webapp이라는 새로운 폴더가 있었을 것이다
안에는 샘플로 index.jsp가 들어있고 WEB-INF라는 폴더가 있는데 WEB-INF안에는
정말 중요한 web.xml이 들어 있었을 것이다
이렇게만 만들면 jsp의존성이 없으므로 pom.xml에 servlet-api 의존성을 추가해
주어야한다
1 |
|
- provided는 코딩 시점에는 사용이 되지만 패키징이나 런타임에선 빠지게 된다
보통은 서블릿 컨테이너에서 제공이 되기에 provided로 scope 지정한다 - main에 java디렉토리를 만들고 이를 ide에서 소스 폴더로 지정한다
이제 예전 기억을 되살리면서 패키지를 만든후 서블릿을 만들어 보자
1 | import javax.servlet.http.HttpServlet; |
- 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을 추가해주면 된다
- 톰캣 COnfigure가 되어있다면 선택하면 되고 아니면 Configure눌러서 Base Home을 지정해준다
- 서블릿 컨테이너에 war로 배포하는 방법과 압축을 해제한 상태로 하는 방법이 있는데 exploded를 선택하면 2번쨰 방법으로 배포된다
- 실행시 로그를 보면 war exploded 아티팩트가 서버가 올라갈떄까지 대기하다가 올라가면 배포하는 것을 확인 할 수 있다
- 현재 실행시 보이는 localhost:8080/mavenSimpleJavaWebApp_war_exploded/
화면은 webapp밑의 index.jsp가 보이는 화면이다
위에서 만든 서블릿은 아직 실행이 불가능하며 실행하려면 서블릿을 등록해야한다
가장 기본적인 방법은 web.xml에 등록하는 방법이다
1 |
|
- 이제 실행후 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 |
|
- @WebServlet(“/hello”)처럼 urlPatterns대신 value에 값을 주어도 같은 결과
- 다만 필터에서 필요한 서블릿 이름떄문에 위처럼 하였다
서블릿 기본 프로그래밍을 다시 리뷰하였다
공부하고있는 스프링 MVC도 결국 HttpServlet을 사용한다는 것은 변함이 없다
그럼 그 서블릿등이 스프링 컨트롤러의 어노테이션처럼 멋지게 매핑이 되었을까?
또한 스프링에서는왜 web.xml등의 설정을 하지 않는가?
서블릿 리스너라는 것이 있어서 주요 이벤트를 감지하고 필요한 경우 작업을 할 수 있다. 예를 들어 서블릿이 초기화 될때 DB 커넥션을 만들고 각 작업을 할떄 커넥션을 넘겨주며 destory될떄 커넥션을 닫을 수 있다. 서블릿에서는 서블릿 컨텍스트라는 곳에 들어 있는 addAttribute에 접근할 수 있으며 그 addAttribute에 들어 있는 DB커넥션을 사용할 수도 있다.
리스너에도 여러 종류가 있다
서블릿 컨텍스트 수준
- 컨텍스트 라이프사이클 이벤트
- 컨텍스트 애트리뷰트 변경 이벤트
세션 수준
- 세션 라이프사이클 이벤트
- 세션 애트리뷰트 변경 이벤트
리스너 예제 1
2
3
4
5
6
7
8
9
10
11import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class MyListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
System.out.println("Listener Context Initialized.");
//서블릿콘텍스트에 attribute 추가
// 키값과 object 형태이므로 object에 문자열이 아닌 임의의 객체도 가능
sce.getServletContext().setAttribute("name","ahn");
}
}
이렇게 등록한 attribute는 어떻게 사용할 수 있을까?
1 |
|
서블릿의 영역(scope)은 ServletContext영역과 ServletConfig로 나눠진다
- ServletContext(SC)
- 모든 서블릿이 공유하는 영역
- 하나의 웹 어플리케이션 당 하나가 만들어진다
- ServletConfig
- 서블릿 하나마다 할당되는 영역
- ServletContext(SC)
ServletContext(SC) 얻는 방법
- SC는 ServletConfig의 getServletContext()로 얻는다
- 서블릿은 HttpServlet을 상속하고 HttpServlet은 ServletConfig를 구현하기에
getServletContext()를 바로 사용할 수 있다
이렇게 만든 리스너를 등록하는 방법의 원초적인 방법은 역시 web.xml이다
1 | <web-app> |
servlet과 마찬가지로 3.0부터는 어노테이션으로 가능하다
1 | import javax.servlet.annotation.WebListener; |
또한 서블릿 필터라는 것이 있다.
서블릿 컨테이너와 서블릿 사이에 존재하며 요청이 들어왔을떄 서블릿으로 보낼때. 그리고
서블릿이 작성한 응답을 클라이언트에 보내기전에 특별한 처리등이 가능하다. 필터를 이용해 여러 서블릿이나 특정한 url에 대한 작업이 가능하다 필터는 체인형태로 필터를 추가하면 순서대로 적용이 된다. 순서는 web.xml에 기록한 순서대로 적용된다
리스너는 서블릿 컨텍스트 위쪽의 개념이다
서블릿 필터 예제. servlet의 filter를 import하는 것이다. 햇갈리지 말자 1
2
3
4
5
6
7
8
9
10
11
12
13
14public class MyFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {System.out.println("filter init"); }
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
System.out.println("filter work");
//필터에서 그냥 메세지만 출력하면 다음 필터로 요청 응답이 전달이 안된다
//따라서 chaining을 해주어야한다
filterChain.doFilter(request,response);
}
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//여러 서블릿: (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가지 뜻을 가진다
- 서블릿에서 스프링 프레임워크가 제공하는 IoC 컨테이너를 사용하겠다
- 스프링이 제공하는 DispatcherServlet을 사용하겠다
먼저 1번을 해보자
1) 스프링 프레임워크가 제공하는 IoC 컨테이너를 사용하기
가장 먼저 필요한 것은 의존성이다.
요새 스프링 부트를 사용하면서 아예 손놓고 있지만 그래도 까먹진 말자
위의 2가지를 한꺼번에 처리하기 위해 spring-webmvc를 추가한다
1 | <dependency> |
그리고 원래 등록했었던 MyListener대신에 다음 리스너를 등록한다
1 | <listener> |
1 | 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 WAC는 강제되지 않으므로 여러 DS을 사용할 것이 아니면
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의 초기화 작업을 실제로 수행한다
- MyListener처럼 ServletContextListener를 구현
1 | <context-param> |
- 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
- 사실 그냥 WAC지만 Root WAC와 구분짓기 위해 Servlet WAC 라고 하겠다
계층 관계 컨텍스트
- 부모 자식 모두 param-name이 contextConfigLocation이다
- 전역변수와 지역변수처럼 덮어 써지는 것이 아니라 클래스 상속같은 계층 관계이다
- 따라서 자식 컨텍스트의 빈들은 부모 컨텍스트의 빈들을 주입받을 수 있다
백기선님 설명
- ContextLoaderListener는 스프링 Ioc 컨테이너, 즉 ApplicationContext를 이 서블릿 어플리케이션 생명주기에 맞춰서 바인딩해준다
- 따라서 웹 어플리케이션에 등록된 서블릿들이 사용할 수 있도록 AC를 만들어서 그 AC를 서블릿 컨텍스트에 등록해준다
- 그리고 서블릿 이 종료될 시점에 어플리케이션 컨텍스트를 제거해준다
- 이게 정확히 서블릿 리스너가 할 수 있는 일
- 서블릿 컨텍스트의 라이프 사이클에 맞춰서 스프링이 제공해주는 AC를 연동해주는 가장 핵심적인 리스너
- 이 컨텍스트 리스너는 AC를 만들어야 하기에 스프링 설정파일을 필요로 한다
- 이 리스너의 소스를 열어서 파라미터들을 보면
컨텍스트설정파일 위치라던지 생성할 ApplicationContext의 타입등을 지정할 수 있다 - 기본은 xml이었으나 요새는 자바 설정파일을 하는 추세
다시 예제로 돌아간다
1 | <listener> |
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
8servletContext.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";
}
- 이제 위에서 ContextLoaderListener가 contextConfigLocation로 지정된 me.rkaehdaos.AppConfig 자바 설정 클래스를 참고하여 contextClass로 지정된 AnnotationConfigWebApplicationContext를 만들고 ServletContext에 등록한다
이제 설정파일 클래스와 빈을 만들어보자
1 | package me.rkaehdaos; |
1 | package me.rkaehdaos; |
- 설명
- AppConfig에 @Configuration을 붙여서 스프링 자바 설정 클래스로 만들었다
- 필요시엔 @ComponentScan에 basePackage로 스캔을 시작할 부분을 지정할 수 있다
- 위처럼 하면 HelloService가 빈으로 등록된다
IDE에서 보면 web.xml의
1 | import static org.springframework.web.context.WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE; |
- 설명
- 무식한 예지만 이제 저장된 서블릿컨텍스트에서 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에서 필터랑 서블릿 부분을 삭제하였으면 컨트롤러를 작성한다
1 |
|
- Controller와 RestController의 차이
- HTTP Response Body가 생성되는 방식의 차이
- 기존 MVC의 Controller는 View 기술을 사용
- RestController는 객체 반환시 객체 데이터를 JSON/XML타입의 응답으로 직접 리턴
- @Controller의 메서드에 @ResponseBody를 선언해서도 객체 리턴이 가능하다
- 실제 RestController 소스를 보면 위의 방법이 쓰인다
Root WAC에 컨트롤러가 등록이 안되도록 해보자
1 | import org.springframework.context.annotation.ComponentScan; |
이렇게 하면 이제 Root WAC에 Controller를 제외한 빈들이 등록된다
이제 하나의 Config를 더 만들어보자
1 | import org.springframework.context.annotation.ComponentScan; |
- 설명
- AppConfig와 같은 자바 설정 파일이다
- ComponentScan에서 기본 필터를 끈 후 Controller만 등록하도록 하였다
이제 이 설정파일을 web.xml에 등록하면 끝난다
아래는 완성된 web.xml이다
1 |
|
- 설명
- DispatcherServlet은 서블릿이므로
을 사용해서 등록한다 - 파라미터를 보면 ContextLoaderListener와 거의 흡사함을 알 수 있다
- 새로 지정한 WebConfig를 참조하여 WAC를 만들게 된다
- 서블릿 매핑을
/app/*
하여서/app/*
으로 들어오는 모든 요청은 DS가 처리한다 - 실행후
localhost:8080/app/hello
해보면 정상 작동하는 것을 볼 수 있다
- DispatcherServlet은 서블릿이므로
계층구조로 이루어진 어플리케이션을 작성해 보았다. 사실 꼭 이런 구현이 필요한 것은 아니다. 기초에서 보았던 내용 상기하자. 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()에서 여러 핸들러 매핑
아무런 설정없어도 DS가 자동으로 2개의 핸들러 구현체를 기본으로 등록하였다
- BeanNameUrlHandlerMapping
- 요청 URI와 동일한 이름을 갖는 Controller빈을 매핑한다
- 빈 설정에서 name에 URI와 동일하게 주면 된다
- SimpleUrlHandlerMapping
- Ant 스타일의 경로 매핑 방식을 사용
- mappings 프로퍼티를 통해 매핑 설정 목록을 입력받는다
- RequestMappingHandlerMapping
- @MVC 개발 필수조건
- 기존 DefaultAnnotationHandlerMapping이 deprecated되고 대체됨
- 모든 @Controller빈의 @RequestMapping 어노테이션을 검색
- BeanNameUrlHandlerMapping
위의 경우 반복문을 돌면서 처음 BeanNameUrlHandlerMapping에서 null이 나온다
다시 반복문을 돌면서 RequestMappingHandlerMapping에서 핸들러를 찾는다
핸들러를 찾았으니 반복문을 벗어나서 이제 HandlerAdapter를 찾게 된다
아무런 설정없이도 DS가 자동으로 3개의 핸들러어댑터 구현체를 등록하였다
아래의 3개의 핸들러아답터중 현재 핸들러를 처리할 수 있는 아답터를 찾는다
- HttpRequestHandlerAdapter
- SimpleControllerHandlerAdapter
- 어노테이션 기반이 아닌 Controller 인터페이스 구현으로 만든
클래식 컨트롤러를 처리 가능한 핸들러어댑터
- 어노테이션 기반이 아닌 Controller 인터페이스 구현으로 만든
- RequestMappingHandlerAdapter
- RequestMappingHandlerMapping 에 대응되는 아답터 - 어노테이션 기반
- 컨트롤러 타입을 결정해야하므로 파라미터를 알아야한다
- 따라서 HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler
인터페이스를 가지고 있다
핸들러 아답터까지 받아오게 되면 아답터의.handle()을 이용해 invoke한다
adapter인터페이스의 handle()은 handleInternal()를 호출하고
handleInternal()안에서 invokeHandlerMethod()로 invoke를 하게 된다핸들러에 메소드까지 들어있으므로 자바 리플렉션을 이용해서 해당 메소드를 실행한다
즉 컨트롤러의 hello()가 실행되서 문자열이 반환된다위에서 말했듯이 @RestController는 @Controller에 메소드(여기선 hello())에 @ResponseBody를 붙인것과 동일한 결과이다.
.이 리턴값은 문자열인지 model일지 responeEntity일지 View이름인지 모르므로 이를 처리하기위한 Valuehandler가 존재한다
returnValueHandler는 Converter를 사용해서 리턴값을 Http 본문에 넣어주는 처리를 한다
@ResponseBody를 사용한경우 mv(ModelAndView)는 null이다
따라서 view에 렌더링 하는 과정없이 응답을 바로 보여주게 된다
4-1. DispatcherServlet 더 살펴보기
이젠 뷰가 있는 경우를 살펴보자.
뷰를 처리하기 위해선 RestController를 일반 Controller로 바꿔줘야한다
1 |
|
- 설명
- @RestController를 @Controller로 바꿈
- 이전에 사용했던 부분은 그대로 동작하도록 하기 위해 @ResponseBody를 메소드에 붙임
- 리턴타입에 주는 어노테이션은 존재하지 않는다
- 리턴타입과 관련이 있다고 해서 접근지시자 뒤에 어노테이션을 붙이는 것인데
이 경우 마치 리턴 타입에 어노테이션을 붙이는 모양이지만 이 경우에도
메소드에 적용이 되는 것이지 리턴타입에 적용되지 않음을 꼭 알아두자
- 새로운 매핑을 가지는 sample()을 만든다. 반환되는 리턴값은 역시 문자열이다
- 이 두 메소드의 리턴값이 가지는 의미가 다르다
- hello() : @ResponseBody를 가지므로 문자열 자체가 내용으로 반환된다
위에서 디버거로 잘 살펴보았던 부분을 상기하자 - sample() :
- hello의 경우와 처음부터 리턴값에 대한 returnValueHandler 검색까지는 동일
- sample의 경우 12번째인 ViewNameMethodReturnValueHandler가 선택된다
(즉 View 이름으로 인식을 한 것) - invokeHandlerMethod를 거친후 @ResponseBody가 아니기때문에 mv값이 null이
아니며 객체의 view값에는 sample의 리턴된 문자열이 들어가 있다 - 만약 model객체가 있다면 mv객체의 모델에 바인딩하고 뷰를 렌더링한다
- 뷰 렌더링 -> 문자열에 해당한 jsp를 response응답에 추가
- hello() : @ResponseBody를 가지므로 문자열 자체가 내용으로 반환된다
DispatcherServlet 정리
- DispatcherServlet 초기화
- 다음의 특별한 타입의 빈들을 찾거나, 기본 전력에 해당하는 빈들을 등록
- (대부분 Strategy 패턴 적용)
- HandlerMapping : 핸들러를 찾아주는 인터페이스
- HandlerAdapter : 핸들러를 실행하는 인터페이스
- HandlerExeceptionResolver
- ViewResolver
- …
- 다음의 특별한 타입의 빈들을 찾거나, 기본 전력에 해당하는 빈들을 등록
- DispatcherServlet 동작 순서
- 요청 분석 - Locale, Theme, Multipart 등
- (HandlerMapping에 위임하여)요청을 처리할 핸들러를 찾는다
- (등록된 HandlerAdapter중에) 해단 핸들러를 실행할 수 있는 핸들러 아답터를 찾는다
- 찾아낸 핸들러 아답터를 사용해 핸들러의 응답을 처리한다
- (부가적으로)예외 발생시, 예외처리 핸들러에 요청처리를 위임한다
- 핸들러의 리턴값을 보고 어떻게 처리할지 확인한다
- 뷰이름에 해당하는 뷰를 찾아 모델 데이터를 렌더링한다
- @ResponseEntity가 있다면 Converter를 사용해서 응답 본문을 만든다
- response를 보낸다
스프링 Ioc와 DS의 연결 및 커스텀 뷰 리졸버
1 | /** |
- 동작하는 방식은 다 비슷해서 하나만 따라가봐도 이해가 된다
1 | private void initViewResolvers(ApplicationContext context) { |
- 다른 뷰 리졸버 초기화도 비슷하다
- 예를 들어 initHandlerMappings의 경우에는 detectAllHandlerMappings라는 플래그에
따라서 true면 모든 핸들러를 매핑하고 그렇지 않으면 한개의 핸들러만 매핑하며 따로 선언하지 않았다면 기본 핸들러매핑을 등록한다
(기본값은 true이며 어지간하면 유지. 높은 최적화가 필요할떄는 false로 한개의
핸들러매핑만 등록해서 루프를 덜타게 하는 것도 방법이다) - 기본전략은 하나씩 추적해가면 결국 spring-webmvc-5.1.4.RELEASE.jar쪽에 있는
dispatcherServlet.properties에 기본 전략이 정의되어있다
1 | # Default implementation classes for DispatcherServlet's strategy interfaces. |
- 이제 기존 컨트롤러의 sample()에서 리턴을 다음처럼 view 이름만 해도 된다
HelloController의 sample() 1
2
3
4
5
public String sample() {
//return "/WEB-INF/sample.jsp";
return "sample";
}
이제 initViewResolvers부분에서 기본 리졸버가 등록되지 않는다
자동으로 prefix와 suffix를 합해서 뷰 이름이 생성이 된다
spring mvc 구성 요소
위에서 보았듯이 DS의 기본 전략은 dispatcherServlet.properties에 정의되어 있고
DS의 initStrategies()에서 여러 전략 인터페이스를 초기화 하는 부분을 보았다
1 | /** |
이제 DS이 사용하는 여러가지 인터페이스에 대해 살펴본다
MultipartResolver
- 파일 업로드 요청 처리에 필요한 인터페이스
- http의 contents의 multipart에 바이너리 데이터를 부분부분 쪼개서 보내는데
이를 처리할 수 있는 로직이 필요하다 - HttpServletRequest를 MultipartHttpServletRequest로 변환해주어서
요청이 담고 있는 File을 꺼낼 수 있는 API를 제공한다 - 이 인터페이스 구현체가 bean으로 등록이 되어 있어야 DS가 사용해서 파일 업로드를
처리할 수 있다 - 구현체는 2가지가 존재한다
- CommonsMultiPartResolver
- 먼저 만들어짐
- Apache Commons를 사용
- StandardServletMultipartResolver
- 나중에 만들어짐 (스프링 MVC가 나온 다음)
- Servlet 3.0 기반의 구현체
- CommonsMultiPartResolver
- 스프링 기본 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
- ExceptionHandlerExceptionResolver
RequestToViewNameTranslator
- 핸들러에서 뷰 이름을 명시적으로 리턴하지 않은 경우,
요청을 기반으로 뷰 이름을 판단하는 인터페이스 - 구현체는 DefaultRequestToViewNameTranslator 하나이며 기본으로 등록된다
반환하던 뷰이름을 void로 반환하지 않더라도 정상작동한다 1
2
3
// 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은 다음과 같이 되어있다
1 |
|
이 web.xml을 지우고 코드로 고치면 다음과 같다
1 | public class WebApplication implements WebApplicationInitializer { |
web.xml과 같은 의미이다
DS를 생성해야하는데 web.xml에서 init-param으로 주었던 초기화를 한 context를 넘겨준다
DS를 서블릿으로 등록하고 매핑을 하였다
이기능은 스프링 3.1 이상, 서블릿 3.0 이상에서 지원하는 기능이다
깊게 내려가면 자바가 제공하는 Service Locator까지 가게 된다
(나중에 한번 보자)이처럼 스프링 부트가 없이 사용하는 스프링 MVC의 경우
- Bean에 무엇이 등록되었는지가 중요하다. 등록이 안되었을 경우 기본 빈들이 등록된다
- 서블릿 컨테이너에 DS를 등록하는 형태이다
스프링 부트로 사용하는 스프링 MVC의 경우
- 자바 어플리케이션 안에서 임베디드 톰캣 (혹은 다른 임베디드 서버)가 실핻되는 형태
- 내장 톰캣을 만들고 그 안에 DS를 등록한다
- 스프링부트의 AutoConfigure에 의해 자동으로 설정된다