DispatcherServlet 설정 및 Spring 콘텍스트 설정

Spring MVC를 사용하기 위해서는 가장 먼저 web.xml 파일에 DispatcherServlet 설정을 추가해주어야 한다. DispatcherServlet은 서블릿 클래스로서 다음과 같이 서블릿 설정 및 서블릿 매핑 설정을 web.xml 파일에 추가해주면 된다.

<?xml version="1.0" ?>

<web-app version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
                        http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <servlet>
        <servlet-name>example</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>example</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>

위 설정은 모든 *.do로 들어오는 요청을 DispatcherServlet이 처리하도록 하고 있다.

DispatcherServlet은 설정 파일에서 지정한 서블릿 이름을 사용하여 Spring 설정 파일을 로딩한다. 예를 들어, 위 코드의 경우 DispatcherServlet의 이름이 "example"인데, 이 경우 사용되는 Spring 설정 파일은 'WEB-INF/example-servlet.xml' 이다.

만약 기본적으로 사용되는 Spring 콘텍스트 설정 파일이 아닌 다른 이름의 파일을 사용하고 싶다면 다음과 같이 contextConfigLocation 초기화 파라미터에 사용할 설정 파일의 목록을 지정해주면 된다.

    <servlet>
        <servlet-name>multipleConfig</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                /WEB-INF/mvc1.xml,
                /WEB-INF/mvc2.xml
            </param-value>
        </init-param>
        <load-on-startup>3</load-on-startup>
    </servlet>

이때 각 설정 파일은 콤마를 사용하여 구분한다. 만약 각 설정 파일에서 동일한 이름의 빈을 지정했다면 더 뒤에 위치한 설정 파일에 명시된 빈이 우선순위를 갖는다.

HandlerMapping 설정

클라이언트의 요청을 Spring의 DispatcherServlet이 처리하도록 설정했다면, 다음으로 해야 할 작업은 어떤 HandlerMapping을 사용할지의 여부를 지정하는 것이다. HandlerMapping은 클라이언트의 요청을 어떤 Commender가 수행할 지의 여부를 결정해주는데, 표 2와 같이 두 개의 구현체가 주로 사용된다.

구현체 설명
BeanNameUrlHandlerMapping 요청 URI와 동일한 이름을 가진 Controller 빈을 매핑한다.
SimpleUrlHandlerMapping Ant 스타일의 경로 매핑 방식을 사용하여 URI와 Controller 빈을 매핑한다.

BeanNameUrlHandlerMapping

BeanNameUrlHandlerMapping은 요청 URI와 동일한 이름을 갖는 Controller 빈으로 하여금 클라이언트의 요청을 처리하도록 한다. 예를 들어, http://some.com/hello.do 와 같은 요청 URL에 대해 "/hello.do" 라는 이름을 가진 Controller 빈이 해당 요청을 처리하도록 한다. 아래 코드는 BeanNameUrlHandlerMapping를 사용하는 경우의 Spring 설정 파일의 예를 보여주고 있다.

<?xml version="1.0" encoding="UTF-8" ?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="beanNameUrlMapping" 
        class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />
    
    <bean name="/hello.do"
        class="net.daum.ts.techReport.report2.springmvc.HelloController" />
    
    <bean name="/login.do"
        class="net.daum.ts.techReport.report2.springmvc.LoginController" />
    
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/view/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

만약 요청 URL이 http://some.com/login.do 였다면, 이름이 "/login.do"인 LoginController가 해당 요청을 처리하게 된다. 비슷한 이유로, http://some.com/hello.do 요청은 HelloController가 처리하게 된다.

HandlerMapping 빈을 따로 생성하지 않은 경우 DispatcherServlet은 기본적으로 HandlerMapping의 구현체로 BeanNameUrlHandlerMapping을 사용한다. 따라서, 위 코드에서 HandlerMapping과 관련된 설정은 생략할 수 있다.

SimpleUrlHandlerMapping

SimpleUrlHandlerMapping은 Ant 스타일의 매핑 방식을 사용하여 Controller를 매칭한다. 아래 코드는 SimpelUrlHandlerMapping을 사용하여 클라이언트의 요청 URL과 매핑될 Controller를 지정하는 예를 보여주고 있다.

