스프링 MVC - Spring MVC 구조 및 예외처리

600g (Kim Dong Geun)·2020년 8월 29일
4

본 글은 스프링 MVC에 대해 지식을 정리하고 나중에 헷갈릴 때 다시 보기 위한 글입니다 👀

본 게시글은 Spring MVC Quick Start를 참조하여 정리한 글입니다. 📖 👀

본 게시글은 Spring MVC Documentation를 참조하여 정리한 글입니다 📚 👀

사실 저번 Dispatcher Servlet을 직접 만들어 봄으로써 대략적인 MVC구조가 어떻게 돌아가는지 보았다.'
왜 MVC 구조를 통해서 뷰와 비즈니스 로직을 분리함으로써 좀 더 유지보수를 편하게 만들어봤다.

이번에는, 스프링 MVC에서 제공하는 DispatcherServlet을 이용해보도록 하겠다.

스프링 MVC 수행흐름

스프링 MVC는 사용자의 요청이 들어오면 응답을 하기까지 다음과 같은 과정을 거친다.

  1. 클라이언트부터 들어오는 모든 요청을 DispatcherServlet이 받는다.
  2. DispatcherServlet은 HandlerMapping을 통해서 요청을 처리할 Controller를 검색한다.
  3. DispatcherServlet은 검색된 Controller를 실행하여 클라이언트의 요청을 처리한다.
  4. Controller는 비즈니스 로직의 수행 결과로 얻어낸 Model 정보와 Model을 보여줄 View정보를 ModelAndView 객체에 저장하여 리턴한다.
  5. DispatcherServlet은 ViewResolver로부터 View정보를 추출하고, ViewResolver를 이용하여 응답으로 사용할 View를 얻어낸다.
  6. DispatcherServlet은 ViewResolver를 통해 찾아낸 View를 실행하여 사용자에게 응답을 전송한다.

DispatcherServlet등록 및 스프링 컨테이너 구동

DispatcherServlet 등록

Spring MVC에서 가장 중요한 요소가 모든 클라이언트의 요청을 가장 먼저 받아들이는 DispatcherServlet이다. 따라서 Spring MVC 적용에서 가장 먼저 해야할 일은 WEB-INF/web.xml 파일에 스프링에서 제공하는 DispatcherServlet으로 변경하는 것이다.

대부분 Initializer를 사용하면 초기에 등록된 상태다.

  • web.xml
<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은 대소문자를 구분하므로 정확하게 등록해야한다.

  • web.xml
<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이라는 파라미터로 설정한 정보를 추출하여 스프링 컨테이너를 구동할 때 사용한다.

소스 코드로 따지자면 다음과 같이 될 것이다.

  • DispatcherServlet.java
public class DispatcherServlet extends HttpServlet {
    private String contextConfigLocation;
    
    public void init(ServletConfig config) throws ServletException {
        contextConfigLocation = config.getInitParameter("contextConfigLocation");
        new XmlWebApplicationContext(contextConfigLocation);
    }
}

스프링 MVC 적용 준비

우리는 앞서 Controller Interface를 직접 구현해봤다. 하지만 스프링 MVC에서 제공하는 DispatcherServlet을 사용하기 위해서는 스프링에서 제공하는 Controller의 인터페이스를 사용해야 한다.

내부 구조의 Controller는 다음과 같이 구현되어있다.

  • Controller.java
public interface Controller{
    ModelAndView handleRequest(HttpServletRequest request, 
                               HttpServletResponse response) throws Exception;
}

사실 스프링에서 제공하는 Controller 인터페이스도 이전 포스트에 올려놨던 직접 만들어본 Controller와 크게 다르지 않다. 다만 handleRequest()의 리턴타입이 String이 아닌 ModelAndView라는 점이 다를 뿐이다.

Controller 기능 구현

앞에 만들었던 컨트롤러를 들고와서 수정하겠다.

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을 이용해서 각각의 로직을 실행하고 어떤 뷰를 응답으로 반환할지 선택한다.

HandlerMapping 등록

이제 작성된 LoginController가 클라이언트의 "login.do" 요청에 대해서 동작하게 하려면 스프링 설정 파일인 presentation-layer.xml에 HandlerMapping과 LoginController를 <bean>에 등록해야 한다.

  • presentation-layer.xml
<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 객체를 이용한 것만 제외하면 같은 기능을 제공한다.

View Resolver 활용하기

우리는 스프링 설정 파일인 presentation-layer.xml에 HandlerMapping, Controller 클래스들을 Bean으로 등록하여 Spring 컨테이너가 객체를 생성하도록 하였다.

아직 적용하지 않은 한 가지 요소가 있는데 바로 View Resolver다.

ViewResolver를 이용하면 클라이언트로부터 직접적인 View(Jsp와 같은) 호출을 차단할 수 있어서 대부분 웹 프로젝트에서 ViewResolver 사용은 거의 필수다.

ViewResolver역시 여러가지 기능이 있지만 JSP를 View로 사용하는 경우에는 InternalResourceViewResolver를 사용한다.

ViewResolver 적용하기

  • presentation-layer.xml
<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 요청이 있을때만 가능하다.

따라서 컨트롤러도 수정한다.

  • LoginController.java
        ModelAndView mav = new ModelAndView();
        if(user!=null){
        	mav.setViewName("redirect:getBoardList.do");
        } else {
        	mav.setViewName("redirect:login.jsp");
        };

viewName에 각각 redirect:를 붙여준다.

이렇게 하면 외부로부터 직접적인 뷰 호출은 막고, 오직 컨트롤러의 요청에 의해서만 뷰 호출을 할 수 있다.

다음은 위 과정을 어노테이션으로 구현해보겠다!

Annotation 관련 설정

스프링 MVC에서 어노테이션을 사용하려면, 각 객체가 스프링 컨테이너내에 <beans> 형태로 존재해야한다. 따라서 기존에 만들어뒀던 HandlerMapping,Controller,ViewResolver 클래스 모두 삭제하고 <context:component-scan>을 추가 시켜준다.

<beans ...
       ...>
  <context:component-scan base-package="com.x.y.z"/>
</beans>

@Controller 사용하기

기존에는 스프링 컨테이너가 Controller 클래스를 생성하게 하려면 Controller 클래스들을 스프링 설정 파일에 빈으로 등록해야 했다. 그러나 어노테이션을 사용하면 모두 Bean으로 등록할 필요없이 클래스 선언부 위에 @Controller를 붙이면 된다

@Controller는 내부적으로 @Component를 상속하고 있기때문으로 Bean으로 등록되며, 추가적으로 DispatcherServlet이 인식할 수 있는 객체로 만들어 준다.

만약 Controller를 붙이지 않는다면 SpringMVC에서 제공하는 Controller 인터페이스를 재구현해줘야 한다.

RequestMapping 사용하기

@RequestMapping은 기존의 HandlerMapping과 같은 역할을 한다. RequestMapping을 이용하여 요청을 받을 경로와 HTTP 메소드를 설정할 수 있다.

  • SomeController.java
@Controller
public class SomeController {
	
    @RequestMapping(value={"/hello"}, method=RequestMethod.GET)
    public String helloWorld(){
        return "helloworld";
    }
}

RequestParam 사용하기

스프링 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 사용하기

@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로 설정된 에러처리 로직으로 빠지게 된다.

profile
수동적인 과신과 행운이 아닌, 능동적인 노력과 치열함

0개의 댓글