본 글은 스프링 MVC에 대해 지식을 정리하고 나중에 헷갈릴 때 다시 보기 위한 글입니다 👀
본 게시글은 Spring MVC Quick Start를 참조하여 정리한 글입니다. 📖 👀
본 게시글은 Spring MVC Documentation를 참조하여 정리한 글입니다 📚 👀
사실 저번 Dispatcher Servlet을 직접 만들어 봄으로써 대략적인 MVC구조가 어떻게 돌아가는지 보았다.'
왜 MVC 구조를 통해서 뷰와 비즈니스 로직을 분리함으로써 좀 더 유지보수를 편하게 만들어봤다.
이번에는, 스프링 MVC에서 제공하는 DispatcherServlet
을 이용해보도록 하겠다.
스프링 MVC는 사용자의 요청이 들어오면 응답을 하기까지 다음과 같은 과정을 거친다.
DispatcherServlet
이 받는다.Spring MVC에서 가장 중요한 요소가 모든 클라이언트의 요청을 가장 먼저 받아들이는 DispatcherServlet이다. 따라서 Spring MVC 적용에서 가장 먼저 해야할 일은 WEB-INF/web.xml 파일에 스프링에서 제공하는 DispatcherServlet으로 변경하는 것이다.
대부분 Initializer를 사용하면 초기에 등록된 상태다.
<web-app ... >
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
간단하게 설명하자면, action이라는 이름을 가진 DispatcherServlet을 등록하고, 서블릿 컨테이너는 *.do
라는 경로의 요청이 있어야 DispatcherServlet 객체를 생성한다.
DispatcherServlet
객체가 생성되고 나면 DispatcherServlet 클래스 내에 init()
메소드가 자동으로 실행되어 XmlWebApplicationContext
라는 컨테이너가 구동된다.
즉 DispatcherServlet이 XmlWebApplcationContext
를 생성하여 객체들의 라이프 사이클을 관리한다.
Spring MVC 구성요소 중에서 DispatcherServlet 클래스가 유일한 서블릿이다. 따라서 서블릿 컨테이너는 web.xml 파일에 등록된 DispatcherServlet만 생성해준다. 하지만 DispatcherServlet 객체 혼자서는 클라이언트의 요청을 처리할 수 없고, 반드시 HandlerMapping
, Controller
, View Resolver
객체들과의 상호작용이 있어야 한다.
위와 같은 객체들을 메모리에 생성하기 위해 DispatcherServlet은 스프링 컨텡이너를 구동하는 것이다.
현재 상태는 DispatcherServlet이 스프링 컨테이너를 구동할 때 무조건 /WEB-INF/action-servlet.xml 파일을 찾아 로딩한다. 그런데 해당 위치에 action-servlet.xml이 존재하지 않아, FileNotFoundException
이 발생한다.
DispatcherServlet은 Spring Container를 구동할 때, web.xml파일에 등록된 서블릿 이름 뒤에
-servlet.xml
을 붙여서 스프링 설정파일을 찾는다.
따라서 위에서 servlet-name을 action으로 설정을 했기 때문에, action-servlet.xml을 찾으려 할 것이다.
DispatcherSerlvet은 자신이 사용할 객체들을 생성하기 위해서 스프링 컨테이너를 구동한다. 앞서 스프링 컨테이너를 위한 설정 파일의 이름과 위치는 서블릿 이름을 기준으로 자동으로 결정된다. 하지만 필요에 따라서 설정파일의 이름을 바꾸거나 위치를 변경할 수 있다.
서블릿 초기화 파라미터를 이용하면 된다.
WEB-INF 폴더내에 임의의 이름을 가진 servlet.xml 파일을 생성한다. 예를들면 presentation-layer.xml
같은 것 말이다.
그리고 web.xml 파일을 열어서 DispatcherServvlet 클래스를 등록한 곳에 <init-param>
설정을 추가한다. 이때, <param-name>엘리 먼트로 지정한 contextConfigLocation
은 대소문자를 구분하므로 정확하게 등록해야한다.
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/config/presentation-layer.xml</param-value>
</init-param>
</servlet>
으로 수정한다.
이렇게 DispatcherServlet을 설정하면 스프링 컨테이너가 DispatcherServlet
객체를 생성한 후, 다음과 같이 init()
메소드를 호출한다. 그리고 contextConfigLocation
이라는 파라미터로 설정한 정보를 추출하여 스프링 컨테이너를 구동할 때 사용한다.
소스 코드로 따지자면 다음과 같이 될 것이다.
public class DispatcherServlet extends HttpServlet {
private String contextConfigLocation;
public void init(ServletConfig config) throws ServletException {
contextConfigLocation = config.getInitParameter("contextConfigLocation");
new XmlWebApplicationContext(contextConfigLocation);
}
}
우리는 앞서 Controller
Interface를 직접 구현해봤다. 하지만 스프링 MVC에서 제공하는 DispatcherServlet을 사용하기 위해서는 스프링에서 제공하는 Controller의 인터페이스를 사용해야 한다.
내부 구조의 Controller는 다음과 같이 구현되어있다.
public interface Controller{
ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception;
}
사실 스프링에서 제공하는 Controller 인터페이스도 이전 포스트에 올려놨던 직접 만들어본 Controller와 크게 다르지 않다. 다만 handleRequest()의 리턴타입이 String이 아닌 ModelAndView
라는 점이 다를 뿐이다.
앞에 만들었던 컨트롤러를 들고와서 수정하겠다.
public class LoginController implements Controller{
@Override
public ModelAndView handleRequest(HttpServletRequest request,HttpServletResponse response){
System.out,println("로그인 처리");
String id = request.getParameter("id");
String password = request.getParameter("password");
UserVO vo = new UserVO();
vo.setId(id);
vo.setPassword(password); //JavaBeans 패턴
UserDao userDao = new UserDao();
UserVo user = userDao.getUser(vo);
ModelAndView mav = new ModelAndView();
if(user!=null){
mav.setViewName("getBoardList.do");
} else {
mav.setViewName("login.jsp");
};
return mav;
}
}
기존에 사용하던 LoginController 클래스에서 handleRequest()메소드의 리턴타입을 ModelAndView
로 수정한다. 그리고 setViewName을 이용해서 각각의 로직을 실행하고 어떤 뷰를 응답으로 반환할지 선택한다.
이제 작성된 LoginController가 클라이언트의 "login.do" 요청에 대해서 동작하게 하려면 스프링 설정 파일인 presentation-layer.xml
에 HandlerMapping과 LoginController를 <bean>에 등록해야 한다.
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/login.do">login</prop>
</props>
</property>
</bean>
<!-- Controller 등록-->
<bean id="login" class="com.x.y.z.LoginController"/>
위 설정에서는 SimpleUrlHandlerMapping
객체는 Setter 인젝션을 통해 Properties 타입의 컬렉션 객체를 의존성 주입하고 있다.
그리고 의존성 주입된 Properties
컬렉션에는 "/login.do" 경로 요청에 대해 아이디가 login인 객체가 매핑되어있다.
따라서 login.do라는 경로가 들어오면 login이라는 Id를 가진 Bean의 HandlerRequest()를 수행하게 될 것이다.
SimpleUrlHandlerMapping의 기능은 우리가 직접 구현한 HandlerMappping과 같다.
대신 Properties 대신 HashMap 객체를 이용한 것만 제외하면 같은 기능을 제공한다.
우리는 스프링 설정 파일인 presentation-layer.xml
에 HandlerMapping, Controller 클래스들을 Bean으로 등록하여 Spring 컨테이너가 객체를 생성하도록 하였다.
아직 적용하지 않은 한 가지 요소가 있는데 바로 View Resolver다.
ViewResolver를 이용하면 클라이언트로부터 직접적인 View(Jsp와 같은) 호출을 차단할 수 있어서 대부분 웹 프로젝트에서 ViewResolver 사용은 거의 필수다.
ViewResolver역시 여러가지 기능이 있지만 JSP를 View로 사용하는 경우에는 InternalResourceViewResolver
를 사용한다.
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/board/"/>
<property name="suffix" value=".jsp"/>
</bean>
View Resolver를 등록하면 WEB-INF 폴더는 절대 브라우저에서 접근하라 수 없다. 하지만 InternalResouceViewResolver
를 위와 같이 설정하면 JSP 파일을 View화면으로 사용할 수 있다.
단 컨트롤러의 Redirect 요청이 있을때만 가능하다.
따라서 컨트롤러도 수정한다.
ModelAndView mav = new ModelAndView();
if(user!=null){
mav.setViewName("redirect:getBoardList.do");
} else {
mav.setViewName("redirect:login.jsp");
};
viewName에 각각 redirect:
를 붙여준다.
이렇게 하면 외부로부터 직접적인 뷰 호출은 막고, 오직 컨트롤러의 요청에 의해서만 뷰 호출을 할 수 있다.
다음은 위 과정을 어노테이션으로 구현해보겠다!
스프링 MVC에서 어노테이션을 사용하려면, 각 객체가 스프링 컨테이너내에 <beans>
형태로 존재해야한다. 따라서 기존에 만들어뒀던 HandlerMapping
,Controller
,ViewResolver
클래스 모두 삭제하고 <context:component-scan>
을 추가 시켜준다.
<beans ...
...>
<context:component-scan base-package="com.x.y.z"/>
</beans>
기존에는 스프링 컨테이너가 Controller 클래스를 생성하게 하려면 Controller 클래스들을 스프링 설정 파일에 빈으로 등록해야 했다. 그러나 어노테이션을 사용하면 모두 Bean으로 등록할 필요없이 클래스 선언부 위에 @Controller를 붙이면 된다
@Controller는 내부적으로 @Component를 상속하고 있기때문으로 Bean으로 등록되며, 추가적으로 DispatcherServlet이 인식할 수 있는 객체로 만들어 준다.
만약 Controller를 붙이지 않는다면 SpringMVC에서 제공하는 Controller 인터페이스를 재구현해줘야 한다.
@RequestMapping은 기존의 HandlerMapping
과 같은 역할을 한다. RequestMapping을 이용하여 요청을 받을 경로와 HTTP 메소드를 설정할 수 있다.
@Controller
public class SomeController {
@RequestMapping(value={"/hello"}, method=RequestMethod.GET)
public String helloWorld(){
return "helloworld";
}
}
스프링 MVC에서는 HTTP 요청 파라미터 정보를 추출하기 위한 @RequestParam
을 제공한다. @RequestParam을 이용하면 Command 클래스에는 없는 파라미터 정보를 추출할 수 있다.
@Controller
public BoardController {
@RequestMapping("/getBoardList.do")
public String getBoardList(@RequestParam(value="searchCondition") String searchCondition){
System.out.print("검색 조건 : " + searchCondition);
return "getBoardList.jsp";
}
}
@ModelAttribute
가 설정된 메소드는 @RequestMapping
어노체이션이 적용된 메소드보다 먼저 호출된다. 그리고 @ModelAttribute 실행결과로 리턴된 객체는 자동으로 Model에 저장된다.
스프링에서는 @ControllerAdvice
와 @ExceptionHandler
어노테이션을 이용하여 컨트롤러 메소드 수행 중 발생하는 예외를 일괄적으로 처리할 수 있다.
위, 두 어노테이션을 사용하기 위해서는 예외처리 관련 네임스페이스를 추가해야한다.
<beans>
<mvc:annotation-driven/>
</beans>
만약 <mvc:annotation-driven/>
을 추가하지 않는다면 @ExceptionHandler 어노테이션을 인식하지 않는다.
다음은 ControllerAdvice에 대한 예제를 설명하면서 이해해보도록 하겠다.
@ControllerAdvice("com.x.y.z.Controller")
public class CommonExceptionHandler{
@ExceptionHandler(ArithmeticException.class)
public ModelAndView handleArithmeticException(Exception e){
ModelAndView mav = new ModelAndView();
mav.addObject("exception",e);
mav.setViewName("/common/arithmeticError.jsp");
return mav;
}
@ExceptionHandler(NullPointerException.class)
public ModelAndView handleNullPointerException(Exception e){
ModelAndView mav = new ModelAndView();
mav.addObject("exception",e);
mav.setViewName("/common/nullPointerError.jsp");
return mav;
}
}
com.x.y.z.Controller내의 컨트롤러 객체중에서 예외가 발생했을때 @ExceptionHandler
내에 선언된 예외클래스라면 진행중인 비즈니스 로직을 멈추고 @ExceptionHandler로 설정된 에러처리 로직으로 빠지게 된다.