<?xml version="1.0" encoding="UTF-8" ?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="beanNameUrlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <value>
                /content/*.html=contentController
                /**/help.html=helpController
            </value>
        </property>
    </bean>
    
    <bean name="contentController" 
        class="net.daum.ts.techReport.report2.springmvc.ContentController" />
    
    <bean name="helpController" 
        class="net.daum.ts.techReport.report2.springmvc.HelpController" />
    
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/view/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

SimpleUrlHandlerMapping은 mappings 프로퍼티를 통해서 매핑 설정 목록을 입력받는다. 이때 설정의 이름은 Ant 스타일의 URL 경로를 의미하며, 값은 매핑될 Controller 빈의 이름을 의미한다. 예를 들어, 위 예제의 경우 http://some.com/content/1.html 이나 http://some.com/content/mvc.html 요청이 들어오면 contentController가 처리하도록 매핑하였다.

SimpleUrlHandlerMapping에서 사용되는 규칙은 다음과 같다.

  • ? - 한 글자에 매칭된다.
  • * - 0 개 이상의 글자에 매칭된다.
  • ** - 0개 이상의 디렉토리에 매칭된다.
아래는 org.springframework.util.AntPathMatcher API 문서에 예시로 나온 몇 가지 매핑 설정의 예이다.

  • com/t?st.jsp - com/test.jsp, com/tast.jsp 또는 com/txst.jsp 등에 매칭된다.
  • com/*.jsp - com 디렉토리에 있는 모든 .jsp에 매칭된다.
  • com/**/test.jsp - com 경로 아래에 위치한 모든 test.jsp와 매칭된다.
  • org/springframework/**/*.jsp - org/springframework 경로 아래에 위치한 모든 jsp와 매칭된다.
두 HandlerMapping 구현체의 공통 프로퍼티

앞서 살펴본 두 HandlerMapping 구현 클래스는 AbstractUrlHandlerMapping 클래스를 상속받고 있으며, AbstractUrlHandlerMapping 클래스는 AbstractHandlerMapping 클래스를 상속받고 있다. 이 두 클래스는 공통으로 사용되는 몇 가지 프로퍼티를 제공하고 있는데, 먼저 AbstractHandlerMapping 클래스는 다음과 같은 프로퍼티를 제공하고 있다.

프로퍼티 설명
interceptors 사용할 인터셉터의 목록
defaultHandler 매칭되는 핸들러를 찾지 못할 때 사용할 기본 핸들러
order 하나의 서블릿 콘텍스트에서 다수의 HandlerMapping을 사용할 수 있는데, 이 경우 HandlerMapping 간의 우선순위를 지정한다.

AbstractUrlHandlerMapping 클래스가 제공하는 프로퍼티 목록은 표 4와 같다.

프로퍼티 설명
alwaysUseFullPath 웹 어플리케이션 콘텍스트의 경로를 포함할 지의 여부를 결정한다. 예를 들어, 콘텍스트 경로가 /main 일때, http://some.com/main/a.html 에 대해, 이 값이 true면 /main/a.html 이 사용된다. 이 값이 false이면 /a.html이 사용된다. 기본값은 false이다.
urlDecode HandlerMapping에 요청 URL을 전달하기 전에 특정 인코딩을 사용해서 디코딩할지의 여부를 결정한다. 이 값을 true로 지정하면 요청에 명시된 인코딩이나 ISO-8859-1을 사용하여 디코딩한다. 기본값은 false이다.
lazyInitHandlers 싱글톤 타입의 핸들러에 대해 lazy initialization을 적용할지의 여부를 지정한다. 기본 값은 false이다.

ViewResolver 설정

HandlerMapping을 설정했다면, 이제 ViewResolver를 설정할 차례이다. ViewResolver는 Controller의 실행 결과를 어떤 뷰를 보여줄지의 여부를 결정하는 기능을 제공한다. 주로 사용되는 ViewResolver에는 다음의 두 가지가 존재한다.

  • InternalResourceViewResolver - JSP를 사용하여 뷰를 생성한다.
  • VelocityViewResolver - Velocity 템플릿 엔진을 사용하여 뷰를 생성한다.
InternalResourceViewResolver

InternalResourceViewResolver는 JSP를 사용하여 Commander의 결과 뷰를 생성할 때 사용된다. 아래 코드는 InternalResourceViewResolver의 설정 예를 보여주고 있다.

