Spring MVC Architecturing
From JCFWiKi
Copyright © 2008 Daewoo Information Systems Co., Ltd. |
|
|
목차 |
[편집] Spring MVC 처리과정
Spring MVC는 클라이언트의 요청을 처리하기 위해 다양한 클래스를 기반으로, 다음과 같은 처리과정을 통해 View를 제공하게 된다.
- 일반적으로 모델 2 개발 방식의 최초 진입지점인 컨트롤러를 담당하는 대상이 서블릿인 것처럼, Spring MVC 또한 클라이언트의 요청이 처음으로 진입되는 지점으로 서블릿인 DispatcherServlet을 정의하였다(①). DispatcherServlet은 Spring MVC에서 가장 핵심적인 기능을 구현하고 있는 클래스로서 클라인언트로부터 발생한 하나의 요청을 처리하기 위하여 필요한 클래스들의 중계를 담당하는 역할을 한다.
- 클라이언트로부터 요청이 들어오면 DispatcherServlet은 빈 설정파일에 정의되어 있는 HandlerMapping을 이용하여 요청 URL에 해당하는 Controller 객체를 얻게 된다(②). 빈 설정파일에 정의되어 있는 Controller는 일반적으로 요청 URL을 기반으로 정의되어 있다. 따라서 HandlerMapping은 요청 URL에 Mapping되어 있는 Controller를 DispatcherServlet에 반환하게 된다.
- DispatcherServlet은 HandlerMapping으로부터 Controller를 얻게되면 요청에 대한 모든 작업을 Controller에게 위임하게 된다(③).
- Controller는 Spring MVC를 구현하기 위하여 개발자들이 직접 구현을 담당하게 되는 부분으로 클라이언트로부터 전달된 인자에 대한 유효성 검증작업, 비즈니스 계층과의 통신, 비즈니스 계층에서 발생한 에러에 대한 처리, 작업 완료후 이동하게 될 뷰화면등에 대한 모든 처리를 담당하게 된다.
- Controller는 비즈니스 계층과의 통신을 완료한 다음 비즈니스 계층에서 전달된 모델 데이터와 클라이언트에게 보여줄 뷰 화면에 대한 정보를 ModelAndView 클래스에 담아서 DispatcherServlet에 반환하게 된다(④).
- DispatcherServlet으로 전달된 ModelAndView 클래스의 View정보는 View 객체(View Object) 또는 논리적인 View 이름(String)을 가지게 된다. ModelAndView 클래스에 저장되어 있는 View정보가 View 객체를 통하여 Controller에서 반환되었다면 DispatcherServlet은 View 객체를 이용하여 클라이언트에 화면을 출력하게 된다(⑥).
- 그러나 ModelAndView에 저장되어 있는 View 정보가 논리적인 View 이름일 경우에는 빈 설정파일에 정의되어 있는 ViewResolver 클래스를 이용하여 클라이언트에게 출력할 View 객체를 얻게 된다(⑤).
위에서 처럼 하나의 요청을 처리하기 위하여 관여하고 있는 클래스가 상당히 많다는 것을 알 수 있다. 그러나 요청을 처리하기 위하여 모든 클래스를 개발자들이 직접 구현할 필요는 없다. 하나의 요청이 추가될 때마다 개발자들이 구현할 필요가 있는 클래스는 Controller 클래스 하나뿐이다. 나머지 클래스는 Spring MVC에서 제공하는 클래스를 이용하여 구현하는 것이 가능하다. 물론 모델 데이터를 출력할 뷰 화면(JSP 또는 Velocity등) 또한 개발자들이 직접 구현해야 한다. 위에서 살펴보았던 대로 Spring MVC가 하나의 요청을 처리하기 위하여 실행되는 단계를 보면 그리 복잡하지 않은 것처럼 생각될 수 있다. 그러나 실질적인 Spring MVC의 복잡도는 유연성을 제공하기 위하여 지원되는 무수히 많은 옵션들 때문이다. Spring MVC는 웹 애플리케이션의 유연성을 지원한다는 명목하에 다양한 종류의 HandlerMapping, Controller, ViewResolver, View 클래스들을 지원하고 있다.
다시 한번 간단히 정리하면 다음과 같다.
1. Resource에 대한 클라이언트의 Request 2. Spring Front Controller가 Request를 가로체서 적당한 Handler Mappings을 찿는다 3. Handler Adapters와 함께 디스패처 서블릿은 Controller에 Request를 디스패치한다. 4. Controller는 클라이언트 Request를 처리하고 FrontController에게 ModelAndView의 폼안에 Model과 View를 리턴한다. 5. Front Controller는 View Resolver 객체에게 문의하여 실제 View를 찿는다. 6. 클라이언트에게 선택된 뷰를 출력한다
[편집] HandlerMapping을 이용하여 URL과 Controller 연결하기
HandlerMapping은 클라이언트가 요청한 URL과 Controller를 매핑하는 역할을 수행한다. 그 동작과정은 클라이언트 Request에 대하여 Dispatcher가 Requet와 Handling 객체맵에서 적당한 Handler Mapping을 찾는다. HanlerMapping은 클라이언트 URL이 Handler에게 매핑되는 방법을 제공한다. Spring MVC는 같은 기능을 지원하기 위하여 여러가지 옵션을 제공하고 있는데, Controller와 URL의 매핑하는 HandlerMapping 또한 여러 가지 방법을 제공하고 있다.
위의 그림은 Spring MVC가 제공하는 HandlerMapping의 다양한 방법을 구현한 클래스의 계층구조를 나타내고 있다. 실제적으로 HandlerMapping을 위해 Spring에서 제공하는 구현체는 다음과 같다.
- BeanNameUrlHandlerMapping
- SimpleUrlHandlerMapping
- CommonsPathMapHandlerMapping
- ControllerBeanNameHandlerMapping
- ControllerClassNameHandlerMapping
- DefaultAnnotationHandlerMapping
[편집] BeanNameUrlHandlerMapping
- BeanNameUrlHandlerMapping은 빈 이름과 URL을 Mapping한다.
- BeanNameUrlHandlerMapping을 이용하여 Mapping하는 방법은 Struts 프레임워크에서 사용하는 방법과 유사하므로 Struts 프레임워크를 사용하여 개발한 경험이 있다면 익숙하게 대응할 수 있다.
- Struts와 다른 점이라면 Spring MVC의 BeanNameUrlHandlerMapping은 매핑을 위한 경로정보로 ANT 빌드툴에서 경로정보를 표현하는 스타일을 사용할 수 있다.
- 예를 들어 ANT 빌드툴에서 별표(*), 물음표(?)와 같은 기호들을 사용할 수 있듯이 BeanNameUrlHandlerMapping에서도 사용이 가능하다.
- <bean name="/*/.html" class="com.daewoobrenic.jcf.web.springmvc.BaseUrlFilenameViewController"/>
- 빈 설정파일에 HandlerMapping이 설정되어 있지 않을 경우 Spring MVC는 BeanNameUrlHandlerMapping을 디폴트 HandlerMapping으로 설정한다.
[편집] SimpleUrlHandlerMapping
- SimpleUrlHandlerMapping은 매핑에 대한 정보를 각각의 Controller에서 설정하는 것이 아니라 하나의 저장소에서 관리한다.
- Controller를 개발하는 개발자들은 빈을 정의하기만 하고 이 Controller가 어떻게 매핑되어서 사용되는지에 대해서는 몰라도 된다.
- SimpleUrlHandlerMapping은 서블릿명-servlet.xml 파일에 다음과 같이 정의한다.
<!--SimpleUrlHandlerMapping을 사용할 경우 방법 #1과 방법 #2가 있다.--> ...... <!--SimpleUrlHandlerMapping 방법 #1--> <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="mappings"> <props> <prop key="**/*.html">staticViewController</prop> ...... <prop key="/user/listUser.do">userController</prop> <prop key="/user/viewUser.do">userController</prop> ...... <prop key="/board/board.do">boardController</prop> </props> </property> </bean> ...... <!--SimpleUrlHandlerMapping 방법 #2--> <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="mappings"> <bean class="org.springframework.beans.factory.config.PropertiesFactoryBean"> <property name="location"> <value>/WEB-INF/UrlMapping.properties</value> </property> </bean> </property> </bean> ......
- SimpleUrlHandlerMapping 방법 #2를 적용한 경우에는 UrlMapping의 프로퍼티를 설정하는 것이 필요하다.
- 위의 xml에서 정의한 UrlMapping.properties에 다음과 같은 mapping 정보를 기술하면 된다.
**/*.html=staticViewController ## user module mapping /user/listUser.do=userController /user/viewUser.do=userController ## board module mapping /board/board.do=boardController
- SimpleUrlHandlerMapping의 장점
- 어플리케이션의 규모가 클 경우 매핑 정보를 수정할 필요가 있을 때 BeanNameUrlHandlerMapping보다 더 효율적으로 관리하는 것이 가능하다.
- 개발자와 통합관리자가 분리된 경우 개발자는 매핑에 대한 문제에 신경쓰지 않고 Controller 클래스만 개발하면 된다.
- 관리하는 입장에서 SimpleUrlHandlerMapping을 사용하면 mapping 정보를 통합하여 관리할 수 있다.
- SimpleUrlHandlerMapping의 단점
- Controller빈을 정의하고 매핑을 별도로 설정하는 불편함이 있다.
- 개발자와 통합관리자가 동일한 경우 개발자는 SimpleUrlHandlerMapping을 사용하는 매핑 방법이 불편할 수도 있다.
BeanNameUrlHandlerMapping과 SimpleUrlHandlerMapping에 대한 사용방법은 현재 진행하고 있는 프로젝트가 어느 부분에 중점을 두고 있느냐에 따라 결정하면 된다.
[편집] CommonsPathMapHandlerMapping
- 드믈게 사용되는 핸들러 매핑이다.
- 컨트롤러의 소스파일에서 직접 URL 매핑을 지정하는 방법이다.
- 다음과 같은 방법으로 설정하면 HandlerMapping을 제공할 수 있다.
/** *@@ org.springframework.web.servlet.handler.commonsattributes. *PathMap("/listUser.jsp") */ public class UserController{ ...... } /** *@@ org.springframework.web.servlet.handler.commonsattributes. *PathMap("/listCode.jsp") */ public class CodeController{ ...... }
- 클래스에서 CommonsAttribute 설정을 활성화하기 위해서 설정파일(서블릿명-servlet.xml)에 다음과 같은 설정이 필요하다.
<beans> ...... <bean id="metaHandlerMapping" class="org.springframework.web.servlet.handler.metadata.CommonsPathMapHandlerMapping"/> ...... </beans>
[편집] ControllerBeanNameHandlerMapping
- BeanNameUrlHandlerMapping와 유사하게 URL과 빈이름을 매핑한다.
- 차이점이 있다면 아래의 소스에서 확인할 수 있듯이 beanName을 기반으로 URL을 구성하는 방식이 다르다는 것이다.
- ControllerBeanNameHandlerMapping는 buildUrlsForHandler 메소드에서 Handler를 위한 mapping을 제공한다.
- generatePathMapping 메소드에서는 beanName과 선택적으로 지정된 urlPrefix와 urlSuffix를 조합하여 PathMap을 생성한다.
public class ControllerBeanNameHandlerMapping extends AbstractControllerUrlHandlerMapping { private String urlPrefix = ""; private String urlSuffix = ""; ...... protected String[] buildUrlsForHandler(String beanName, Class beanClass) { List urls = new ArrayList(); urls.add(generatePathMapping(beanName)); String[] aliases = getApplicationContext().getAliases(beanName); for (int i = 0; i < aliases.length; i++) { urls.add(generatePathMapping(aliases[i])); } return StringUtils.toStringArray(urls); } /** * Prepends a '/' if required and appends the URL suffix to the name. */ protected String generatePathMapping(String beanName) { String name = (beanName.startsWith("/") ? beanName : "/" + beanName); StringBuffer path = new StringBuffer(); if (!name.startsWith(this.urlPrefix)) { path.append(this.urlPrefix); } path.append(name); if (!name.endsWith(this.urlSuffix)) { path.append(this.urlSuffix); } return path.toString(); } ...... }
public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping { /** * Checks name and aliases of the given bean for URLs, starting with "/". */ protected String[] determineUrlsForHandler(String beanName) { List urls = new ArrayList(); if (beanName.startsWith("/")) { urls.add(beanName); } String[] aliases = getApplicationContext().getAliases(beanName); for (int i = 0; i < aliases.length; i++) { if (aliases[i].startsWith("/")) { urls.add(aliases[i]); } } return StringUtils.toStringArray(urls); } }
[편집] ControllerClassNameHandlerMapping
- ControllerClassNameHandlerMapping도 마찬가지로 URL을 구성하는 방법이 다르다.
- pathMapping을 구성하는데 해당 클래스의 패키지명(packageName)에서 '.'을 '/'로 변경하여 기본 path를 구성한다.
- 다음에 CONTROLLER_SUFFIX가 클래스 이름에 있는 경우 CONTROLLER_SUFFIX를 제거한 이름만 소문자로 구성하여 기본 path에 붙여 pathMapping을 구성한다.
- 만약 MultiActionController인 경우에는 pathMapping과 그 하위에 있는 모든 요청을 처리하게 된다.
- MultiActionController가 아닌 경우에는 pathMapping에 정의된 모든 Controller 클래스로 요청을 매핑하게 된다.
public class ControllerClassNameHandlerMapping extends AbstractControllerUrlHandlerMapping { /** * Common suffix at the end of controller implementation classes. * Removed when generating the URL path. */ private static final String CONTROLLER_SUFFIX = "Controller"; private boolean caseSensitive = false; private String pathPrefix; private String basePackage; ...... /** * Generate the actual URL paths for the given controller class. * <p>Subclasses may choose to customize the paths that are generated * by overriding this method. * @param beanClass the controller bean class to generate a mapping for * @return the URL path mappings for the given controller */ protected String[] generatePathMappings(Class beanClass) { StringBuffer pathMapping = buildPathPrefix(beanClass); String className = ClassUtils.getShortName(beanClass); String path = (className.endsWith(CONTROLLER_SUFFIX) ? className.substring(0, className.indexOf(CONTROLLER_SUFFIX)) : className); if (path.length() > 0) { if (this.caseSensitive) { pathMapping.append(path.substring(0, 1).toLowerCase()).append(path.substring(1)); } else { pathMapping.append(path.toLowerCase()); } } if (isMultiActionControllerType(beanClass)) { return new String[] {pathMapping.toString(), pathMapping.toString() + "/*"}; } else { return new String[] {pathMapping.toString() + "*"}; } } /** * Build a path prefix for the given controller bean class. * @param beanClass the controller bean class to generate a mapping for * @return the path prefix, potentially including subpackage names as path elements */ private StringBuffer buildPathPrefix(Class beanClass) { StringBuffer pathMapping = new StringBuffer(); if (this.pathPrefix != null) { pathMapping.append(this.pathPrefix); pathMapping.append("/"); } else { pathMapping.append("/"); } if (this.basePackage != null) { String packageName = ClassUtils.getPackageName(beanClass); if (packageName.startsWith(this.basePackage)) { String subPackage = packageName.substring(this.basePackage.length()).replace('.', '/'); pathMapping.append(this.caseSensitive ? subPackage : subPackage.toLowerCase()); pathMapping.append("/"); } } return pathMapping; } }
[편집] DefaultAnnotationHandlerMapping
- Spring MVC에서 지원하는 Annotation인 @RequestMapping에 설정된 URL로 요청이 발행하면 @RequestMapping이 설정된 해당 클래스의 매소드로 매핑된다.
- 아래의 예제에서 설정된 내용은 %WebContextRoot%/deptInfo/findDeptSpecs.action (servlet-mapping pattern을 .action으로 정의함)이 클라이언트에서 요청되었을 때 findDeptSpecs 메소드에서 해당 요청을 처리하게 된다.
@Controller public class DeptController extends BaseController { protected static Log log = LogFactory.getLog(DeptController.class); @Autowired private DeptInfoService deptInfoService; //Autowired Component Service /** * @param searchMap * @return */ @RequestMapping("/deptInfo/findDeptSpecs") public ModelAndView findDeptSpecs(@ModelAttribute("searchMap") HashMap searchMap) { return new ModelAndView("jsonView", "deptSpecs", deptInfoService.viewDeptSpecs(super.searchMap)); } /** * @param searchList * @return */ @RequestMapping("/deptInfo/saveDeptSpecs") public ModelAndView saveDeptSpecs(@ModelAttribute("searchList") List searchList) { deptInfoService.saveDeptSpec(super.searchList); return new ModelAndView("jsonView"); } }
[편집] 하나의 이상의 HandlerMapping을 사용하기
- Spring MVC 기반의 웹 어플리케이션을 개발하다보면, HandlerMapping을 BeanNameUrlHandlerMapping와 SimpleUrlHandlerMapping 모두 사용해야하는 경우가 발생할 수 있다.
- Spring MVC는 HandlerMapping과 같이 같은 기능을 구현하기 위한 다양한 옵션을 제공하는 기능들이 많다. 그런데 이 같은 옵션들을 하나의 ApplicationContext내에서 같이 사용하고자 할 경우 각 옵션에 우선 순위를 결정하는 것이 가능하다.
- Spring 프레임워크는 org.springframework.core.Ordered 인터페이스를 구현함으로서 이 같은 기능이 가능하도록 지원하고 있다.
- BeanNameUrlHandlerMapping와 SimpleUrlHandlerMapping을 같이 사용하도록 빈 설정파일을 변경하면 다음과 같다.
...... <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"> <property name="order"><value>1</value></property> <!-- Order를 1로 지정함 --> </bean> <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="order"><value>2</value></property> <!-- Order를 2로 지정함 --> <property name="mappings"> <props> <prop key="/board/board.do">boardController</prop> </props> </property> </bean> ......
- 하나의 빈 설정파일에서 BeanNameUrlHandlerMapping와 SimpleUrlHandlerMapping을 모두 사용하고 있는 것을 확인할 수 있다.
- 실무 프로젝트를 진행할 때 위에서 처럼 두가지의 HandlerMapping을 사용하는 경우는 많지 않을 것이다.
- 대부분의 프로젝트들이 프로젝트 초반에 어느 HandlerMapping을 사용할지 결정한 다음 프로젝트를 진행할 것이다.
[편집] HandlerAdapter 이해하기
- Spring MVC에서 DispatcherServlet이 적당한 HandlerMapping을 찾아 Request를 처리하는 방법은 상당히 동적이다.
- 이와 같은 동적인 행위가 HandlerAdapter의 폼 내에서 이루어 진다.
- 다음 그림은 HandlerAdapter 인터페이스의 구현한 클래스를 나타낸다.
- 처음 언급한 Request 처리방법에서 DispatcherServlet은 적당한 Handler Mapping을 선택하고, Request를 설정파일의 컨트롤러에게 넘긴다.
- 이것은 기본적인 경우로 org.springframework.web.servlet.SimpleControllerHandlerAdapter로 정의된 Default HandlerAdapter에서 처리한다.
- 다음은 Spring MVC의 Default HandlerAdapter인 SimpleControllerHandlerAdapter의 소스코드이다.
public class SimpleControllerHandlerAdapter implements HandlerAdapter { public boolean supports(Object handler) { return (handler instanceof Controller); } public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return ((Controller) handler).handleRequest(request, response); } public long getLastModified(HttpServletRequest request, Object handler) { if (handler instanceof LastModified) { return ((LastModified) handler).getLastModified(request); } return -1L; } }
- 다른 핸들러 어뎁터 타입으로 Throwaway Controler HandlerAdapter, 그리고 SimpleServletController handlerAdapter가 있다.
- ThrowawayControllerHandlerAdapter는 DispatcherServlet으로부터 ThrowawayController에게 Request를 전달한다.
- SimpleServletHandlerAdapter는 Request를 DispatherServlet으로부터 Servlet.service() 메쏘드에 의해 만들어지는 Servlet에게 전달한다.
- 설정파일에 다음과 같이 용도에 따라 설정할 수 있다.
...... <bean id="throwawayHandler" class = "org.springframework.web.servlet.mvc.throwaway.ThrowawayControllerHandlerAdapter"/> <!--또는--> <bean id="throwawayHandler" class="org.springframework.web.servlet.mvc.throwaway.SimpleServletHandlerAdapter"/> ......
[편집] HandlerInterceptor 이해하기
- Spring MVC는 서블릿 2.3부터 지원하고 있는 ServletFilter와 같은 기능을 지원하기 위하여 HandlerInterceptor를 지원한다.
- HandlerInterceptor는 Controller가 실행되기 전, 후에 공통적으로 추가해야하는 작업이 있다면 유용하게 사용할 수 있다.
- 서블릿 2.3을 지원하지 않는 환경하에서 ServletFilter와 같은 효과를 필요로할 경우 유용하게 사용할 수 있다.
- HandlerInterceptor의 내부는 다음과 같다.
package org.springframework.web.servlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public interface HandlerInterceptor { boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception; void afterCompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception; }
- Controller 실행 전에 수행될 동작은 preHandle에 정의하고, 실행 후에 수행될 동작은 postHandle에 정의한다.
[편집] HandlerInterceptorAdapter 클래스로 HandlerIntercetor 구현하기
- HandlerInterceptorAdapter 클래스는 HandlerIntercetor을 구현하고 있는 클래스로 Adaptor 패턴이 적용된 클래스이다.
- HandlerIntercetor의 모든 메소드를 구현할 필요가 없는 경우에는 HandlerInterceptorAdapter 클래스를 이용하면 쉽게 구현하는 것이 가능하다.
- 다음 예제는 HandlerIntercetor을 추가하여 Controller가 실행되기 전후에 Logging 메세지를 출력하도록 구현한다.
package com.daewoobrenic.jcf.web; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; public class LoggingHandlerInterceptor extends HandlerInterceptorAdapter { protected final Log logger = LogFactory.getLog(getClass()); public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) throws Exception { if( logger.isDebugEnabled() ) { logger.debug("afterCompletion method called"); } } public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object obj) throws Exception { if( logger.isDebugEnabled() ) { logger.debug("preHandle method called"); } return super.preHandle(req, res, obj); } }
- 앞의 예제와 같이 HandlerIntercetor를 구현한 다음 빈 설정파일에 다음과 같이 정의하면 모든 Controller가 실행되기 전후에 공통적으로 필요한 작업을 수행한다.
<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="interceptors"> <list> <ref bean="loggingInterceptor"/> </list> </property> <property name="mappings"> <props> <!-- static view mapping --> <prop key="**/*.html">staticViewController</prop> <prop key="/index.do">indexController</prop> <prop key="/menu.do">leftMenuController</prop> <prop key="/changelocale.do">sessionLocaleController</prop> <prop key="/user/login.do">loginFormController</prop> <!-- user module mapping --> <prop key="/user/listUser.do">userController</prop> <prop key="/user/viewUser.do">userController</prop> <prop key="/user/editUser.do">userFormController</prop> <!-- board module mapping --> <prop key="/board/board.do">boardController</prop> <prop key="/board/editBoard.do">boardFormController</prop> </props> </property> </bean> <bean id="loggingInterceptor" class="com.daewoobrenic.jcf.web.LoggingHandlerInterceptor"/>
[편집] 다양한 형태의 Controller 구성하기
- Spring MVC는 용도에 따라 다양한 형태의 Controller를 제공한다.
- 전형적인 Controller 유형으로 ViewContoller, CommandController(FormContoller), MulitActionController, ParameterizableViewController, ServletForwardingController, ServletWrappingController를 제공한다.
- 다음은 Controller Interface가 제공하는 다양한 Abstract 클래스와 Implementation 클래스의 계측구조이다.
[편집] AbstractController
- Custom Controller 컴포넌트를 만들려고 할 경우, Controller 인터페이스를 구현하지 말고 AbstractController를 상속한다.
- 기본적으로 GET과 Post를 지원한다.
public class JcfSimpleController extends AbstractController{ public void handleRequestInternal(HttpServletRequest request, HttpServletResponse response){ return new ModelAndView("jcfView"); } }
- DispatcherServlet은 Request와 Response를 파라미터로 하여 handleRequest()를 호출한다
- Implementation 클래스는 ModelAndView 객체를 리턴한다.
- Logical View Name과 실제 Physical Location 뷰 리소스 사이의 매핑을 제공하는 일을 하는 ViewResolver라는 컴포넌트가 있다.
- 예제에서 DispatcherServlet이 JcfSimpleController 객체를 invoke할 경우, View의 형태에 따라 JstlView인 경우는 jcfView.jsp로, JsonView인 경우는 jcfView.js를 통해 jcfView.jsp로 클라이언트에 출력될 것이다.
[편집] AbstractCommandController
- CommandController는 비즈니스 로직이 사용자에 의해 제공되는 값(모델)에 의해 종속될 때 적용하는 것이 적절하다.
- Spring MVC에서는 데이터 바인딩 기능뿐만 아니라 유효성 체크기능까지 입력폼으로 모든 기능을 지원하기 위하여 AbstractCommandController 클래스를 지원한다.
- AbstractCommandController를 기반으로 구현된 Controller는 데이터 바인딩 뿐만 아니라 유효성 체크까지 지원한다.
- 데이터 바인딩 및 데이터에 대한 유효성 체크 기능을 지원하고 있지만 이 Controller를 이용할 경우 입력 화면이 추가될 때마다 Controller가 하나씩 추가되어야 하며, 빈 설정 정보 또한 수정해 주어야 한다는 단점이 있다.
- 다음의 예제에서 사용자 이름의 존재유무에 따라 success.jsp와 failure.jsp가 출력된다.
public class JcfSimpleController extends AbstractCommandController{ public JcfSimpleController(){ setCommandClass(UserInfo.class); } public void handle(HttpServletRequest request, HttpServletResponse response, Object command){ UserInfo userInfo = (UserInfo)command; if ( exists(userInfo.getUserName){ return new ModelAndView("success"); }else{ return new ModelAndView("failure"); } } private boolean exits(String username){ // Some logic here. } }
- 클라이언트 파라미터는 UserInfo라는 클래스로 캡슐화되어 있고 username 필드는 UserInfo의 username 속성에 매핑된다.
- JcfSimpleController의 생성자에서 지정한 CommandClass인 UserInfo는 다음과 같다.
- CommandClass는 생성자에서 지정할 수 있고 서블릿명-servlet.xml 설정파일에서 Controller의 property로 설정할 수 있다.
public class UserInfo{ private String username; // Getters and Setters here. }
[편집] UrlFilenameViewController
- 웹 애플리케이션을 개발하다보면 샘플 애플리케이션의 메인 페이지처럼 정적인 페이지에 접근해야 되는 경우가 종종 발생한다.
- 웹 애플리케이션의 모든 URL에 대하여 공통적인 작업을 처리할 필요가 있을 경우에는 정적인 페이지 또한 DispatcherServlet을 통하여 접근한다.*
- 정적인 페이지의 경우 비즈니스 계층과 통신하는 부분도 없을 뿐 아니라 단지 HTML이나 JSP화면을 출력하는 역할만을 담당하고 있다.
- Spring MVC의 특성상 정적인 페이지에 접근하기 위하여 매번 새로운 Controller를 추가하는 것은 불필요한 작업이 아닐 수 없다.
- 이 같은 단점을 보완하기 위하여 Spring MVC는 UrlFilenameViewController를 지원한다.
- UrlFilenameViewController의 단점은 모든 URL을 다음과 같이 처리한다는 것이다.
http://localhost:8080/jcf/index.html => /jcf/index.jsp http://localhost:8080/jcf/user/index.html => /jcf/index.jsp http://localhost:8080/jcf/board/index.html => /jcf/index.jsp
- 위와 같이 모든 서브 디렉토리에 대한 URL을 하나로 인식하여 처리한다는 것이 단점이다.
- 정적인 페이지는 특정 디렉토리에만 존재하는 것이 아니라 JSP를 관리하는 모든 디렉토리에 존재할 수 있기 때문에 UrlFilenameViewController를 모든 서브 디렉토리에 대해서도 처리하는 것이 가능하도록 다음과 같이 구현할 수 있다.
.... 중간 생략 .... public class JcfUrlFilenameViewController implements Controller { protected final Log logger = LogFactory.getLog(getClass()); public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) { String contextPath = request.getContextPath(); String uri = request.getRequestURI(); if( logger.isDebugEnabled() ) { logger.debug("URI : " + uri); logger.debug("ContextPath : " + contextPath); } int begin = 0; if ((contextPath == null) || (contextPath.equals(""))) { begin = 1; } else { begin = contextPath.length() + 1; } if( logger.isDebugEnabled() ) { logger.debug("Begin : " + begin); } int end; if (uri.indexOf(";") != -1) { end = uri.indexOf(";"); } else if (uri.indexOf("?") != -1) { end = uri.indexOf("?"); } else { end = uri.length(); } String fileName = uri.substring(begin, end); if (fileName.indexOf(".") != -1) { fileName = fileName.substring(0, fileName.lastIndexOf(".")); } for (Enumeration en = request.getParameterNames(); en.hasMoreElements();) { String attribute = (String) en.nextElement(); Object attributeValue = request.getParameter(attribute); if( logger.isDebugEnabled() ) { logger.debug("set Attribute in Request : " + attribute + "=" + attributeValue); } request.setAttribute(attribute, attributeValue); } return new ModelAndView(fileName); } }
- 정적인 페이지에 접근할 때 사용할 확장자를 web.xml에 다음과 같이 추가한다.
<servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.html</url-pattern> </servlet-mapping>
- web.xml에 servlet-mapping을 추가했으므로 확장자가 ".html"에 해당하는 모든 URL은 DispatcherServlet이 처리하게 된다.
- 구현된 JcfUrlFilenameViewController를 서블릿명-servlet.xml 설정파일에 다음과 같이 설정한다.
<beans> <bean name="/**/*.html" class="com.daewoobrenic.jcf.web.JcfUrlFilenameViewController"/> <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass"> <value>org.springframework.web.servlet.view.JstlView</value> </property> <property name="cache" value="false" /> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> </beans>
- MyUrlFilenameViewController에서 사용한 빈 이름을 보면 별표(asterisk, *)를 사용하여 확장자가 ".html"에 해당하는 모든 URL에 대하여 MyUrlFilenameViewController을 매핑(Mapping)하도록 설정하고 있다.
- Spring MVC에서는 URL과 Controller를 매핑할 때 별표(*)를 사용하는 것이 가능하다.
[편집] MultiActionController
- Spring MVC로 웹 애플리케이션을 개발하면 가장 큰 제약사항으로 하나의 페이지가 추가될 때마다 새로운 Controller를 하나씩 추가해야 하는 번거로움이 발생한다
- 이와 같은 번거로움 뿐만아니라 페이지 수가 많아지면 많아질수록 관리해야될 Controller 클래스의 수도 같이 증가하게 되므로 빈 설정파일 또한 상당히 복잡해지기 때문에 Controller의 이 같은 문제점을 해결하기 위하여 Spring MVC는 MultiActionController를 제공한다.
- MultiActionController는 하나의 Controller를 구현함으로서 다수의 요청을 처리하는 것이 가능하도록 지원한다.
- Spring MVC에서는 입력 화면(또는 수정화면)에서 전달되는 입력 데이터를 비즈니스 계층에 전달하기 위하여 HttpServletRequest에 담긴 인자와 도메인 모델의 속성을 자동적으로 연결하는 데이터 바인딩을 지원하고 있다.
- MultiActionController를 이용하여 데이터바인딩을 처리하는 방법은 다음과 같다.
public class UserController extends MultiActionController { protected static final Log logger = LogFactory.getLog(UserController.class); private UserService userService = null; public void setUserService(UserService userService) { this.userService = userService; } .... 중간 생략 .... public ModelAndView add(HttpServletRequest request, HttpServletResponse response) throws Exception { User command = new User(); bind(request, command); userService.addUser(command); return dispatchView(request, response); } public ModelAndView update(HttpServletRequest request, HttpServletResponse response) throws Exception { User command = new User(); bind(request, command); userService.updateUser(command); return dispatchView(request, response); } private ModelAndView dispatchView(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpSession session = request.getSession(); if (session.getAttribute("loginUser") != null) { User loginUser = (User) session.getAttribute("loginUser"); if (loginUser.isAdmin()) { return list(request, response); } } return new ModelAndView("redirect:/index.html"); } }
- MultiActionController 클래스의 bind() 메써드를 이용하여 HttpServletRequest에 전달된 인자를 User 도메인 모델에 전달하고 있는 것을 확인할 수 있다.
- 클라이언트가 입력한 데이터를 도메인 모델과 데이터 바인딩하기 위해서는 입력 화면에서 사용한 속성이름과 도메인 모델의 속성 이름이 같아야 한다.
.... 중간 생략 ....
<table border="0" cellpadding="0" cellspacing="1" width="100%"
bgcolor="BBBBBB">
<tr>
<td width=150 align=center bgcolor="E6ECDE" height="22">사용자 아이디</td>
<td bgcolor="ffffff" style="padding-left:10">
<input type="text" style="width:150" name="userId"/>
</td>
</tr>
<tr>
<td width=150 align=center bgcolor="E6ECDE" height="22">비밀번호</td>
<td bgcolor="ffffff" style="padding-left:10">
<input type="password" style="width:150" name="password"/>
</td>
</tr>
<tr>
<td width=150 align=center bgcolor="E6ECDE" height="22">비밀번호 확인</td>
<td bgcolor="ffffff" style="padding-left:10">
<input type="password" style="width:150" name="password2"/>
</td>
</tr>
<tr>
<td width=150 align=center bgcolor="E6ECDE" height="22">이름</td>
<td bgcolor="ffffff" style="padding-left:10">
<input type="text" style="width:240" name="name"/>
</td>
</tr>
<tr>
<td width=150 align=center bgcolor="E6ECDE" height="22">이메일 주소</td>
<td bgcolor="ffffff" style="padding-left:10">
<input type="text" style="width:240" name="email"/>
</td>
</tr>
<c:if test="${ loginUser.admin }">
<tr>
<td width=150 align=center bgcolor="E6ECDE" height="22">Admin 유무</td>
<td bgcolor="ffffff" style="padding-left:10">
<input type="checkbox" name="admin" value="true" />
</td>
</tr>
</c:if>
</table>
.... 중간 생략 ....
.... 중간 생략 .... public class User extends BaseObject { private String userId = null; private String password = null; private String password2 = null; private String name = null; private String email = null; private boolean admin = false; .... 각 속성에 대한 setter, getter 메써드 .... }
- 하나의 Controller에 사용자 관리 프로젝트의 모든 기능을 구현하도록 개발하는 것은 Controller 개발을 용이하게 할 뿐만 아니라 빈 설정파일이 단순해지기 때문에 개발 및 관리에 많은 장점이 된다.
- 하지만 Spring MVC의 MultiActionController는 입력 데이터에 대한 유효성 체크 기능을 지원하지 않는다.
- Spring MVC에서 지원하는 유효성 체크기능을 사용하지 않고 데이터 바인딩 기능만을 이용하고자할 때는 MultiActionController를 이용하여 구현한다.
[편집] SimpleFormController
- 일반적으로 폼에 데이터를 넣고 submit 하는 목적으로 사용하는데 적합하다.
- 클라이언트가 empInfo.jsp라는 empName, empAge, empSalary 필드가 있는 페이지를 요청한다고 가정하고 요청의 처리가 성공적으로 마치면 empSuccess.jsp이 클라이언트에게 출력된다.
- 이것을 SimpleFormController로 다음과 같이 구현할 수 있다.
- 먼저 Getter와 Setter를 이용하여 다음 내용을 정의하자.
public class EmpInfo{ private String empName; private int empAge; private double empSalary; // Getters and setters for the above properties. }
- 다음은 SimpleFormController를 확장하여 클래스를 작성한다.
- 여기서 doSubmitAction은 override된 것으로 폼이 submit 될때 호출된다.
public class EmpFormController extends SimpleFormController{ public EmpFormController(){ setCommandClass(EmpInfo.class); } public void doSubmitAction(Object command){ EmpInfo info = (EmpInfo)command; process(info); } private void process(EmpInfo info){ //Do some processing with this object. } }
- 설정파일에서 submission이 성공했을 경우에 대한 처리를 다음과 같이 정의한다.
...... <bean id = "empForm" class="EmpFormController"> <property name="formView"><value>empInfo</value></property> <property name="successView"><value>empSuccess</value></property> </bean> ......
- SimpleFormController는 접근하는 URL이 GET/POST에 따라서 서로 다른 방식으로 실행된다.
- SimpleFormController가 GET/POST에 따라 처리하는 과정을 살펴보면 다음 그림과 같다.
- 위의 그림에서 보는 바와 같이 GET 방식으로 접근할 때 여러 단계를 거친 다음에 최종 화면을 출력한다.
- GET 방식으로 SimpleFormController에 접근할 때의 과정을 살펴보면 다음과 같다.
1. 처음으로 호출되는 메써드는formBackingObject() 메써드이다. formBackingObject() 메써드는 디폴트로 설정한 Command 클래스의 인스턴스를 생성한다. 만약 수정화면과 같이 데이터베이스로부터 데이터를 추출해야 하는 경우 이 메써드를 오버라이드(Override)해야한다. 2. Request 패러미터로 전달되는 모든 값은 String이다. 그러나 바인딩할 Command 클래스의 속성으로는 모든 타입을 가질 수 있다. 심지어 String을 Object 타입으로 바인딩해야 하는 경우도 있다. 이와 같이 Request 패러미터를 Command 클래스의 속성과 바인딩시키기 위하여 생성한 Custom Property Editor가 있다면 initBinder() 메써드에서 등록할 수 있다. 3. 빈 설정파일에서 bindOnNewForm 속성이 true로 설정되어 있다면 Request 패러미터와 Command 클래스의 데이터바인딩을 실행한다. 4. 빈 설정파일에서 sessionForm 속성이 true로 설정되어 있다면 Command 클래스를 세션에 저장한다. 5. referenceData() 메써드는 폼 화면을 구현하기 위하여 Command 클래스 이외에 다른 데이터가 필요할 때 오버라이드하여 새롭게 구현하는 것이 가능하다. 6. showForm() 메써드는 디폴트로 빈 설정파일에서 정의한 "formView" 속성에 해당하는 ModelAndView 객체를 반환한다. 만약 요청인자, 상태에 따라 다른 페이지로 이동하고자 한다면 이 메소드를 오버라이드하여 새롭게 구현하는 것이 가능하다.
- Spring MVC에서는 GET 요청에 대한 워크플로우상에서 개발자들이 추가적인 기능을 구현할 수 있도록 지원하고 있다.
- POST 방식으로 접근할 때가 GET 방식으로 접근할 때보다 상당히 복잡한 워크플로우를 가지고 있다.
- 그 이유는 POST 방식으로 폼 화면의 데이터를 전송하고 있기 때문에 데이터 바인딩, 데이터에 대한 유효성 체크 기능이 추가적으로 구현되어야 하기 때문이다.
- 다음으로 POST 방식으로 SimpleFormController에 접근할 때의 과정을 살펴보면 다음과 같다.
1. POST 방식으로 접근할 때의 첫 단계는 빈 설정파일에 sessionForm 속성이 true로 설정되어 있는지에 따라 나뉜다. sessionForm 속성이 true로 설정되어 있다면 세션에 저장되어 있는 Command 클래스를 반환하게 된다. 만약 Command 클래스가 세션에 존재하지 않는다면 handleInvalidSubmit() 메써드가 호출되면서 에러 페이지로 이동하게 된다. 2. sessionForm속성이 설정되어 있지 않다면(디폴트 false) GET 방식으로 접근할 때와 같이 formBackingObject(), initBinder() 메써드를 실행한다. 3. Request 패러미터와 Command 클래스의 데이터 바인딩을 진행한다. 4. 데이터 바인딩 후에 추가적인 구현이 필요하다면 onBind() 콜백 메써드를 오버라이드하여 구현하면 된다. 5. 빈 설정파일에 추가된 Validator가 존재한다면 Validator의 validate() 메써드를 호출함으로서 유효성 체크를 진행한다. 6. 유효성 체크까지 완료한 후 추가적인 구현이 필요하다면 onBindAndValidate() 콜백 메써드를 오버라이드하여 구현하면 된다. 7. BindException의 에러 리스트에 에러가 존재할 경우 showForm() 메써드를 호출하여 빈 설정파일의 formView 속성으로 정의되어 있는 페이지로 이동하게 된다. 8. BindException 에러 없이 정상적으로 처리될 경우 onSubmit() 또는 doSubmitAction() 메써드가 호출된다. onSubmit() 또는 doSubmitAction() 메써드의 디폴트 ModelAndView는 빈 설정파일의 successView 속성으로 정의되어 있는 페이지로 이동한다. 만약 Request 패러미터로 전달되는 값에 따라 다른 페이지로 이동해야한다면 오버라이드함으로서 구현하는 것이 가능하다.
- SimpleFormController는 개발자들에게 유연성을 제공하기 위하여 각 단계마다 콜백 메소드를 제공한다.
- 개발자들은 SimpleFormController를 상속하는 하위 클래스에서 이 콜백 메소드를 오버라이딩(Overriding)함으로서 개발자들이 원하는 기능을 추가적으로 구현할 수 있다.
- SimpleFormController의 데이터 바인딩이나 유효성체크 등에 특별히 추가적으로 구현할 기능이 없다면 formBackingObject(),onSubmit() 메써드만을 오버라이드함으로서 쉽게 구현할 수 있다.
[편집] CancellableFormController
- CancellableFormController는 Form이 Cancel되었을 때 Cancel에 대한 처리를 제공한다.
- SimpleFormController에서 Cancel이 처리될 경우 대한 것만 추가된 것이다.
public class JcfCompleteFormController extends CancellableFormController{ public ModelAndView onCancel(){ return new ModelAndView("cancelView"); } }
[편집] ModelAndView 이해하기
- ModelAndView는 컨트롤러에 의해 DispatcherServlet에 반환된다.
- 이 클래스는 Model과 View 정보를 갖고 있는 컨테이너 클래스이다.
- View 기법은 org.springframework.web.servlet.View에 정의되어 있다.
- 예를 들어 excel, Jasper Reports, pdf, xslt, Free Marker, Html, Tiles, velocity 등을 지원한다.
- Model과 View 객체를 다음과 같이 구현할 수 있다.
View pdfView = …; Map modelData = new HashMap(); ModelAndView mv = new ModelAndView(pdfView, modelData);
- 다음 예제코드를 보자.
ModelAndView mv = new ModelAndView("jcfView", someData);
- 위 예제에서 someData가 jcfView에 전달된다. 여기서 view는 논리뷰이다.
- jcfView는 jcfView.jsp 또는 jcfView.pdf, jcfView.xml에 대응될 수 있다.
- 논리뷰에 대응되는 물리뷰의 위치는 설정파일에서 정의된다.
[편집] ViewResolver 이해하여 적용하기
- ViewResolver는 논리뷰와 물리뷰를 매핑한다.
- Spring MVC에서 지원하는 ViewResolver는 다음과 같다.
1. BeanNameViewResolver 2. FreeMarkerViewResolver 3. InternalResourceViewResolver 4. JasperReportsViewResolver 5. ResourceBundleViewResolver 6. UrlBasedViewResolver 7. VelocityLayoutViewResolver 8. VelocityViewResolver 9. XmlViewResolver 10. XsltViewResolver
- ViewResolver 구현 시 계층구조를 참고하여 용도에 맞는 ViewResolver를 선택해야 한다.
- 다음은 ViewResolver Interface의 계층구조를 나타낸다.
[편집] InternalResourceViewResolver
- InternalResourceViewResolver는 ModelAndView 폼에서 컨트롤러에 의해 리턴된 리소스의 논리적 이름을 Physical View Location에 매핑하려고 시도한다.
- 단점은 뷰파일의 이름이 어플리케이션 컨텍스트 내에 명시되어야 한다는 것이다.
- 따라서 동적으로 생성되는 뷰파일에 대한 처리는 불가능하다.
- 다음 예제코드와 같이 구성된 Controller에서는 조건에 따라 서로 다른 ModelAndView 객체를 리턴한다.
public class JcfController { public void handle(){ if(condition1()){ return new ModelAndView("jcfView1"); }else if (condition2()){ return new ModelAndView("jcfView2"); } return new ModelAndView("jcfView3"); } }
- 만약 클라이언트의 Request가 condition1()을 만족한다면, View는 jcfView1.jsp 이고 condition2() 일 경우에는 jcfView2이다.
- 이와 같은 동작이 정상적으로 이루어지기 위해서는 반드시 다음 설정파일 내용이 필요하다.
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix"><value>/WEB-INF/</value></property> <property name="suffix"><value>.jsp</value></property> </bean>
- 이것은 InternalResolverViewResolver가 논리뷰이름을 Physical Location에 매핑하는 방법을 보여준다.
- 논리뷰이름이 jcfView1일 때, View 이름은 prefix + 논리뷰이름 + suffix이며, 여기서는 /WEB-INF/jcfView.jsp가 된다.
[편집] BeanNameViewResolver
- InternalResourceViewResolver의 단점인 동적 뷰파일 처리의 문제점을 해결하기 위한 ViewResolver이다.
- BeanNameViewResolver는 Pdf 또는 엑셀 등의 View를 동적으로 생성할 수 있다.
...... return ModelAndView("pdf"); ......
- 위와 같이 pdf를 생성해야 한는 ModelAndView인 경우 다음과 같은 설정파일을 정의한다.
<bean id="beanNameResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver"/> <bean id="pdf" class="com.daewoobrenic.jcf.web.servlet.view.JcfPdfGenerator"/>
- 이때 JcfPdfGenerator는 반드시 org.springframework.web.servlet.view.document.AbstractPdfView의 서브클래스이어야 한다.
[편집] View 구현하기
- AbstractView 클래스를 상속받고 Abstract 메소드인 renderMergedOutputModel 메소드를 오버라이딩하여 클라이언트에게 제공하고자 하는 물리적인 뷰를 생성할 수 있도록 구현한다.
- View에서는 생성자나 XML 설정파일을 통해 View 클래스에 대한 초기값을 설정할 수 있다.
- Spring MVC에서 제공하는 View는 다음과 같다.