<?xml version="1.0" encoding="UTF-8" ?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="beanNameUrlMapping"
        class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />
    
    <bean name="/hello.do"
        class="net.daum.ts.techReport.report2.springmvc.HelloController" />

    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/view/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

InternalResourceViewResolver는 다음과 같이 두 개의 프로퍼티를 입력받는다.

  • prefix - Controller가 리턴한 뷰 이름 앞에 붙을 접두어
  • suffix - Controller가 리턴한 뷰 이름 뒤에 붙을 확장자
예를 들어, HelloController가 처리 결과를 보여줄 뷰의 이름으로 "hello"를 리턴했다고 하자. 이 경우 InternalResourceViewResolver에 의해 사용되는 뷰는 "/view/hello.jsp"가 된다. 따라서, /view/hello.jsp가 생성한 응답 결과 화면이 클라이언트에 전달된다.

VelocityViewResolver

VelocityViewResolver는 Velocity 템플릿을 사용하여 뷰를 생성할 때 사용된다. 아래 코드는 VelocityViewResolver의 설정 예를 보여주고 있다.

<?xml version="1.0" encoding="UTF-8" ?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean name="/hello.mul"
        class="net.daum.ts.techReport.report2.springmvc.HelloController" />
    
    <bean id="velocityConfig" 
        class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath" value="/view_vm/" />
        <property name="velocityProperties">
            <value>
                input.encoding=UTF-8
                output.encoding=UTF-8
            </value>
        </property>
    </bean>
    
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="contentType" value="text/html; charset=UTF-8" />
        <property name="cache" value="true" />
        <property name="prefix" value="" />
        <property name="suffix" value=".vm" />
    </bean>
</beans>

VelocityViewResolver를 사용하기 위해서는 먼저 Velocity의 설정 정보를 관리해주는 VelocityConfigurer를 생성해주어야 한다. VelocityConfigurer에서 사용가능한 주요 프로퍼티는 다음과 같다.

  • resourceLoaderPath - Velocity 템플릿이 위치하는 경로 (웹 콘텍스트 루트 기준)
  • velocityProperties - Velocity 설정 프로퍼티
예를 들어, 위 코드의 경우는 웹 어플리케이션 루트에 위치한 /view_vm/ 디렉토리에 Velocity 템플릿 파일이 위치한다는 것을 의미한다.

VelocityViewResolver를 설정할 때에는 다음의 프로퍼티를 알맞게 지정해준다.

  • contentType - 응답의 컨텐트 타입을 지정한다.
  • prefix - InternalResourceViewResolver와 동일
  • suffix - InternalResourceViewResolver와 동일
위 설정의 경우, Controller가 선택한 뷰가 "hello"라고 한다면 /view_vm/hello.vm 파일이 템플릿으로 선택된다. 또한 생성되는 뷰의 컨텐트 타입은 "text/html" 이고 캐릭터셋은 UTF-8이 된다.

Controller 구현

지금까지는 환경 설정과 관련된 부분에 대해서 설명했는데, 이제 본격적으로 클라이언트의 요청을 처리하는 데 사용되는 Controller의 구현에 대해서 살펴보도록 하자. Controller를 구현하는 가장 간단한 방법은 Controller 인터페이스를 implements 하는 것이다. 하지만, Controller 인터페이스를 직접적 implements 하기 보다는, Controller 인터페이스를 implements 하고 몇 가지 추가적인 기능을 구현하고 있는 클래스들을 상속받아 Controller를 구현하는 것이 일반적이다.

Controller 인터페이스

org.springframework.web.servlet.mvc.Controller 인터페이스는 다음과 같이 정의되어 있다.

public interface Controller {
    
    ModelAndView handleRequest(HttpServletRequest request,
                               HttpServletResponse response)
                               throws Exception

}

Controller를 구현하는 가장 단순한 방법은 Controller 인터페이스를 구현한 뒤, handleRequest() 메소드를 알맞게 구현하는 것이다. 아래 코드는 구현 예이다.

package net.daum.ts.techReport.report2.springmvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class SimpleController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        // request를 사용하여 클라이언트 요청 분석 후
        // 클라이언트 요청에 따른 알맞은 처리
        ModelAndView mav = new ModelAndView("hello");
        mav.addObject("greeting", "안녕하세요");
        return mav;
    }

}

handleRequest() 메소드는 파라미터로 전달받은 request 객체와 response 객체를 사용하여 클라이언트가 요청한 기능을 알맞게 구현하면 된다.

handleRequest() 메소드는 ModelAndView 객체를 리턴한다. ModelAndView 객체는 클라이언트의 요청을 처리한 결과를 보여줄 뷰 페이지와 관련해서 다음과 같은 정보를 제공한다.

  • 결과를 보여줄 뷰 이름 지정 : 위 코드의 경우 "hello"를 뷰로 선택
  • 뷰에서 사용할 모델 데이터 : 위 코드의 경우 addObject() 메소드로 추가한 객체, 뷰에서는 "greeting" 이라는 이름을 사용하여 데이터에 접근할 수 있다.
예를 들어, ViewResolver를 다음과 같이 설정했다고 해 보자.

    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/view/" />
        <property name="suffix" value=".jsp" />
    </bean>

위 viewResolver는 "/view/" + 뷰이름 + ".jsp"를 뷰로 사용하게 된다. SimpleController는 뷰 이름으로 "hello"를 리턴하므로, "/view/hello.jsp"가 뷰로 사용된다. 또한, 이 뷰에서는 다음과 같이 ModelAndView에 저장된 객체를 사용할 수 있게 된다.

<%@ page contentType="text/html; charset=UTF-8" %>

<strong>${greeting}</strong> ..

AbstractController를 이용한 구현

Controller를 구현할 때에는 Controller 인터페이스를 직접 구현하기보다는, AbstractController 추상 클래스를 상속 받아 구현하는 경우가 더 많다. AbstractController 클래스는 Controller 인터페이스를 implements 하여 추가적인 기능을 제공하고 있다.

AbstractController 클래스는 표 5와 같은 프로퍼티를 제공하고 있으며, 이 프로퍼티를 사용하여 HTTP Session, 캐시 등의 설정을 제어할 수 있다.

프로퍼티 설명
supportedMethods Controller가 처리할 수 있는 메소드를 지정한다. GET과 POST 등 처리할 수 있는 메소드를 지정할 수 있다. 만약 지원되지 않는 메소드를 사용하여 요청이 들어오면 ServletException 예외를 발생시킨다.
requiresSession Controller가 HTTP Session을 필요로 하는 지의 여부를 지정한다. 이 값이 true인 경우, 클라이언트와 관련된 세션이 존재하지 않으면 ServletException 예외를 발생시킨다.
synchronizeSession HTTP Session을 사용하여 Controller에 대한 처리를 동기화 할지의 여부를 지정한다.
cacheSeconds HTTP 응답에 캐시와 관련된 디렉티브를 생성할지의 여부를 지정한다. 기본 값은 -1이며, 이 경우 캐시 디렉티브가 응답 결과에 포함되지 않는다.
useExpiresHeader HTTP 1.0에 호환되는 "Expires" 헤더의 사용 여부를 지정한다. 기본값은 true 이다.
useCacheHeader HTTP 1.1에 호환되는 "Cache-Control' 헤더의 사용 여부를 지정한다. 기본값은 true 이다.

AbstractController 클래스를 상속받아 Controller를 구현하는 클래스는 아래 코드와 같이 handleRequestInternal() 메소드를 구현해주면 된다.

package net.daum.ts.techReport.report2.springmvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;

public class HelloController extends AbstractController {

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        ModelAndView view = new ModelAndView("hello");
        view.addObject("greeting", "안녕하세요");
        return view;
    }

}

AbstractController 클래스의 handleRequest() 메소드는 내부적으로 필요한 작업을 수행한 뒤, handleRequestInternal() 메소드를 호출한다. 따라서, AbstractController 클래스를 상속받는 경우에는 handleRequest() 메소드가 아닌 handlerRequestInternal() 메소드를 구현해주어야 한다.

AbstractCommandController를 이용한 파라미터 값 전달

요청 파라미터의 값을 특정한 객체에 저장하고 싶다면 AbstractCommandController 클래스를 사용하면 된다. AbstractCommandController 클래스를 상속받은 클래스는 다음과 같이 handle() 메소드를 알맞게 구현해주면 된다.

package net.daum.ts.techReport.report2.springmvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractCommandController;

public class LoginController extends AbstractCommandController {

    @Override
    protected ModelAndView handle(HttpServletRequest request,
            HttpServletResponse response, Object command, BindException errors)
            throws Exception {
        LoginCommand loginCommand = (LoginCommand) command;
        authService.authenticate(loginCommand.getId(), loginCommand.getPassword());
        return new ModelAndView("loginSuccess", "loginCommand", loginCommand);
    }

}

handle() 메소드의 세 번째 파라미터는 파라미터의 값을 저장한 command 객체이다. 이 command 객체의 타입을 지정하기 위해서는 다음과 같이 Spring 설정 파일에서 commandClass 프로퍼티의 값으로 클래스의 이름을 지정해주면 된다.

    <bean name="/login.do"
        class="net.daum.ts.techReport.report2.springmvc.LoginController">
        <property name="commandClass" 
            value="net.daum.ts.techReport.report2.springmvc.LoginCommand" />
    </bean>

handle() 메소드에 전달되는 command 파라미터는 commandClass 프로퍼티에 지정한 타입의 객체가 된다. 이때 파라미터와 생성할 command 객체의 프로퍼티 사이의 매핑은 자바빈 스타일을 따른다. 예를 들어, 파라미터 이름이 id 인 경우 setId() 메소드를 통해 파라미터 값이 전달되며, 파라미터 이름이 address 인 경우 setAddress() 메소드를 통해 파라미터 값이 전달된다.

SimpleFormController를 이용한 폼 전송 처리

SimpleFormController는 AbstractCommandController와 마찬가지로 파라미터의 값을 객체에 저장할 때 사용된다. 차이점이 있다면, SimpleFormController는 폼 전송 개념이 적용되어 있다는 것이다. SimpleFormController는 POST 방식으로 요청이 들어올 경우 doSubmitAction() 메소드를 통해 요청을 처리하는 반면에, GET 방식으로 요청이 들어오면 "formView" 프로퍼티로 지정한 뷰를 출력한다.

SimpleFormController는 클라이언트가 POST로 전송한 데이터는 commandClass 프로퍼티로 지정한 타입의 객체에 저장되어 doSubmitAction() 메소드에 전달된다. 아래 코드는 SimpleFormController의 구현 예를 보여주고 있다.

package net.daum.ts.techReport.report2.springmvc;

import org.springframework.web.servlet.mvc.SimpleFormController;

public class LoginFormController extends SimpleFormController {

    @Override
    protected void doSubmitAction(Object command) throws Exception {
        LoginCommand loginCommand = (LoginCommand) command;
        if (!loginCommand.getId().equals("madvirus")) {
            throw new LoginFailException("invalid id: "+loginCommand.getId());
        }
    }

}

클라이언트가 요청한 작업을 성공적으로 수행하지 못한 경우, doSubmitAction() 메소드는 예외를 발생시켜서 올바르게 처리하지 못했음을 알리게 된다. 이 경우, 예외 타입에 따라 알맞게 예외 처리를 해 주어야 한다.

GET 방식으로 데이터를 전송하는 경우에는 doSubmitAction() 메소드가 호출되지 않는다. 대신, formView 프로퍼티로 지정한 뷰를 보여준다. 아래 코드는 SimpleFormController 클래스를 상속받은 클래스의 구현 예를 보여주고 있다.

    <bean name="/loginUsingForm.do"
        class="net.daum.ts.techReport.report2.springmvc.LoginFormController">
        <property name="commandClass" 
            value="net.daum.ts.techReport.report2.springmvc.LoginCommand" />
        <property name="formView" value="loginForm" />
        <property name="successView" value="loginSuccess" />
    </bean>

뷰 영역에 데이터를 전달하고 싶다면 다음과 같이 onSubmit() 메소드를 추가적으로 구현해주면 된다.

package net.daum.ts.techReport.report2.springmvc;

import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;

public class LoginFormController extends SimpleFormController {

    @Override
    protected void doSubmitAction(Object command) throws Exception {
        LoginCommand loginCommand = (LoginCommand) command;
        if (!loginCommand.getId().equals("madvirus")) {
            throw new LoginFailException("invalid id: "+loginCommand.getId());
        }
    }
    
    @Override
    protected ModelAndView onSubmit(Object command, BindException errors) 
    throws Exception {
        try {
            ModelAndView mav = super.onSubmit(command, errors);
            mav.addObject("loginCommand", command);
            return mav;
        } catch(Exception e) {
            throw e;
        }
    }
}

위 코드에서 super.doSubmit() 메소드를 호출하면 doSubmitAction() 메소드가 호출된다. 따라서, doSubmitAction() 메소드가 성공적으로 수행되면 ModelAndView.addObject() 메소드를 사용하여 뷰에 데이터를 전달할 수 있게 된다.

뷰의 구현

뷰에서는 ModelAndView 객체에 저장한 객체를 사용할 수 있다. 예를 들어, Controller에서 다음과 같이 ModelAndView에 데이터를 저장했다고 해 보자.

    protected ModelAndView handle(HttpServletRequest request,
            HttpServletResponse response, Object command, BindException errors)
            throws Exception {
        LoginCommand loginCommand = (LoginCommand) command;
        ModelAndView mav = new ModelAndView("loginSuccess");
        mav.addObject("loginCommand", loginCommand);
        return mav;
    }

이 경우 "loginSuccess" 뷰에 해당되는 JSP는 다음과 같이 EL(Expression Language)이나 request.getAttribute() 등의 메소드를 사용하여 ModelAndView 객체에 저장된 값을 사용할 수 있다.

로그인 정보: ${loginCommand.id}
<%
    LoginCommand cmd = (LoginCommand) request.getAttribute("loginCommand");
%>

Velocity 템플릿에서도 다음과 같이 Velocity의 템플릿 언어를 사용하여 ModelAndView 객체에 저장된 값을 사용할 수 있다.

'Spring' 카테고리의 다른 글

Spring 구조 및 DispatcherServlet  (1) 2016.11.24

스프링MVC는 DispatcherServlet 등장으로 정말 엄청나게 web.xml의 역할이 축소되었습니다. 예전같으면 서블릿을 URL로 활용하기 위해선 반드시 web.xml에 등록해야 했지만 이젠 DispatcherServlet이 해당 어플리케이션으로 들어오는 요청을 모두 핸들링해주니 말이죠.

물론 아직까지 web.xml의 역할은 중요합니다. <servlet>으로 DispatcherServlet을 등록해줘야 하는데다 이 객체의 URL 적용범위 또한 web.xml에다 설정해야 하구요. 향후 고급서비스를 위해 <filter>나 <listener>를 등록하는 역할 또한 web.xml의 기능으로 남아 있습니다.

대신 앞으로 web.xml에서 가장 주요하고 자주 쓰이는 기능인 <servlet>매핑은 이제 완벽하게 DispatcherServlet으로 넘어갔다고 생각합시다. 우리가 web.xml에 DispatcherServlet의 <url-pattern>을 '/'로 설정함과 동시에 이제 모든 요청은 DispatcherServlet의 영역이 된 셈입니다. 물론 DispatcherServlet을 web.xml에 등록해도 계속 서블릿을 web.xml에 매핑해쓸 수 있긴 합니다만 우리가 이런 옛방식을 버리고 DispatcherServlet을 이용해 웹개발을 한다면 앞으로 서블릿 파일을 만들 필요도 없어짐과 동시에 획기적이고 놀라운 @MVC의 혜택을 얻을 수 있습니다. @MVC에 대해서는 차후 설명하기로 하고 오늘은 DispatcherServlet의 역할에 대해 설명드리겠습니다.

먼저 DispatcherServlet을 이용한다는 것은 스프링에서 제공하는 @MVC를 이용하겠단 뜻입니다. @MVC는 그동안 추상적으로만알아오고 발전했던 MVC(Model, View, Controller) 설계영역을 아예 노골적으로 분할하여 사용자가 무조건 MVC로 어플리케이션을 설계하게끔 유도하는 방식인데요. (스프링이 전략패턴을 Dependency Injection이란 이름하에 유도하는 것과 마찬가지죠.) 이 말인즉 초보건 구루건 모두 @MVC를 이용해 어플리케이션을 개발한다면 99% MVC 설계의 원칙대로 웹어플리케이션이 제작할 수 있게 된다는 뜻입니다.

우리가 @MVC라는 이름 하에 DispatcherServlet 클래스를 web.xml에 등록하는 순간 스프링이 있기 전 힘겹게 배웠던 모델1, 모델2는 이제 완전한 추억거리가 되고 말 것입니다. @MVC는 설계자체를 모델1 방식으로 할 수 없게 만드는 데다 그동안 구현하기는 까다롭지만 활용성이 높다고 배웠던 모델2 방식을 모델1보다 쉽게 만들 수 있도록 환경을 조성해주기 때문입니다. 게다가 정확히 따지자면 @MVC는 모델 2방식의 설계도 아닙니다. 다만 우리가 @MVC로 코드를 작성하는 방식이 모델 2와 비슷해서 모델2 방식이라고 부르는 것 뿐이죠.

그렇다면 @MVC에서 DispatcherServlet가 담당하는 역할이 무엇인지 알아봅시다. 먼저 DispatcherServlet에 대해 간단히 정의해보자면 우리가 각각 분리하여 만든 Model 파트와 Controller파트 View파트를 조합하여 브라우저로 출력해주는 역할을 수행하는 클래스라 할 수 있겠네요.

간단하게 DispatcherServlet이 어떤 식으로 클라이언트의 요청을 처리하고 응답하는지 UML과 비슷한 방식으로 나타내 보았습니다. 아마 DispatcherServlet을 처음 접해본 분이시라면 모델2보다 복잡한 처리과정에 당황하실 수도 있겠네요. 하지만 위의 그림이 아무리 복잡해도 당황하실 필요는 없습니다. 어디까지나 저 처리과정의 대부분은 컨테이너가 대신 작업해주며 사용자가 직접 구현해야 될 분량은 얼마 되지 않으니까요. 먼저 위의 작업흐름을 풀어 자세히 설명하자면 다음과 같습니다.

① 클라이언트가 해당 어플리케이션에 접근하면 접근한 URL 요청을 DispatcherServlet이 가로챕니다. 이렇게 요청을 가로챌 수 있는 이유는 web.xml에 등록된 DispatcherServlet의 <url-pattern>이 '/'와 같이 해당 어플리케이션의 모든 URL로 등록되있기 때문입니다. 만약 특정 URL만 적용하고 싶다면 <url-pattern>의 내용을 바꿔주어 범위를 변경시키주면 됩니다.

② 가로챈 정보를 HandlerMapping에게 보내 해당 요청을 처리할 수 있는 Controller를 찾아냅니다. (스프링은 기본적으로 5가지의 핸들러 매핑을 제공합니다.) 이 부분은 스프링의 디폴트 전략에 의해 BeanNameUrlHandlerMapping과 DefaultAnnotationHandlerMapping이 기본으로 스프링MVC에 탑재되있기 때문에 특별한 경우가 아니라면 따로 설정할 필요가 없습니다.

③ 핸들러매핑이 해당 요청을 처리할 컨트롤러를 찾아냈다면 요청을 컨트롤러에 보내줍니다. 컨트롤러는 사용자가 직접 구현해주는 부분입니다. @MVC는 매우 다양한 코딩방식과 직관적이고 편리한 컨트롤러 작성방법을 제공하므로 이 부분에 대해서는 차후 심층적인 분석하여 자신에게 알맞는 전략을 선정해야 합니다.

④ 컨트롤러를 해당 요청을 처리한 후에 보통 컨트롤러는 요청을 응답받을 View의 이름을 리턴하게 됩니다. (물론 다른 핸들러 매핑 전략을 이용한다면 응답 과정이 다를 수도 있습니다.) 그 때 이 이름을 ViwResolver가 먼저 받아 해당하는 View가 존재하는지 검색합니다.

⑥ 해당 View가 있다면 처리결과를 View에 보낸 후 ⑦ 이 결과를 다시 DispatcherServier에 보낸 후 ⑧ DispatcherServlet은 최종 결과를 클라이언트에 전송합니다.

매우 복잡한 과정으로 처리되긴 하지만 이런 DispatcherServlet 전략에서 사용자가 직접 구현해야할 부분은 컨트롤러와 뷰 밖에 없습니다. 나머지 핸들러 매핑이나 리졸버는 대략적인 흐름만 알고 있다가 나중에 필요할 때 필요한 클래스를 컨텍스트에 등록시키기만 하면 그만입니다.



여기서 하나 문제가 발생했습니다. 디스패처 서블릿이 모든 요청을 컨트롤러에 넘겨주는 방식은 괜찮은데 모든 요청을 처리하다보니 이미지나 HTML 파일을 불러오는 요청마저 전부 컨트롤러로 넘겨버리는 것입니다. 게다가 JSP 안의 자바스크립트나 스타일시트 파일들도 전부 디스패처 서블릿이 요청을 가로채는 바람에 제대로 자원을 불러올 수도 없는 상황입니다. 도대체 어떻게 해야 할까요?


만약 이런 처리를 해주지 않는다면 위와 같은 에러가 로그에 기록될 것입니다. 디스패처 서블릿이 해당 요청을 처리할 컨트롤러를 찾지 못했다는 에러메시지죠. 이 문제를 해결하기 위한 방법이 몇가지 있습니다. 첫번째 방법은 아래와 같이 디스패처 서블릿이 처리할 URL과 자바와 관련없는 리소스들의 영역을 분리시키는 것입니다.

/apps - 클라이언트가 이 URL로 접근하면 앞으로 디스패처 서블릿이 담당
/resources - 이 URL은 디스패처 서블릿이 컨트롤할 수 없는 영역으로 분리

근데 이런 처리방식은 괜찮긴 하지만 상당히 코드가 지저분 해지는데다 form에서 모든 요청을 보낼 때 apps라는 URL을 붙여줘야 하기 때문에 직관적인 설계가 불가능해진다는 점이 있습니다. 두번째로는 진짜 말그대로 모든 요청을 컨트롤러에 등록하는 방법입니다. 허나 이런 방식은 정말 소규모 어플리케이션이 아니라면 절대로 해서는 안될 방법이겠죠.

스프링은 이런 모든 문제에 해결함과 동시에 굉장히 편리한 해결방법을 고안해 냈습니다. 그건은 바로 <mvc:resources />입니다. 만약 이클립스를 통해 저처럼 프로젝트를 Spring Template Project 생성하셨다면 servlet-context.xml에서 다음과 같은 코드를 발견하실 수 있으실 겁니다.

<resources mapping="/resources/-*" location="/resources/" />

이 전략은 다음과 같습니다. 먼저 디스패처 서블릿을 통해 들어온 요청을 처리하는데 만약 디스패처 서블릿이 해당 요청에 대한 컨트롤러가 찾을 수 없다면 2차적으로 위의 설정된 경로를 검색하여 해당 자원을 찾아내게 되는 것이죠. 저같은 경우는 기존의 resources폴더의 Depoloyment Assembly를 변경하여 아래와 같이 설정하는 것으로 문제를 해결했습니다.


만약 이런 비 어플리케이션 자원을 따로 분리하지 않고 어플리케이션 자원과 한데 섞어 작업하신다면 지금부터라도 이렇게 분리해서 작업하시길 권장해 드립니다. 먼저 이런 영역분리는 효율적인 리소스 관리와 확장을 돕기 때문입니다.

현대의 많은 대형 웹서비스들의 비 어플리케이션 자원 URL을 보시면 철저하게 static 성격의 외부 자원들을 분리시켜 사용하고 있습니다. 네이버같은 경우는 http://static.naver.net/란 URL을 통해 이런 자원들을 분리하였으며 페이스북은 http://static.ak.fbcdn.net/과 같은 URL로 분리시켰습니다. 이런 일례를 들어서라도 차후 확장을 위해 비어플리케이션 자원은 반드시 분리해야할 영역이라는 사실을 알려드리고 싶네요.

여기까지 DispatcherServlet에 대한 모든 설명이 끝났습니다. 다음 포스트부터는 슬슬 스프링 시큐리티에 대해 다뤄보려고 하는데 아직 아는 부분이 그렇게 많지 않은데다가 해결되지 않은 의문점이 다소 있어서 바로 포스팅하기는 조금 힘들 것 같네요 ㅠㅠ 문제가 해결되는 즉시 포스팅 하도록 하겠습니다.

'Spring' 카테고리의 다른 글

Spring DispatcherServlet 심도 있는 내용  (0) 2016.11.24

+ Recent posts