Annotation-based Controller

개요

스프링 프레임워크는 2.5 버젼 부터 Java 5+ 이상이면 @Controller(Annotation-based Controller)를 개발할 수 있는 환경을 제공한다.
인터페이스 Controller를 구현한 SimpleFormController, MultiActionController 같은 기존의 계층형(Hierarchy) Controller와의 주요 차이점 및 개선점은 아래와 같다.

  1. 어노테이션을 이용한 설정 : XML 기반으로 설정하던 정보들을 어노테이션을 사용해서 정의한다.
  2. 유연해진 메소드 시그니쳐 : Controller 메소드의 파라미터와 리턴 타입을 좀 더 다양하게 필요에 따라 선택할 수 있다.
  3. POJO-Style의 Controller : Controller 개발시에 특정 인터페이스를 구현 하거나 특정 클래스를 상속해야할 필요가 없다. 하지만, 폼 처리, 다중 액션등 기존의 계층형 Controller가 제공하던 기능들을 여전히 쉽게 구현할 수 있다.

계층형 Controller로 작성된 폼 처리를 @Controller로 구현하는 예도 설명한다.
예제 코드 easycompany의 Controller는 동일한 기능(또한 공통의 Service, DAO, JSP를 사용)을 계층형 Controller와 @Controller로 각각 작성했다.

  • 계층형 Controller - 패키지 com.easycompany.controller.annotation
  • @Controller - 패키지 com.easycompany.controller.hierarchy

설명

어노테이션을 이용한 설정

계층형 Controller들을 사용하면 여러 정보들(요청과 Controller의 매핑 설정 등)을 XML 설정 파일에 명시 해줘야 하는데, 복잡할 뿐 아니라 설정 파일과 코드 사이를 빈번히 이동 해야하는 부담과 번거로움이 될 수 있다.
@MVC는 Controller 코드안에 어노테이션으로 설정함으로써 좀 더 편리하게 MVC 프로그래밍을 할 수 있도록 했다.
@MVC에서 사용하는 주요 어노테이션은 아래와 같다.

이름설명
@Controller해당 클래스가 Controller임을 나타내기 위한 어노테이션
@RequestMapping요청에 대해 어떤 Controller, 어떤 메소드가 처리할지를 맵핑하기 위한 어노테이션
@RequestParamController 메소드의 파라미터와 웹요청 파라미터와 맵핑하기 위한 어노테이션
@ModelAttributeController 메소드의 파라미터나 리턴값을 Model 객체와 바인딩하기 위한 어노테이션
@SessionAttributesModel 객체를 세션에 저장하고 사용하기 위한 어노테이션

@Controller

@MVC에서 Controller를 만들기 위해서는 작성한 클래스에 @Controller를 붙여주면 된다. 특정 클래스를 구현하거나 상속할 필요가 없다.

package com.easycompany.controller.annotation;
 
@Controller
public class LoginController {
   ...
}

앞서 DefaultAnnotationHandlerMapping에서 언급한 대로 <context:component-scan> 태그를 이용해 @Controller들이 있는 패키지를 선언해 주면 된다.
@Controller만 스캔 한다면 include, exclude 등의 필터를 사용하라.

<?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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
 
        <context:component-scan base-package="com.easycompany.controller.annotation" />
 
</beans>

@RequestMapping

@RequestMapping은 요청에 대해 어떤 Controller, 어떤 메소드가 처리할지를 맵핑하기 위한 어노테이션이다. @RequestMapping이 사용하는 속성은 아래와 같다.

이름타입설명
valueString[]URL 값으로 맵핑 조건을 부여한다.
@RequestMapping(value=”/hello.do”) 또는 @RequestMapping(value={”/hello.do”, ”/world.do” })와 같이 표기하며,
기본값이기 때문에 @RequestMapping(”/hello.do”)으로 표기할 수도 있다.
”/myPath/*.do”와 같이 Ant-Style의 패턴매칭을 이용할 수도 있다.
methodRequestMethod[]HTTP Request 메소드값을 맵핑 조건으로 부여한다.
HTTP 요청 메소드값이 일치해야 맵핑이 이루어 지게 한다.
@RequestMapping(method = RequestMethod.POST)같은 형식으로 표기한다.
사용 가능한 메소드는 GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE이다
paramsString[]HTTP Request 파라미터를 맵핑 조건으로 부여한다.
params=“myParam=myValue”이면 HTTP Request URL중에 myParam이라는 파라미터가 있어야 하고 값은 myValue이어야 맵핑한다.
params=“myParam”와 같이 파라미터 이름만으로 조건을 부여할 수도 있고, ”!myParam”하면 myParam이라는 파라미터가 없는 요청 만을 맵핑한다.
@RequestMapping(params={“myParam1=myValue”, “myParam2”, ”!myParam3”})와 같이 조건을 주었다면,
HTTP Request에는 파라미터 myParam1이 myValue값을 가지고 있고, myParam2 파라미터가 있어야 하고, myParam3라는 파라미터는 없어야 한다.

@RequestMapping은 클래스 단위(type level)나 메소드 단위(method level)로 설정할 수 있다.

type level
/hello.do 요청이 오면 HelloController의 hello 메소드가 수행된다.

@Controller
@RequestMapping("/hello.do")
public class HelloController {
 
    @RequestMapping   //type level에서 URL을 정의하고 Controller에 메소드가 하나만 있어도 요청 처리를 담당할 메소드 위에 @RequestMapping 표기를 해야 제대로 맵핑이 된다.
    public String hello(){
	...		
    }
}

method level
/hello.do 요청이 오면 hello 메소드, 
/helloForm.do 요청은 GET 방식이면 helloGet 메소드, POST 방식이면 helloPost 메소드가 수행된다.

@Controller
public class HelloController {	
 
	@RequestMapping(value="/hello.do")
	public String hello(){
		...
	}
 
	@RequestMapping(value="/helloForm.do", method = RequestMethod.GET)
	public String helloGet(){
		...
	}
 
	@RequestMapping(value="/helloForm.do", method = RequestMethod.POST)
	public String helloPost(){
		...
	}	
}

type + method level
둘 다 설정할 수도 있는데, 이 경우엔 type level에 설정한 @RequestMapping의 value(URL)를 method level에서 재정의 할수 없다.
/hello.do 요청시에 GET 방식이면 helloGet 메소드, POST 방식이면 helloPost 메소드가 수행된다.

@Controller
@RequestMapping("/hello.do")
public class HelloController {
 
	@RequestMapping(method = RequestMethod.GET)
	public String helloGet(){
		...
	}
 
	@RequestMapping(method = RequestMethod.POST)
	public String helloPost(){
		...
	}
}

AbstractController 상속받아 구현한 예제 코드 LoginController를 어노테이션 기반의 Controller로 구현해 보겠다. 
기존의 LoginController는 URL /loginProcess.do로 오는 요청의 HTTP 메소드가 POST일때 handleRequestInternal 메소드가 실행되는 Controller였는데, 다음과 같이 구현할 수 있겠다.

package com.easycompany.controller.annotation;
...
@Controller
public class LoginController {
 
	@Autowired
	private LoginService loginService;
 
	@RequestMapping(value = "/loginProcess.do", method = RequestMethod.POST)
	public String login(HttpServletRequest request) {
 
		String id = request.getParameter("id");
		String password = request.getParameter("password");
 
		Account account = (Account) loginService.authenticate(id,password);
 
		if (account != null) {
			request.getSession().setAttribute("UserAccount", account);
			return "redirect:/employeeList.do";
		} else {
			return "login";
		}
	}	
}

위 예제 코드에서 서비스 클래스를 호출하기 위해서 @Autowired가 사용되었는데 자세한 내용은 여기를 참고하라.

@RequestParam

@RequestParam은 Controller 메소드의 파라미터와 웹요청 파라미터와 맵핑하기 위한 어노테이션이다.
관련 속성은 아래와 같다.

이름타입설명
valueString파라미터 이름
requiredboolean해당 파라미터가 반드시 필수 인지 여부. 기본값은 true이다.

아래 코드와 같은 방법으로 사용되는데, 
해당 파라미터가 Request 객체 안에 없을때 그냥 null값을 바인드 하고 싶다면, pageNo 파라미터 처럼 required=false로 명시해야 한다.
name 파라미터는 required가 true이므로, 만일 name 파라미터가 null이면 org.springframework.web.bind.MissingServletRequestParameterException이 발생한다.

@Controller
public class HelloController {
 
    @RequestMapping("/hello.do")
    public String hello(@RequestParam("name") String name, //required 조건이 없으면 기본값은 true, 즉 필수 파라미터 이다. 파라미터 pageNo가 존재하지 않으면 Exception 발생.
			@RequestParam(value="pageNo", required=false) String pageNo){ //파라미터 pageNo가 존재하지 않으면 String pageNo는 null.
	...		
    }
}

위에서 작성한 LoginController의 login 메소드를 보면 파라미터 아이디와 패스워드를 Http Request 객체에서 getParameter 메소드를 이용해 구하는데,
@RequestParam을 사용하면 아래와 같이 변경할수 있다.

package com.easycompany.controller.annotation;
...
@Controller
public class LoginController {
 
	@Autowired
	private LoginService loginService;
 
	@RequestMapping(value = "/loginProcess.do", method = RequestMethod.POST)
	public String login(
			HttpServletRequest request,
			@RequestParam("id") String id,
			@RequestParam("password") String password) {		
 
		Account account = (Account) loginService.authenticate(id,password);
 
		if (account != null) {
			request.getSession().setAttribute("UserAccount", account);
			return "redirect:/employeeList.do";
		} else {
			return "login";
		}
	}
}

@ModelAttribute

@ModelAttribute의 속성은 아래와 같다.

이름타입설명
valueString바인드하려는 Model 속성 이름.

@ModelAttribute는 실제적으로 ModelMap.addAttribute와 같은 기능을 발휘하는데, Controller에서 2가지 방법으로 사용된다.

1.메소드 리턴 데이터와 Model 속성(attribute)의 바인딩. 
메소드에서 비지니스 로직(DB 처리같은)을 처리한 후 결과 데이터를 ModelMap 객체에 저장하는 로직은 일반적으로 자주 발생한다.

...
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
		Department department = departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
		model.addAttribute("department", department); //데이터를 모델 객체에 저장한다.
		return "modifydepartment";
	}
...

@ModelAttribute를 메소드에 선언하면 해당 메소드의 리턴 데이터가 ModelMap 객체에 저장된다.
위 코드를 아래와 같이 변경할수 있는데, 사용자로 부터 GET방식의 /updateDepartment.do 호출이 들어오면,
formBackingObject 메소드가 실행 되기 전에 DefaultAnnotationHandlerMapping이 org.springframework.web.bind.annotation.support.HandlerMethodInvoker을 이용해서 
(@ModelAttribute가 선언된)getEmployeeInfo를 실행하고, 결과를 ModelMap객체에 저장한다.
결과적으로 getEmployeeInfo 메소드는 ModelMap.addAttribute(“department”, departmentService.getDepartmentInfoById(…)) 작업을 하게 되는것이다.

...
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject() {
		return "modifydepartment";
	}
 
	@ModelAttribute("department")
	public Department getEmployeeInfo(@RequestParam("deptid") String deptid){
		return departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
	}
	또는
	public @ModelAttribute("department") Department getDepartmentInfoById(@RequestParam("deptid") String deptid){
		return departmentService.getDepartmentInfoById(deptid);
	}
...

2.메소드 파라미터와 Model 속성(attribute)의 바인딩. 
@ModelAttribute는 ModelMap 객체의 특정 속성(attribute) 메소드의 파라미터와 바인딩 할때도 사용될수 있다.
아래와 같이 메소드의 파라미터에 ”@ModelAttribute(“department”) Department department” 선언하면 department에는 (Department)ModelMap.get(“department”) 값이 바인딩된다.
따라서, 아래와 같은 코드라면 formBackingObject 메소드 파라미터 department에는 getDepartmentInfo 메소드가 ModelMap 객체에 저장한 Department 데이터가 들어 있다.

...
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject(@ModelAttribute("department") Department department) { //department에는 getDepartmentInfo에서 구해온 데이터들이 들어가 있다.
		System.out.println(employee.getEmployeeid());
		System.out.println(employee.getName());
		return "modifydepartment";
	}
 
	@ModelAttribute("department")
	public Department getDepartmentInfo(@RequestParam("deptid") String deptid){
		return departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
	}
...

@SessionAttributes

@SessionAttributes는 model attribute를 session에 저장, 유지할 때 사용하는 어노테이션이다. @SessionAttributes는 클래스 레벨(type level)에서 선언할 수 있다. 관련 속성은 아래와 같다.

이름타입설명
typesClass[]session에 저장하려는 model attribute의 타입
valueString[]session에 저장하려는 model attribute의 이름

유연해진 메소드 시그니쳐

@RequestMapping을 적용한 Controller의 메소드는 아래와 같은 메소드 파라미터와 리턴 타입을 사용할수 있다.
특정 클래스를 확장하거나 인터페이스를 구현해야 하는 제약이 없기 때문에 계층형 Controller 비해 유연한 메소드 시그니쳐를 갖는다.

@Controller의 메소드 파라미터

사용가능한 메소드 파라미터는 아래와 같다.

  • Servlet API - ServletRequest, HttpServletRequest, HttpServletResponse, HttpSession 같은 요청,응답,세션관련 Servlet API들.
  • WebRequest, NativeWebRequest - org.springframework.web.context.request.WebRequest, org.springframework.web.context.request.NativeWebRequest
  • java.util.Locale
  • java.io.InputStream / java.io.Reader
  • java.io.OutputStream / java.io.Writer
  • @RequestParam - HTTP Request의 파라미터와 메소드의 argument를 바인딩하기 위해 사용하는 어노테이션.
  • java.util.Map / org.springframework.ui.Model / org.springframework.ui.ModelMap - 뷰에 전달할 모델데이터들.
  • Command/form 객체 - HTTP Request로 전달된 parameter를 바인딩한 커맨드 객체, @ModelAttribute을 사용하면 alias를 줄수 있다.
  • Errors, BindingResult - org.springframework.validation.Errors / org.springframework.validation.BindingResult 유효성 검사후 결과 데이터를 저장한 객체.
  • SessionStatus - org.springframework.web.bind.support.SessionStatus 세션폼 처리시에 해당 세션을 제거하기 위해 사용된다.

메소드는 임의의 순서대로 파라미터를 사용할수 있다. 단, BindingResult가 메소드의 argument로 사용될 때는 바인딩 할 커맨드 객체가 바로 앞에 와야 한다.

public String updateEmployee(...,@ModelAttribute("employee") Employee employee,			
			BindingResult bindingResult,...) <!-- (O) -->
 
public String updateEmployee(...,BindingResult bindingResult,
                        @ModelAttribute("employee") Employee employee,...) <!-- (X) -->

이 외의 타입을 메소드 파라미터로 사용하려면?
스프링 프레임워크는 위에서 언급한 타입이 아닌 custom arguments도 메소드 파라미터로 사용할 수 있도록 org.springframework.web.bind.support.WebArgumentResolver라는 인터페이스를 제공한다.
WebArgumentResolver를 사용한 예제는 이곳을 참고하라.

@Controller의 메소드 리턴 타입

사용가능한 메소드 리턴 타입은 아래와 같다.

  • ModelAndView - 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 담긴 Model 객체와 View 정보가 담겨 있다.
            @RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public ModelAndView formBackingObject(@RequestParam("deptid") String deptid) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		ModelAndView mav = new ModelAndView("modifydepartment");
    		mav.addObject("department", department);
    		return mav;
    	}
    또는
    	public ModelAndView formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		model.addAttribute("department", department);
    		ModelAndView mav = new ModelAndView("modifydepartment");
    		mav.addAllObjects(model);
    		return mav;
    	}
  • Model(또는 ModelMap) - 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Model 객체에 담겨 있다.
    View 이름은 RequestToViewNameTranslator가 URL을 이용하여 결정한다. 인터페이스 RequestToViewNameTranslator의 구현클래스인 DefaultRequestToViewNameTranslator가 View 이름을 결정하는 방식은 아래와 같다.
    http://localhost:8080/gamecast/display.html -> display
    http://localhost:8080/gamecast/displayShoppingCart.html -> displayShoppingCart
    http://localhost:8080/gamecast/admin/index.html -> admin/index
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public Model formBackingObject(@RequestParam("deptid") String deptid, Model model) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		model.addAttribute("department", department);
    		return model;
    	}
    또는
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public Model formBackingObject(@RequestParam("deptid") String deptid) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		Model model = new ExtendedModelMap();
    		model.addAttribute("department", department);
    		return model;
    	}
  • Map - 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Map 객체에 담겨 있으며, View 이름은 역시 RequestToViewNameTranslator가 결정한다.
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public Map formBackingObject(@RequestParam("deptid") String deptid) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		Map model = new HashMap();
    		model.put("department", department);
    		return model;
    	}
    또는 
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public Map formBackingObject(@RequestParam("deptid") String deptid, Map model) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		model.put("department", department);
    		return model;
    	}
  • String - 리턴하는 String 값이 곧 View 이름이 된다. 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Model(또는 ModelMap)에 담겨 있다. 리턴할 Model(또는 ModelMap)객체가 해당 메소드의 argument에 선언되어 있어야 한다.
            <!--(O)-->
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		model.addAttribute("department", department);
    		return "modifydepartment";
    	}
     
            <!--(X)-->
    	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
    	public String formBackingObject(@RequestParam("deptid") String deptid) {
    		Department department = departmentService.getDepartmentInfoById(deptid);
    		ModelMap model = new ModelMap();
    		model.addAttribute("department", department);
    		return "modifydepartment";
    	}
  • View - View를 리턴한다. 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Model(또는 ModelMap)에 담겨 있다.
  • void - 메소드가 ServletResponse / HttpServletResponse등을 사용해서 직접 응답을 처리하는 경우. View 이름은 RequestToViewNameTranslator가 결정한다.

POJO-Style의 Controller

@MVC는 Controller 개발시에 특정 인터페이스를 구현 하거나 특정 클래스를 상속해야할 필요가 없다.
Controller의 메소드에서 Servlet API를 반드시 참조하지 않아도 되며, 훨씬 유연해진 메소드 시그니쳐로 개발이 가능하다.
여기서는 SimpleFormController의 폼 처리 액션을 @Controller로 구현함으로써, POJO-Style에 가까워졌지만 기존의 계층형 Controller에서 제공하던 기능들을 여전히 구현할 수 있음을 보이고자 한다.

FormController by SimpleFormController -> @Controller

앞서 SimpleFormController을 설명하면서 예제로 작성된 com.easycompany.controller.hierarchy.UpdateDepartmentController를 @ModelAttribute와 @RequestMapping을 이용해서 같은 기능을 @Controller로 작성해 보겠다.
JSP 소스는 동일한 것을 사용한다. 이곳의 예제 화면 이미지 및 JSP 코드를 참고하라.
기존의 UpdateDepartmentController를 보면 3가지 메소드로 이루어졌다.

  • referenceData - 입력폼에 필요한 참조데이터인 상위부서정보를 가져와서 Map 객체에 저장한다. 이후에 이 Map 객체는 스프링 내부 로직에 의해 ModelMap 객체에 저장된다.
  • formBackingObject - GET 방식 호출일때 초기 입력폼에 들어갈 부서 데이터를 리턴한다. 이 데이터 역시 ModelMap 객체에 저장된다.
  • onSubmit - POST 전송시에 호출되며 폼 전송을 처리한다.
package com.easycompany.controller.hierarchy;
...
 
public class UpdateDepartmentController extends SimpleFormController{
 
	private DepartmentService departmentService;
 
	public void setDepartmentService(DepartmentService departmentService){
		this.departmentService = departmentService;
	}
 
	//상위부서리스트(selectbox)는 부서정보클래스에 없으므로 , 상위부서리스트 데이터를 DB에서 구해서 별도의 참조데이터로 구성한다.
	@Override
	protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception{
 
		Map referenceMap = new HashMap();
		referenceMap.put("deptInfoOneDepthCategory",departmentService.getDepartmentIdNameList("1"));	//상위부서정보를 가져와서 Map에 담는다.
		return referenceMap;
	}
 
	@Override
	protected Object formBackingObject(HttpServletRequest request) throws Exception {
		if(!isFormSubmission(request)){	// GET 요청이면
			String deptid = request.getParameter("deptid");
			Department department = departmentService.getDepartmentInfoById(deptid);//부서 아이디로 DB를 조회한 결과가 커맨드 객체 반영.
			return department;
		}else{	// POST 요청이면
			//AbstractFormController의 formBackingObject을 호출하면 요청객체의 파라미터와 설정된 커맨드 객체간에 기본적인 데이터 바인딩이 이루어 진다.
			return super.formBackingObject(request);
		}
	}
 
	@Override
	protected ModelAndView onSubmit(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors) throws Exception{
 
		Department department = (Department) command;
 
		try {
			departmentService.updateDepartment(department);
		} catch (Exception ex) {
			return showForm(request, response, errors);
		}
 
		return new ModelAndView(getSuccessView(), "department", department);
	}
}

@Controller로 작성된 com.easycompany.controller.annotation.UpdateDepartmentController은 3개의 메소드로 이루어져 있다.
계층형 Controller인 기존의 UpdateDepartmentController와는 달리 각 메소드는 Override 할 필요없기 때문에 메소드 이름은 자유롭게 지을 수 있다.
쉬운 비교를 위해 SimpleFormController과 동일한 메소드 이름을 선택했다.

  • referenceData - 입력폼에 필요한 참조데이터인 상위부서정보를 가져와서 ModelMap에 저장한다.(by @ModelAttribute)
  • formBackingObject - GET 방식 호출일때 처리를 담당한다. 초기 입력폼 구성을 위한 부서데이터를 가져와서 ModelMap에 저장한다.
  • onSubmit - POST 전송시에 호출되며 폼 전송을 처리한다.

(POJO에 가까운) 프레임워크 코드들은 감춰졌고, 보다 직관적으로 비지니스 내용을 표현할 수 있게 되었다고 생각한다.

package com.easycompany.controller.annotation;
 
...
@Controller
public class UpdateDepartmentController {
 
	@Autowired
	private DepartmentService departmentService;
 
	//상위부서리스트(selectbox)는 부서정보클래스에 없으므로 , 상위부서리스트 데이터를 DB에서 구해서 별도의 참조데이터로 구성한다.
	@ModelAttribute("deptInfoOneDepthCategory")
	public Map<String, String> referenceData() {
		return departmentService.getDepartmentIdNameList("1");
	}
 
	// 해당 부서번호의 부서정보 데이터를 불러와 입력폼을 채운다
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
		Department department = departmentService.getDepartmentInfoById(deptid);
		model.addAttribute("department", department); //form tag의 commandName은 이 attribute name과 일치해야 한다. <form:form commandName="department">.
		return "modifydepartment";
	}
 
	//사용자가 데이터 수정을 끝내고 저장 버튼을 누르면 수정 데이터로 저장을 담당하는 서비스(DB)를 호출한다.
	//저장이 성공하면 부서리스트 페이지로 이동하고 에러가 있으면 다시 입력폼페이지로 이동한다.
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.POST)
	public String onSubmit(@ModelAttribute("department") Department department, BindingResult bindingResult) {
 
		//validation code
		new DepartmentValidator().validate(department, bindingResult);		
		if(bindingResult.hasErrors()){
			return "modifydepartment";
		}
 
		try {
			departmentService.updateDepartment(department);
			return "redirect:/departmentList.do?depth=1";
		} catch (Exception e) {
			e.printStackTrace();
			return "modifydepartment";
		}
	}
}

참고자료

출처 - http://www.egovframe.org/wiki/doku.php?id=egovframework:rte:ptl:annotation-based_controller&s[]=requestmapping


==================================================================================

19.Annotation based Spring MVC

Spring XML 만을 독립적으로 사용할 경우 때때로 방대하고 복잡한 속성 정의 파일들로 인해 시스템 개발 및 유지보수의 지연을 초래할 가능성이 높아진다. 이러한 문제점을 해결하기 위해 Spring Framework에서는 별도 XML 정의없이도 사용 가능한 annotation 지원에 주력하고 있는 실정이다. SpringMVC기반의 Controller 구현을 위해서 Spring에서 제공하는 annotation의 종류와 그 사용법에 대해서 상세히 살펴보도록 한다. Annotation을 사용하여 SpringMVC기반의 Controller를 작성하면, Controller라는 인터페이스를 상속받거나 그 외 Spring에서 제공하는 Controller 구현체들을 상속받지 않아도 된다. 따라서 Servlet API와는 독립적으로 작성할 수 있다는 장점이 있다. (단, annotation은 JAVA 5 이상에서만 사용가능함에 유의하도록 한다.) 본 문서에서는 annotation에 대한 일반적인 내용보다는, annotation을 사용하여 어떻게 Spring MVC의 각 구성요소들을 구현하는지 알아보도록 한다. Annotation에 대한 보다 자세한 내용은 본 매뉴얼 >> Spring >> Annotation 을 참고하기 바란다.

19.1.Configuration

Annotation을 사용하여 SpringMVC 기반의 웹어플리케이션을 구현하기 위해서는 속성 정의 XML에 추가되어야 할 설정들이 있다.

19.1.1.Handler 설정

@RequestMapping annotation를 처리하는 default 클래스는 다음과 같다.

  • DefaultAnnotationHandlerMapping

  • AnnotationMethodHandlerAdapter

앞에서 BeanNameUrlHandlerMapping이나 SimpleUrlHandleMapping에서 처럼, 위의 DefaultAnnotationHandlerMapping을 사용할 경우에도 Interceptor를 정의할 수 있는데, DefaultAnnotationHandlerMapping에 Interceptor를 정의하면 모든 Request URL이 Interceptor 영향을 받게 되는 불편함이 발생한다. 이 때, 특정 URL에만 Interceptor를 정의하고자 하는 경우에 SelectedAnnotationHandlerMapping 을 사용하여 다음과 같이 설정할 수 있다. common-servlet.xml 파일 예이다.

<bean id="annotationHandlerMapping"
    class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
    <property name="order" value="1" />
    <property name="interceptors" ref="loginInterceptor" />
</bean>

<!-- 특정URL에만 Interceptor를 적용하기 위해 사용
※ 참고 
http://www.scottmurphy.info/spring_framework_annotation_based_controller_interceptors-->
<bean id="selectedAnnotationHandlerMapping"
    class="org.springplugins.web.SelectedAnnotationHandlerMapping">
    <!-- order 값이 작은 것이 우선적으로 적용된다. -->
    <property name="order" value="0" />
    <property name="urls">
        <list>
            <value>/updateCategory.do</value>
        </list>
    </property>
    <property name="interceptors">
        <list>
            <ref bean="authorizationInterceptor" />
        </list>
    </property>
</bean>

19.1.2.Component Scan 설정

@Controller annotation으로 정의된 컨트롤러 클래스를 사용하기 위해서는 <context:component-scan/> 을 속성 정의 XML에 추가해 주어야 한다. <context:component-scan/>에 대한 자세한 내용은 본 매뉴얼 >> Spring >> Annotation 을 참고하기 바란다.

19.1.2.1.Using Filters to customize scanning

<context:component-scan/>은 해당 클래스패스 내에 @Component, @Service, @Repository, @Controller annotation 이 적용된 클래스를 모두 찾아서 Spring 컨테이너가 관리하는 컴포넌트로 등록하도록 하는 설정이다. 이와 같은 디폴트 설정으로 stereotype annotation을 Auto Detecting하여 사용 시, 비즈니스 레이어와 프레젠테이션 레이어에 중복으로 <context:component-scan/>을 설정하는 경우 다음과 같은 문제가 발생할 수 있다.

  • Auto Detecting으로 야기되는 문제점

    • Annotation이 적용된 컴포넌트 클래스가 비즈니스 레이어의 Application Context와 프레젠테이션 레이어의 WebApplication Context에 중복하여 등록된다.

    • 비즈니스 레이어의 Application Context와 프레젠테이션 레이어의 WebApplication Context는 Parent-child 관계이며 일반적으로 AOP 설정은 비즈니스 레이어에서 관리한다.

    • 따라서 Proxy 기반의 Spring AOP는 비즈니스 레이어의 Application Context에 등록된 컴포넌트에만 적용된다.

    • WebApplication Context에 등록된 비즈니스 레이어에 해당하는 컴포넌트는 AOP가 적용되지 않는다.

    • 이로 인해 WebApplication Context에서는 AOP가 적용되지 않은 비즈니스 컴포넌트를 먼저 참조하여 Spring AOP가 동작하지 않을 문제점이 발생한다.

    이와 같은 문제를 방지하기 위해서 비즈니스 레이어(Application Context)에서 관리되어야하는 컴포넌트와 프레젠테이션 레이어(Web Application Context)에서 관리되어야하는 컴포넌트를 구분할 필요가 있다.

    다음은 프레젠테이션 레이어에서 @Controller annotation이 적용된 클래스만 WebApplication Context에 등록하는common-servlet.xml 파일의 설정 예이다.

    <!-- use-default-filters="false"로 설정하고 include-filter를 사용했기 때문에 
          WebApplicationContext에는 stereotype @Contoller Bean 만 등록된다. -->
    <context:component-scan base-package="anyframe.sample.springmvc" 
                                                      use-default-filters="false">
        <context:include-filter type="annotation" 
            expression="org.springframework.stereotype.Controller" />
    </context:component-scan>

    위의 예와 같이 <context:component-scan>하위에 <context:include-filter>나 <context:exclude-filter>를 추가하면 컨테이너에 의해 검색될 대상의 범위를 조정할 수 있다. filter에 대한 자세한 내용은 본 매뉴얼 >> Spring >>Annotation 을 참고 바란다.

19.2.Controller

사용자가 Spring MVC의 컨트롤러를 작성하려면 AbstractController나 SimpleFormController 등 Spring에서 제공하는 컨트롤러 클래스를 상속받아야만 했다. Spring 2.5 이상에서는 다른 클래스를 상속받거나 Servlet API를 사용하지 않아도 annotation을 사용하여 컨트롤러를 구현할 수 있다. 본 문서에서는 annotation을 사용하여 Spring MVC 컨트롤러를 작성하는 방법에 대해서 알아본다.

  • @Controller : 컨트롤러 클래스 정의

  • @RequestMapping : HTTP Request URL을 처리할 컨트롤러 클래스 또는 메소드 정의

  • @RequestParam : HTTP Request에 포함된 파라미터 참조 시 사용

  • @ModelAttribute : HTTP Request에 포함된 파라미터를 Model 객체로 바인딩함, @ModelAttribute의 'name'으로 정의한 Model객체를 다음 View에서 사용 가능

  • @SessionAttributes : Session에 저장할 Model attribute를 정의

19.2.1.@Controller

특정 클래스에 @Controller annotation을 적용하면 다른 클래스를 상속받거나 Servlet API를 사용하지 않아도 해당 클래스가 컨트롤러 역할을 수행하도록 해준다.

다음은 @Controller를 사용하여 작성한 ProductController 클래스 파일의 일부이다.

@Controller
public class ProductController {
    // 중략
}

19.2.2.@RequestMapping

@RequestMapping annotation은 컨트롤러 클래스나 메소드가 특정 HTTP Request URL을 처리하도록 매핑하기 위해서 사용한다. 그래서 클래스 선언부에 @RequestMapping을 적용할 수도 있고(이하 Type-Level), 클래스의 메소드에 @RequestMapping을 적용할 수도 있다(이하 Method-Level). Type-Level의 @RequestMappign에 URL path를 정의한 경우, Method-Level의 @RequestMapping에서는 Type-Level의 URL path를 상속받는다.

@Controller
@RequestMapping("/listProduct.do")
public class ProductController {
    // 중략    
    @RequestMapping
    public ModelAndView list(HttpServletRequest request, ProductSearchVO searchVO) 
                                                                    throws Exception {
        // 중략
        Page resultPage = productService.getPagingList(searchVO);
        
        mnv.addObject("productList", resultPage.getList());
        // 중략

        return mnv;
    }
}

@RequestMapping은 구현하는 컨트롤러 종류에 따라 아래와 같은 방식으로 사용할 수 있다.

  • Form Controller 구현

  • Multi-action Controller 구현

기존에 SimpleFormController와 같은 Controller 클래스를 상속받아서 컨트롤러를 작성할 때는, 상위클래스에 정의된 메소드를 override하여 구현하기 때문에 입력 argument 타입과 return 타입이 이미 정해져있다. 이에 반해 @RequestMapping을 적용하여 작성하는 핸들러 메소드는 다양한 argument 타입과 return 타입을 사용할 수 있다.

19.2.2.1.Form Controller 구현

  • 클래스 선언부에 @RequestMapping을 사용하여 처리할 Request URL Mapping

  • 메소드에는 @RequestMapping의 'method', 'params'와 같은 상세 속성 정보를 정의하여 Request URL의 Mapping을 세분화

위와 같이 작성하면 기존에 SimpleFormController를 상속받아 작성하였던 폼을 처리하는 컨트롤러를 구현할 수 있다. 다음은 폼 처리 컨트롤러를 작성한 EditProductController 의 예이다.

@Controller
@RequestMapping("/product.do")
public class EditProductController {

    @RequestMapping(method = RequestMethod.GET)
    public ModelAndView addProductView() {
        // 중략
        return mnv;
    }

    @RequestMapping(method = RequestMethod.POST)
    public String addProduct(HttpServletRequest request, @ModelAttribute("product")
        Product product, BindingResult result, SessionStatus status) throws Exception {
        // 중략
        return "/listProduct.do";
    }
}

19.2.2.2.Multi-action Controller 구현

@RequestMapping annotation을 사용하여 여러 HTTP Request를 처리할 수 있는 Multi-action 컨트롤러를 구현할 수 있다.

  • 메소드에 Request URL을 Mapping한 @RequestMapping을 정의

다음은 Multi-action 컨트롤러를 구현한ProductController 의 예이다.

@Controller
public class ProductController {

    @RequestMapping("/listProduct.do")
    public ModelAndView getProductList() {
        // 중략
        return mnv;
    }

    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo")
    String productNo, ModelMap model) {
        // 중략
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }    
}
        

@RequestMapping annotation에는 다음과 같은 상세 속성 정보를 부여할 수 있다.

nameDescription
value"value='/getProduct.do'"와 같은 형식의 매핑 URI이다. 디폴트 속성이기 때문에 value만 정의하는 경우에는 'value='은 생략할 수 있다. 예 : @RequestMapping(value = {"/addProduct.do", "/updateProduct.do" }) 위의 경우 "/addProduct.do", "/updateProduct.do" 두 URL 모두 처리한다.
methodGET, POST, HEAD 등으로 표현되는 HTTP Request method에 따라 requestMapping을 할 수 있다. 'method=RequestMethod.GET' 형식으로 사용한다. method 값을 정의하지 않는 경우 모든 HTTP Request method에 대해서 처리한다. 예 : @RequestMapping(method = RequestMethod.POST). 이 경우 value 값은 클래스 선언에 정의한 @RequestMapping의 value 값을 상속받는다.
paramsHTTP Request로 들어오는 파라미터 표현이다.'params={"param1=a", "param2", "!myParam"}' 로 다양하게 표현 가능하다. 예 : @RequestMapping(params = {"param1=a", "param2", "!myParam"}) 위의 경우 HTTP Request에 param1과 param2 파라미터가 존재해야하고 param1의 값은 'a'이어야 하며, myParam이라는 파라미터는 존재하지 않아야한다. 또한, value 값은 클래스 선언에 정의한 @RequestMapping의 value 값을 상속받는다.

19.2.2.3.Supported argument types

@RequestMapping을 사용하여 작성하는 핸들러 메소드는 다음과 같은 타입의 입력 argument를 순서에 관계없이 정의할 수 있다. 단, validation results를 입력 argument로 받을 경우에는 해당 command 객체 바로 다음에 위치해야한다.

  • Servlet API의 Request와 Response 객체

    ServletRequest 또는 HttpServletRequest 등을 메소드 내부에서 직접 사용해야 하는 경우

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product
                                             , BindingResult result, SessionStatus status) throws Exception {
        // 중략
        String message = messageSource.getMessage(
           "product.error.exist", new String[] {product.getProductNo() },
            localeResolver.resolveLocale(request));
       }

  • Servlet API의 Session

    HttpSession 객체를 메소드 내부에서 사용하는 경우 예 : user 정보와 같은 global session attribute를 사용할 때

    @RequestMapping("/login.do")
    protected ModelAndView handleRequestInternal(HttpSession session, 
    @RequestParam("userId") String userId) throws Exception {
    	session.setAttribute("userId", userId);
    	return new ModelAndView("/index.jsp");
    }

  • java.util.Locale

    현재 request의 locale을 사용할 경우

    @RequestMapping(params = "param=add")
    public String addProduct(Locale locale, Product product, BindingResult result
                                                                  , SessionStatus status) throws Exception {
        // 중략
        String message = messageSource.getMessage(
        			"product.error.exist", new String[] {product.getProductNo()}, locale);
       }

  • java.io.InputStream 또는 java.io.Reader

    Request의 content를 직접 처리할 경우 (Servlet API가 제공하는 raw InputStream/Reader)

    @RequestMapping(params = "param=add")
    public String addProduct(InputStream is, Product product, BindingResult result
                                                                , SessionStatus status) throws Exception {
            // 중략
            for(int totalRead = 0; totalRead < totalBytes; totalRead += readBytes) {
                readBytes = is.read(binArray, totalRead, totalBytes - totalRead);
                // 중략
            }
        
        // 중략
    }

  • java.io.OutputStream 또는 java.io.Writer

    Response의 content를 직접 처리할 경우(Servlet API가 제공하는 raw OutputStream/Writer)

    @RequestMapping(params = "param=add")
    public String addProduct(OutputStream os, Product product, BindingResult result, SessionStatus status) throws Exception {
        // 중략
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();    
        byte[] content = outStream.toByteArray();
        os.write(content);
        os.flush();
        // 중략
    }

  • @RequestParam annotation이 적용된 argument

    ServletRequest.getParameter(java.lang.String name)와 같은 역할 수행

    @RequestMapping("/deleteProduct.do")
    public String deleteProduct(@RequestParam("productNo") String productNo) {
        productService.deleteProduct(productNo);
        return "/listProduct.do";
    }

  • java.util.Map 또는 org.springframework.ui.Model 또는 org.springframework.ui.ModelMap

    Web View로 데이터를 전달해야 하는 경우 위 타입의 argument를 정의하고, 메소드 내부에서 View로 전달할 데이터를 추가함

    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo") String productNo, Map map) {
        Product product = productService.getProduct(productNo);    
        
        map.put("product", product);
        
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }
    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo") String productNo, Model model) {
        Product product = productService.getProduct(productNo);
        
       model.addAttribute("product", product);
        
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }
    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo") String productNo, ModelMap modelMap) {
        Product product = productService.getProduct(productNo);
        
       modelMap.addAttribute("product", product);
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }
  • Command 또는 form 객체

    HTTP Request로 전달된 parameter를 binding한 객체로 다음 View에서 사용 가능하고 @SessionAttributes를 통해 session에 저장되어 관리될 수 있다. @ModelAttribute annotation을 이용하여 사용자 임의로 이름을 부여할 수 있다.

    @RequestMapping("/addProduct.do")
    public String updateProduct(Product product, SessionStatus status) throws Exception {
        // 여기서 'product'가 Command(/form) 객체이다.
        return "/listProduct.do";
    }

    @RequestMapping("/addProduct.do")
    public String updateProduct(@ModelAttribute("updatedProduct") Product product, 
        SessionStatus status) throws Exception {
        // 여기서 'updatedProduct'라는 이름의 'product'객체가 Command(/form) 객체이다.
        return "/listProduct.do";
    }
  • org.springframework.validation.Errors 또는 org.springframework.validation.BindingResult

    바로 이전의 입력파라미터인 Command 또는 form 객체의 validation 결과 값을 저장하는 객체로 해당 command 또는 form 객체 바로 다음에 위치해야 함에 유의하도록 한다.

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product, BindingResult result
                                                                                 , SessionStatus status) throws Exception {
            
        new ProductValidator().validate(product, result);
        if (result.hasErrors()) {
            return "/WEB-INF/jsp/annotation/sales/product/productForm.jsp";
        } else {
            // 중략
            return "/listProduct.do";
        }
    }

  • org.springframework.web.bind.support.SessionStatus

    폼 처리가 완료되었을 때 status를 처리하기 위해서 argument로 설정. SessionStatus.setComplete()를 호출하면 컨트롤러 클래스에 @SessionAttributes로 정의된 Model객체를 session에서 지우도록 이벤트를 발생시킨다.

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product, BindingResult result
                                                                , SessionStatus status) {
        // 중략
        productService.addProduct(product);
        status.setComplete();
        return "/listProduct.do";
    }

19.2.2.4.Supported return types

@RequestMapping을 이용한 핸들러 메소드는 다음과 같은 리턴타입을 가질 수 있다.

  • ModelAndView 객체

    View와 Model 정보를 모두 포함한 객체를 리턴하는 경우.

    @RequestMapping(params = "param=addView")
    public ModelAndView addProductView() {
        ModelAndView mnv = 
                 new ModelAndView("/WEB-INF/jsp/annotation/sales/product/productForm.jsp");
        mnv.addObject("product", new Product());
        return mnv;
    }

  • Map

    Web View로 전달할 데이터만 리턴하는 경우.

    @RequestMapping("/productList.do")
    public Map getProductList() {
        List productList = productService.getProductList();
        ModelMap map = new ModelMap(productList);//productList가 "productList"라는 이름으로 저장됨.
        return map;
    }

    여기서 View에 대한 정보를 명시적으로 리턴하지는 않았지만, 내부적으로 View name은 RequestToViewNameTranslator에 의해서 입력된 HTTP Request를 이용하여 생성된다. 예를 들어DefaultRequestToViewNameTranslator는 입력된 HTTP Request URI를 변환하여 View name을 다음과 같이 생성한다.

    http://localhost:8080/anyframe-sample/display.do     -> 생성된 View name : 'display'
    http://localhost:8080/anyframe-sample/admin/index.do -> 생성된 View name : 'admin/index'

    위와 같이 자동으로 생성되는 View name에 'jsp/'와 같이 prefix를 붙이거나 '.jsp' 같은 확장자를 덧붙이고자 할 때는 아래와 같이 속정 정의 XML(xxx-servlet.xml)에 추가하면 된다.

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

  • Model

    Web View로 전달할 데이터만 리턴하는 경우Model 은 Java-5 부터 추가된 인터페이스이다. 기본적으로 ModelMap과 같은 기능을 제공한다. Model 인터페이스의 구현클래스에는 BindingAwareModelMap 와 ExtendedModelMap이 있다. View name은 위에서 설명한 바와 같이 RequestToViewNameTranslator에 의해 내부적으로 생성된다.

    @RequestMapping("/productList.do")
    public Model getProductList() {
        List productList = productService.getProductList();
        ExtendedModelMap map = new ExtendedModelMap();
        map.addAttribute("productList", productList);
        
        return map;
    }

  • String

    View name만 리턴하는 경우.

    @RequestMapping(value = {"/addProduct.do", "/updateProduct.do" })
    public String updateProduct(Product product, SessionStatus status) throws Exception {
    
        // 중략
        
        return "/listProduct.do";
    }

  • void

    메소드 내부에서 직접 HTTP Response를 직접 처리하는 경우. 또는 View name이 RequestToViewNameTranslator에 의해 내부적으로 생성되는 경우

    @RequestMapping("/addView.do")
    public void addView(HttpServletResponse response) {
        // 중략
        // response 직접 처리
    }

    @RequestMapping("/addView.do")
    public void addView() {
        // 중략
        // View name이 DefaultRequestToViewNameTranslator에 의해서 내부적으로 'addView'로 결정됨.
    }

19.2.3.@RequestParam

@RequestParam annotation은 HTTP Request parameter를 컨트롤러 메소드의 argument로 바인딩하는데 사용되며ServletRequest.getParameter(java.lang.String name) 와 같은 역할을 한다. 다음은 @RequestParam annotation의 사용 예이다.

@RequestMapping("/updateProduct.do")
public String updateProduct(@RequestParam("productNo") String productNo,
    @RequestParam("sellAmount") int sellAmount, 
    @RequestParam("realImageFile") MultipartFile picturefile) {
    // 중략
    return "/listProduct.do";
}

@RequestParam을 적용한 파라미터는 반드시 HTTP Request에 존재해야 한다. 그렇지 않은 경우 다음과 같이 org.springframework.web.bind.MissingServletRequestParameterException이 발생한다.

org.springframework.web.bind.MissingServletRequestParameterException:
                         Required java.lang.String parameter 'productNo' is not present

그러나 아래와 같이 @RequestParam의 required 속성을 false로 설정할 경우 HTTP Request에 파라미터가 존재하지 않아도 Exception이 발생하지 않는다.

@RequestMapping("/deleteProduct.do")
public String deleteProduct(@RequestParam(value="productNo", required="false")String productNo){
    // 중략
}

19.2.4.@ModelAttribute

@ModelAttribute는 컨트롤러에서 다음과 같이 두 가지 방법으로 사용할 수 있다.

  • 메소드 자체에 정의

    입력 폼 페이지에서 출력해 줄 reference data를 전달하고자 할 때. SimpleFormController의 referenceData() 메소드와 같은 역할

  • 메소드의 입력 argument에 정의

    메소드의 argument로 입력된 Command 객체에 이름을 부여하고자 할 때.

다음은 위에서 설명한 두가지 방법으로 @ModelAttribute를 사용한 EditProductController 의 예이다.

@Controller
@RequestMapping("/product.do")
public class EditProductController {
    // 메소드 자체에 정의
    @ModelAttribute("categoryList")
    public List populateCategoryList() throws Exception {
        return categoryService.getCategoryList();
    }
    
    // 메소드의 입력 argument에 정의
    @RequestMapping(params = "param=add")
    public String addProduct(@ModelAttribute("updatedProduct")
        Product product, BindingResult result, SessionStatus status) throws Exception {
        // 중략
    }
}

19.2.5.@SessionAttributes

@SessionAttributes는 session에 저장하여 관리할 model attribute를 정의할 때 사용한다. @SessionAttributes에 정의하는 attribute의 이름은 해당 컨트롤러 클래스안에서 사용되는 model attribute의 이름과 같아야 한다.

다음은 @SessionAttributes를 사용하여 session에 저장하여 관리할 model을 정의한 예이다.

@Controller
@RequestMapping("/product.do")
@SessionAttributes(value = {"product", "category"})
public class EditProductController {
    // 중략
}

19.3.Dependency Injection

컨트롤러 클래스에서 기능 수행을 위해 다른 Bean을 참조해야 하는 경우 @Autowired 또는 @Resource annotation을 사용한다. @Resource와 @Autowired annotation에 대한 자세한 설명은 본 매뉴얼 >> Spring >>Annotation 부분을 참고하기 바란다.

다음은 컨트롤러 클래스에서 @Resource annotation을 사용한 EditProductController 의 예이다.

@Controller
@RequestMapping("/product.do")
public class EditProductController {
    @Resource(name = "productService")
    ProductService productService;

    @Resource
    CategoryService categoryService;

    @Resource
    MessageSource messageSource;

    @Resource
    LocaleResolver localeResolver;
    // 중략
}

19.4.Double Submit Prevention

Spring MVC에서는 double submit을 방지하기 위해 AbstractFormController를 제공하고있고, 폼 컨트롤러 구현 시에 사용하는 SimpleFormController 또한 AbstractFormController를 상속받았기 때문에 double submit 방지가 가능하다. XML 기반의 double submit 방지 기능 적용 방법은 본 매뉴얼 >> Spring MVC >> Extension >>Double Submit 부분을 참고한다. 본 문서에서는 Annotation을 사용하여 다른 클래스를 상속받지 않고도 double submit 방지 기능을 구현하는 방법에 대해서 자세히 알아본다.

19.4.1.Annotation을 이용한 Double Submit 방지

annotation을 이용한 Double Submit 방지는 다음과 같은 원리로 구현된다.

  • Double submission을 방지하고자 하는 form 객체를 model로 저장

    다음 예제와 같이 ModelAndView, ModelMap 등을 이용하여 저장한다.

    @RequestMapping(params = "param=addView")
    public ModelAndView addProductView() {
        ModelAndView mnv = 
                  new ModelAndView("/WEB-INF/jsp/annotation/sales/product/productForm.jsp");
        mnv.addObject("product", new Product());
        return mnv;
    }

  • 저장한 model을 @SessionAttributes로 정의

    다음 예제와 같이 컨트롤러 클래스 선언부에 @SessionAttributes("product")로 정의한다.

    @Controller
    @RequestMapping("/product.do")
    @SessionAttributes("product")
    public class EditProductController {
            // 중략
    }

  • 컨트롤러 메소드에서 폼 처리 완료 후 Session status 변경

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product, BindingResult result
                                   , SessionStatus status) throws Exception {
        productService.addProduct(product);
        status.setComplete();
        return "/listProduct.do";    
    }
  • status.setComplete()는 session에서 저장된 model을 삭제하는 이벤트 발생

  • 사용자가 같은 버튼을 여러번 클릭하는 경우와 같이, 여러 thread가 동시에 Session에 접근할 수도 있기 때문에 반드시 AnnotationMethodHandlerAdapter의 synchronizeOnSession 속성을 true로 설정

    <bean id="annotationHandlerAdaptor"
        class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="synchronizeOnSession" value="true" />
    </bean>

  • 따라서, 이후에 다시 submit 요청이 온 경우 session에 저장된 model이 삭제되었기 때문에 아래와 같이 org.springframework.web.HttpSessionRequiredException발생

    org.springframework.web.HttpSessionRequiredException: 
          Session attribute 'dept' required - not found in session

19.5.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.annotation.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-tasks-2.0.10.jar 파일이 있어야 한다.)

    표 19.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.annotation.zipDownload
    maven-ant-tasks-2.0.10.jarDownload


출처 - http://dev.anyframejava.org/docs/anyframe/4.0.0/reference/html/ch19.html







11.Controller

MVC에서 C에 해당하는 컨트롤러는 사용자의 요청을 받아서 어플리케이션에 정의된 적절한 Service를 수행한 후, 그 결과를 다시 View를 통해 사용자에게 보여줄 수 있는 Model 데이터로 변환하는 역할을 담당한다. Spring에서는 이러한 컨트롤러를 특정 API에 종속되지 않고 사용자가 자유롭게 작성할 수 있는 추상적인 구현 방법을 제공하고 있다.

Spring 2.5에서부터 @RequestMapping, @RequestParam, @ModelAttribute 등을 이용한 Annotation 기반의 컨트롤러 개발 방식을 소개했다. Annotation을 사용하여 SpringMVC기반의 컨트롤러를 작성하면, 특정 인터페이스를 상속받거나 특정 클래스를 상속받지 않아도 된다. 또한 Servlet API와도 독립적으로 작성할 수 있다는 장점이 있다. (단, annotation은 JAVA 5 이상에서만 사용가능함에 유의하도록 한다.)

Spring MVC controller hierarchy deprecated

기존에 Spring에서 제공하던 AbstractFormController등의 Form 관련 Class 계층은 Spring 3 부터는 더이상 지원하지 않는다. Spring MVC에서는 @Controller, @RequestMapping 등의 Annotation을 기반으로 컨트롤러를 개발하도록 가이드하고 있다.

11.1.Configuration

컨트롤러 역할을 수행하는 클래스를 정의하기 위해서는 Spring에서 제공하는 Stereotype Annotation 중 @Controller를 사용한다. 이렇게 정의된 컨트롤러 클래스는 XML 설정 파일에 명시적으로 Bean으로 정의하여 Spring Container에 등록할 수도 있지만, 자동으로 검색 및 등록이 가능하게 할 수도 있다. Spring에서는 이를 Autodetection이라고 한다. Stereotype Annotation이 적용된 클래스들에 대한 Autodetection이 이루어 지도록 하기 위해서는 <context:component-scan/> 을 속성 정의 XML에 추가해 주어야 한다. <context:component-scan/>에 대한 자세한 내용은 본 매뉴얼 >> Foundation Plugin >> Spring >> Annotation을 참고하기 바란다.

11.1.1.Using Filters to customize scanning

<context:component-scan/>은 해당 클래스패스 내에 @Component, @Service, @Repository, @Controller가 적용된 클래스를 모두 찾아서 Spring 컨테이너가 관리하는 컴포넌트로 등록하도록 하는 설정이다. 이와 같은 디폴트 동작 방식으로 Autodetection 기능 이용 시, 비즈니스와 프레젠테이션 레이어 간 Bean 정의 XML을 분리하여 관리하면서 <context:component-scan/>을 중복으로 설정하는 경우 다음과 같은 문제가 발생할 수 있다.

  • Autodetection 중복 설정으로 인해 야기되는 문제점

    • Stereotype Annotation이 적용된 클래스가 비즈니스 레이어의 Root WebApplicationContext와 프레젠테이션 레이어 의 WebApplicationContext에 중복하여 등록된다.

    • 비즈니스 레이어의 Root WebApplicationContext와 프레젠테이션 레이어의 WebApplicationContext는 Parent Container - Child Container 관계로 구성된다. Container가 계층 구조를 가질 때, 사용하고자 하는 Bean 검색 순서는 현재 자기 Container가 먼저이고, Bean이 없을 경우 Parent Container가 그 다음이다.

    • 일반적으로 AOP 설정은 비즈니스 레이어에서 관리한다. 따라서 Proxy 기반의 Spring AOP는 비즈니스 레이어의 Root WebApplicationContext에 등록된 Bean에만 적용되고, 프레젠테이션 레이어의 WebApplicationContext에 중복으로 등록된 Bean에는 적용되지 않는다.

    • 결과적으로 프레젠테이션 레이어의 WebApplicationContext에서는 Proxy 기반의 Spring AOP가 적용되지 않은 Bean을 먼저 참조하게 되어 Spring AOP를 사용하여 설정한 기능들이 동작하지 않는 문제점이 발생한다.

    다음은 위의 내용을 그림으로 나타낸 것이다.

    이와 같은 문제를 방지하기 위해서 비즈니스 레이어(Root WebApplicationContext)에서 관리되어야하는 Bean과 프레젠테이션 레이어(Child WebApplicationContext)에서 관리되어야하는 Bean을 구분할 필요가 있다.

    다음은 프레젠테이션 레이어에서 @Controller annotation이 적용된 클래스만 WebApplication Context에 등록하는common-servlet.xml 파일의 설정 예이다.

    <!-- use-default-filters="false"로 설정하고
    include-filter를 사용했기 때문에 이 WebApplicationContext에는 @Contoller가 적용된 클래스만 등록된다. -->
    <context:component-scan base-package="anyframe.sample.springmvc" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    위의 예와 같이 <context:component-scan>하위에 <context:include-filter> 나 <context:exclude-filter>를 추가하면 컨테이너에 의해 검색될 대상의 범위를 조정할 수 있다. filter에 대한 자세한 내용은 본 매뉴얼 >> Foundation Plugin >> Spring >> Annotation 을 참고 바란다.

11.2.컨트롤러 구현

앞에서 설명했듯이, Spring MVC에서는 요청을 처리하는 컨트롤러를 특정 인터페이스 구현하거나 특정 클래스 상속받아서 구현하지 않아도 된다. @Controller, @RequestMapping 등의 Annotation만을 이용하여 다양한 형태의 컨트롤러를 만들 수 있다. 본 문서에서는 Spring MVC에서 제공하는 Annotation을 사용하여 컨트롤러를 작성하는 방법에 대해서 알아본다.

  • @Controller : 컨트롤러 클래스 정의

  • @RequestMapping : 처리할 HTTP Request URL과 컨트롤러 클래스 또는 메소드 매핑

  • @RequestParam : HTTP Request에 포함된 파라미터 참조 시 사용

  • @RequestHeader : HTTP Request의 Header 값 참조 시 사용

  • @CookieValue : HTTP Cookie 값 참조 시 사용

  • @ModelAttribute : HTTP Request에 포함된 파라미터를 Model 객체로 바인딩함, @ModelAttribute의 'name'으로 정의한 Model객체를 다음 View에서 사용 가능

  • @SessionAttributes : Session에 저장할 Model attribute를 정의

  • @RequestBody/@ResponseBody : 핸들러 메소드가 HTTP Request와 Response의 Body 메세지를 전체를 직접 접근할 경우에 사용 가능. (HttpEntity 객체를 이용하여 HTTP Request나 Response의 Body 메세지나 Header 값을 처리할 수도 있다.)

11.2.1.@Controller

특정 클래스에 @Controller annotation을 적용하면 다른 클래스를 상속받거나 Servlet API를 사용하지 않아도 해당 클래스가 컨트롤러 역할을 수행하도록 정의할 수 있다.

다음은 @Controller를 사용하여 작성한 MovieController 클래스 파일의 일부이다.

@Controller
public class MovieController {
    // 중략
}

11.2.2.@RequestMapping

@RequestMapping annotation은 컨트롤러 클래스나 메소드가 특정 HTTP Request URL을 처리하도록 매핑하기 위해서 사용한다. 그래서 클래스 선언부에 @RequestMapping을 적용할 수도 있고(이하 Type-Level), 클래스의 메소드에 @RequestMapping을 적용할 수도 있다(이하 Method-Level). 예를 들어, Type-Level에 @RequestMapping("/movies")라고 정의하고, Method-Level에 @RequestMapping("/new") 라고 정의하면 @RequestMapping("/new")라고 정의한 메소드가 처리하는 URL 경로는 "/movies/new" 가 된다. @RequestMapping은 "/movies/*.do"와 같은 Ant 스타일 경로 패턴도 지원한다. @RequestMapping에는 URL 경로 외에도 HTTP method나 Request 파라미터 등을 추가하여 처리할 URL의 범위를 줄일 수 있다.

또한, Spring 3 부터 REST 스타일의 Web Application 개발을 위해서 URI templates을 지원하기 시작했다. Spring 3에서 추가된 REST 관련 기능들과 REST Style 웹 어플리케이션 개발에 대한 자세한 내용은 본 매뉴얼 Restweb Plugin을 참고하기 바란다.

다음은 @RequestMapping을 사용하여 처리할 URL을 매핑한 코드예이다.

@Controller
@RequestMapping("/foundationMovie.do")
public class MovieController {
    @RequestMapping(params="method=get")
    public String get(@RequestParam("movieId") String movieId, Model model) throws Exception {
        Movie movie = this.movieService.get(movieId);
        //...
        model.addAttribute(movie);
        return "foundationViewMovie";
    }
}

@RequestMapping annotation에는 다음과 같은 상세 속성 정보를 부여하여 처리할 URL의 범위를 한정지을 수 있다.

nameDescription
value

"value='/getMovie.do'"와 같은 형식의 매핑 URL 값이다. 디폴트 속성이기 때문에 value만 정의하는 경우에는 'value='은 생략할 수 있다.

예 : @RequestMapping(value={"/addMovie.do", "/updateMovie.do" }) 이와 같은 경우 "/addMovie.do", "/updateMovie.do" 두 URL 모두 처리한다.

methodGET, POST, HEAD 등으로 표현되는 HTTP Request method에 따라 requestMapping을 할 수 있다. 'method=RequestMethod.GET' 형식으로 사용한다. method 값을 정의하지 않는 경우 모든 HTTP Request method에 대해서 처리한다. 예 :@RequestMapping(method = RequestMethod.POST). 이 경우 value 값은 클래스 선언에 정의한 @RequestMapping의 value 값을 상속받는다.
params

HTTP Request로 들어오는 파라미터 표현이다.'params={"param1=a", "param2", "!myParam"}' 로 다양하게 표현 가능하다.

예 : @RequestMapping(params = {"param1=a", "param2", "!myParam"})위의 경우 HTTP Request에 param1과 param2 파라미터가 존재해야하고 param1의 값은 'a'이어야하며, myParam이라는 파라미터는 존재하지 않아야한다. 또한, value 값은 클래스 선언에 정의한 @RequestMapping의 value 값을 상속받는다.

headersHTTP Request의 헤더 값이다.'headers="someHader=someValue"', 'headers="someHader"', 'headers="!someHader"' 로 다양하게 표현 가능하다. Accept나 Content-Type 같은 헤더에 대해서 media type 표현 시 '*' 도 지원한다. 예 : @RequestMapping(value="/movie.do", headers="content-type=text/*") 의 경우 HTTP Request에 Content-Type 헤더 값이 "text/html", "text/plain" 모두 매칭이 된다. 또한, Type-Level, Method-Level에서 모두 사용할 수 있는데, Type-Level에 정의된 경우, 하위의 모든 핸들러 메소드에서도 Type-Level에서 정의한 헤더값 제한이 적용된다.

@RequestMapping은 구현하는 컨트롤러 종류에 따라 아래와 같은 방식으로 사용할 수 있다.

  • Form 컨트롤러 구현

  • Multi-action 컨트롤러 구현

기존에 SimpleFormController와 같은 컨트롤러 클래스를 상속받아서 컨트롤러를 작성할 때는, 상위클래스에 정의된 메소드를 override하여 구현하기 때문에 입력 argument 타입과 return 타입이 이미 정해져있다. 이에 반해 @RequestMapping을 적용하여 작성하는 핸들러 메소드는 다양한 argument 타입과 return 타입을 사용할 수 있다.

11.2.2.1.Form 컨트롤러 구현

  • 클래스 선언부에 @RequestMapping을 사용하여 처리할 Request URL Mapping

  • 메소드에는 @RequestMapping의 'method', 'params'와 같은 상세 속성 정보를 정의하여 Request URL의 Mapping을 세분화

위와 같이 작성하면 기존에 SimpleFormController를 상속받아 작성하였던 폼을 처리하는 컨트롤러를 구현할 수 있다. 다음은 폼 처리 컨트롤러를 작성한 EditMovieController 의 예이다.

@Controller
@RequestMapping("/foundationMovie.do")
public class EditMovieController {

    @RequestMapping(method = RequestMethod.GET)
    public String createView() {
        // 중략
        return foundationViewMovie;
    }
    
    @RequestMapping(method = RequestMethod.POST)
    public String addMovie(HttpServletRequest request, @ModelAttribute("movie"),
            Movie movie, BindingResult result, SessionStatus status) throws Exception {
        // 중략
        return "redirect:/foundationMovieFinder.do";
    }
}

11.2.2.2.Multi-action 컨트롤러 구현

@RequestMapping annotation을 사용하여 여러 HTTP Request를 처리할 수 있는 Multi-action 컨트롤러를 구현할 수 있다.

  • 메소드에 처리할 Request URL을 Mapping한 @RequestMapping을 정의

다음은 Multi-action 컨트롤러를 구현한MovieController 의 예이다.

@Controller
public class MovieController {

    @RequestMapping("/deleteMovie.do")
    public ModelAndView delete(@RequestParam("movieId") String movieId) {
        // 중략
        return "redirect:/foundationMovieFinder.do";
    }

    @RequestMapping("/getMovie.do")
    public String get(@RequestParam("movieId") String movieId, ModelMap model) {
        // 중략
        model.addAttribute(movie);

        return "foundationViewMovie";
    }
}

11.2.2.3.Supported argument types

@RequestMapping을 사용하여 작성하는 핸들러 메소드는 다음과 같은 타입의 입력 argument를 순서에 관계없이 정의할 수 있다. 단, validation results를 입력 argument로 받을 경우에는 해당 command 객체 바로 다음에 위치해야한다.

  • Servlet API의 Request와 Response 객체

    ServletRequest 또는 HttpServletRequest 등을 메소드 내부에서 직접 사용해야 하는 경우

    @RequestMapping(params = "param=add")
    public String addMovie(HttpServletRequest request,
            Movie movie, BindingResult result, SessionStatus status)
    		throws Exception {
        // 중략
        String message = messageSource.getMessage(
                                        "movie.error.exist", new String[] {movie.getMovieId()},
                                        localeResolver.resolveLocale(request));
    }

  • Servlet API의 Session

    HttpSession 객체를 메소드 내부에서 사용하는 경우 예 : user 정보와 같은 global session attribute를 사용할 때

    @RequestMapping("/login.do")
    protected ModelAndView handleRequestInternal( HttpSession session,
                @RequestParam("userId") String userId) throws Exception {
        session.setAttribute("userId", userId);
        return new ModelAndView("/index.jsp");
    }

    AnnotationMethodHandlerAdapter의 'synchronizeOnSession' 속성

    Servlet 환경에서 Session 접근은 thread-safe하지 않기 때문에, Session에 저장된 정보에 여러개의 thread가 동시에 접근하여 변경할 가능성이 있는 경우 반드시 AnnotationMethodHandlerAdapter의 "synchronizeOnSession" 속성을 "true"로 셋팅하도록 한다.

  • java.util.Locale

    현재 request의 locale을 사용할 경우

    @RequestMapping(params = "param=add")
    public String addMovie(Locale locale,Movie movie, BindingResult result,
            SessionStatus status) throws Exception {
        // 중략
        String message = messageSource.getMessage(
                                        "movie.error.exist", new String[] {movie.getMovieId()}, 
                                        locale);
    }

  • java.io.InputStream 또는 java.io.Reader

    Request의 content를 직접 처리할 경우 (Servlet API가 제공하는 raw InputStream/Reader)

    @RequestMapping(params = "param=add")
    public String addMovie(InputStream is, Movie movie, BindingResult result
                                                             SessionStatus status) throws Exception {
        // 중략
        for(int totalRead = 0; totalRead < totalBytes; totalRead += readBytes) {
            readBytes = is.read(binArray, totalRead, totalBytes - totalRead);
            // 중략
        }
        // 중략
    }

  • java.io.OutputStream 또는 java.io.Writer

    Response의 content를 직접 처리할 경우 (Servlet API가 제공하는 raw OutputStream/Writer)

    @RequestMapping(params = "param=add")
    public String addMovie(OutputStream os, Movie movie, BindingResult result, 
                                                             SessionStatus status) throws Exception {
        // 중략
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        byte[] content = outStream.toByteArray();
        os.write(content);
        os.flush();
        // 중략
    }

  • @PathVariable annotation이 적용된 argument

    URI template 내의 변수를 핸들러 메소드에서 접근할 경우

    @PathVariable에 대한 자세한 사용 방법은 본 매뉴얼 >> Restweb Plugin >> URI Template 참고

    @RequestMapping(value = "/movies/{movieId}/edit", method = RequestMethod.GET)
    public String get(@PathVariable String movieId, Model model)
            throws Exception {
        Movie movie = this.movieService.get(movieId);
        // 중략
        model.addAttribute(movie);
        return "restwebViewMovie";
    }
  • @RequestParam annotation이 적용된 argument

    ServletRequest.getParameter(java.lang.String name)와 같은 역할 수행

    @RequestMapping(params = "method=remove")
    public String remove(@RequestParam("movieId") String movieId)
    		throws Exception {
        this.movieService.remove(movieId);
        return "redirect:/foundationMovieFinder.do?method=list";
    }
    

  • @RequestHeader annotation이 적용된 argument

    @RequestHeader를 사용하면 Servlet Request HTTP 헤더 값을 핸들러 메소드에서 사용 가능

    @RequestMapping("/displayHeaderInfo")
    @ResponseBody
    public String displayHeaderInfo(@CookieValue("JSESSIONID") String cookie,
    		@RequestHeader("Accept-Encoding") String encoding,
    		@RequestHeader("Accept") String accept) {
        StringBuffer sf = new StringBuffer();
        sf.append("JSESSIONID : " + cookie);
        sf.append("\n");
        sf.append("Accept-Encoding : " + encoding);
        sf.append("\n");
        sf.append("Accept : " + accept);
         
        return sf.toString();
    }

  • @RequestBody annotation이 적용된 argument

    @RequestBody를 사용하면 HTTP Request Body를 핸들러 매소드에서 직접 사용 가능

    HTTP Request Body가 HttpMessageConverter에 의해서 선언한 메소드 argument 타입으로 변환되어 전달됨

    @RequestMapping(value = "/movies/add", method = RequestMethod.POST)
    @ResponseBody
    public String add(@RequestBody Movie movie) throws Exception {
        this.movieService.createMovie(movie);		
        return "/movies/" + movie.getMovieId() + "/edit";
    }

  • HttpEntity<?> 객체

    Servlet request HTTP Header와 Body를 핸들러 메소드에서 접근하기 위해 사용 가능. Request 스트림은 HttpMessageConverter를 통해 entity body로 변환됨.

  • java.util.Map 또는 org.springframework.ui.Model 또는 org.springframework.ui.ModelMap

    Web View로 데이터를 전달해야 하는 경우 위 타입의 argument를 정의하고, 메소드 내부에서 View로 전달할 데이터를 추가함

    @RequestMapping("/getMovie.do")
    public String getMovie(@RequestParam("movieId") String movieId, Map map) {
        Movie movie = movieService.getMovie(movieId);
        map.put("movie", movie);
        return "/WEB-INF/jsp/annotation/sales/movie/viewMovie.jsp";
    }
    @RequestMapping("/getMovie.do")
    public String getMovie(@RequestParam("movieId") String movieId, Model model) {
        Movie movie = movieService.getMovie(movieId);
        model.addAttribute("movie", movie);
        return "/WEB-INF/jsp/annotation/sales/movie/viewMovie.jsp";
    }
    @RequestMapping("/getMovie.do")
    public String getMovie(@RequestParam("movieId") String movieId,  ModelMap modelMap) {
        Movie movie = movieService.getMovie(movieId);
        modelMap.addAttribute("movie", movie);
        return "/WEB-INF/jsp/annotation/sales/movie/viewMovie.jsp";
    }
  • Command 또는 Form 객체

    HTTP Request로 전달된 parameter를 binding한 객체로, 다음 View에서 사용 가능하고 @SessionAttributes를 통해 session에 저장되어 관리될 수 있음. @ModelAttribute annotation을 이용하여 사용자 임의로 이름 부여 가능.

    @RequestMapping("/addMovie.do")
    public String updateMovie(Movie movie, SessionStatus status) throws Exception {
        // 여기서 'movie'가 Command(또는 Form) 객체이다.
        return "/listMovie.do";
    }

    @RequestMapping(params="method=update")
    	public String update(@ModelAttribute("updatedMovie") Movie movie, SessionStatus status) throws Exception {
        // 여기서 'updatedMovie'라는 이름의 'movie'객체가 Command(/form) 객체이다.
        // 중략
        return "redirect:/foundationMovieFinder.do?method=list";
    }
  • org.springframework.validation.Errors 또는 org.springframework.validation.BindingResult

    바로 이전의 입력파라미터인 Command 또는 Form 객체의 validation 결과 값을 저장하는 객체로, 해당 Command 또는 Form 객체 바로 다음에 위치해야 함에 유의

    @RequestMapping(params = "method=create")
    public String create(
            @RequestParam(value="realPosterFile", required=false) MultipartFile posterFile,
            @Valid Movie movie, BindingResult results, SessionStatus status)
                throws Exception {
        if (results.hasErrors()) {
            return "foundationViewMovie";
        }
        
        // 중략
        return "redirect:/foundationMovieFinder.do?method=list";
    }

  • org.springframework.web.bind.support.SessionStatus

    Form 처리가 완료되었을 때 status를 처리하기 위해서 argument로 설정. SessionStatus.setComplete()를 호출 하면 컨트롤러 클래스에 @SessionAttributes로 정의된 Model객체를 session에서 지우도록 이벤트 발생

    @RequestMapping(params = "method=create")
    public String create(
            @RequestParam(value="realPosterFile",required=false) MultipartFile posterFile,
            @Valid Movie movie, BindingResult results, SessionStatus status)
                throws Exception {
        // 중략
        this.movieService.create(movie);
        status.setComplete();
        return "redirect:/foundationMovieFinder.do?method=list";
    }

11.2.2.4.Supported return types

@RequestMapping을 이용한 핸들러 메소드는 다음과 같은 리턴타입을 가질 수 있다.

  • ModelAndView 객체

    View와 Model 정보를 모두 포함한 객체를 리턴하는 경우.

    @RequestMapping(params="param=addView")
    public ModelAndView addMovieView() {
        ModelAndView mnv = new ModelAndView("/WEB-INF/jsp/annotation/sales/movie/movieForm.jsp");
        mnv.addObject("movie", new Movie());
        return mnv;
    }

  • Map

    Web View로 전달할 데이터만 리턴하는 경우.

    @RequestMapping("/movieList.do")
    public Map getMovieList() {
        List movieList = movieService.getMovieList();
        ModelMap map = new ModelMap(movieList);//movieList가 "movieList"라는 이름으로 저장됨.
        return map;
    }

    여기서 View에 대한 정보를 명시적으로 리턴하지는 않았지만, 내부적으로 View 이름은 RequestToViewNameTranslator에 의해서 입력된 HTTP Request를 이용하여 생성된다. 예를 들어 DefaultRequestToViewNameTranslator 는 입력된 HTTP Request URI를 변환하여 View 이름을 다음과 같이 생성한다.

    http://localhost:8080/anyframe-sample/display.do
        -> 생성된 View 이름 : 'display'
    http://localhost:8080/anyframe-sample/admin/index.do 
        -> 생성된 View 이름 : 'admin/index'

    위와 같이 자동으로 생성되는 View 이름에 'jsp/'와 같이 prefix를 붙이거나 '.jsp' 같은 확장자를 덧붙이고자 할 때는 아래와 같이 속정 정의 XML(xxx-servlet.xml)에 추가하면 된다.

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

  • Model

    Web View로 전달할 데이터만 리턴하는 경우 Model 은 Java-5 이상에서 사용할 수 있는 인터페이스이다. 기본적으로 ModelMap과 같은 기능을 제공한다. Model 인터페이스의 구현클래스에는 BindingAwareModelMap 와 ExtendedModelMap 이 있다. View 이름은 위에서 설명한 바와 같이 RequestToViewNameTranslator에 의해 내부적으로 생성된다.

    @RequestMapping("/movieList.do")
    public Model getMovieList() {
        List movieList = movieService.getMovieList();
        ExtendedModelMap map = new ExtendedModelMap();
        map.addAttribute("movieList",movieList);
        return map;
    }

  • String

    View 이름만 리턴하는 경우.

    @RequestMapping(value = {"/addMovie.do", "/updateMovie.do" })
    public String updateMovie(Movie movie, SessionStatus status) 
            throws Exception {
        // 중략
        return"/listMovie.do";
    }

  • void

    메소드 내부에서 직접 HTTP Response를 직접 처리하는 경우. 또는 View 이름이 RequestToViewNameTranslator에 의해 내부적으로 생성되는 경우

     @RequestMapping("/addView.do")
    public void addView(HttpServletResponse response) {
        // 중략
        //response 직접 처리
    }
     @RequestMapping("/addView.do")
    public void addView() {
        // 중략
        // View 이름이 DefaultRequestToViewNameTranslator에 의해서 내부적으로 'addView'로 결정됨.
    }

  • @ResponseBody

    핸들러 메소드의 리턴 객체를 Response HTTP Body로 바로 보내는 경우. HttpMessageConverter를 통해서 리턴 객체가 변환되어 Response로 전달됨.

    @RequestMapping(value = "/welcome", method = RequestMethod.GET)
    @ResponseBody
    public String welcome() {
        return "Welcome!";
    }

  • HttpEntity<?> 또는 ResponseEntity<?>

    Response HTTP의 Body와 Header를 핸들러 메소드에서 접근하기 위해 사용 가능. HttpEntity나 ResponseEntity의 Body는 HttpMessageConverter를 통해 response 스트림으로 변환됨.

    @RequestMapping(value = "/welcome", method = RequestMethod.GET)
    @ResponseBody
    public String welcome() {
        return "Welcome!";
    }

11.2.3.@RequestParam

@RequestParam annotation은 HTTP Request parameter를 컨트롤러 메소드의 argument로 바인딩하는데 사용되며 ServletRequest.getParameter(java.lang.String name) 와 같은 역할을 한다. 다음은 @RequestParam annotation의 사용 예이다.

@RequestMapping("/updateMovie.do")
public String updateMovie(@RequestParam("movieId") String movieId,         
        @RequestParam("sellAmount") int sellAmount, @RequestParam("realImageFile") MultipartFile picturefile) {
    // 중략
    return "/listMovie.do";
}

@RequestParam을 적용한 파라미터는 반드시 HTTP Request에 존재해야 한다. 그렇지 않은 경우 다음과 같이 org.springframework.web.bind.MissingServletRequestParameterException이 발생한다.

 org.springframework.web.bind.MissingServletRequestParameterException:
				
Required java.lang.String parameter 'movieId' is not present

그러나 아래와 같이 @RequestParam의 required 속성을 false로 설정할 경우 HTTP Request에 파라미터가 존재하지 않아도 Exception이 발생하지 않는다.

@RequestMapping("/deleteMovie.do")
public String deleteMovie(@RequestParam(value="movieId", required="false") String movieId){
    // 중략
}

또한 defaultValue 속성을 이용하여 해당 파라미터가 존재하지 않을 경우 사용할 디폴트 값을 정의할 수 있다.

@RequestMapping("/movies.do")
public String findMovies(@RequestParam(value="pageIndex", defaultValue = "1") int pageIndex,
            Movies movies, BindingResult result, Model model) {
    // 중략
}

11.2.4.@RequestBody

@RequestBody annotation은 HTTP Request Body를 컨트롤러 메소드의 argument로 바인딩하는데 사용된다. 다음은 @RequestBody annotation의 사용 예이다.

@RequestMapping(value = "/movies/add", method = RequestMethod.POST)
@ResponseBody
public String add(@RequestBody Movie movie) throws Exception {
    // 중략
}

Request Body의 내용을 메소드의 argument 객체로 전달하기 위해서는 HttpMessageConverter에 의해서 변환이 이루어져야만 한다. HttpMessageConverter는 HTTP Request body와 객체간, 그리고 객체와 HTTP Response body간의 변환을 담당한다. Spring 3 부터 AnnotationMethodHandlerAdapter가 @RequestBody를 지원하고, 다음의 HttpMessageConverter 들을 디폴트로 등록하도록 기능이 확장되었다.

  • ByteArrayHttpMessageConverter : byte 배열로 변환

  • StringHttpMessageConverter : String으로 변환

  • FormHttpMessageConverter : Form 데이터와 MultiValueMap<String, String> 간의 변환

  • SourceHttpMessageConverter : javax.xml.transform.Source로 변환

  • MarshallingHttpMessageConverter : org.springframework.oxm 패키지에서 제공하는 Marshaller와 Unmarshaller를 사용하여 객체와 XML간 변환

  • MappingJacksonHttpMessageConverter : Jackson 라이브러리의 ObjectMapper를 사용해서 객체와 JSON 간의 변환

위와 같은 MessageConverter들이 어플리케이션에서 사용되려면 AnnotationMethodHandlerAdapter에 설정되어 있어야한다. AnnotationMethodHandlerAdapter에 "messageConverters" 속성을 이용하여 설정할 수도 있지만, 앞서 언급했던 <mvc:annotation-driven />만 정의하면 디폴트로 자동으로 등록해준다. MessageConverter에 대한 더 자세한 내용은 본 매뉴얼 >> Restweb Plugin >> HTTP Message Conversion을 참고하기 바란다.

11.2.5.@ResponseBody

@ResponseBody annotation은 핸들러 메소드가 리턴 값을 HTTP Response를 통해서 바로 전달할 경우에 사용할 수 있다. @ResponseBody가 적용되면 Model과 View를 리턴하여 ViewResolver를 통해 View를 찾는 등의 과정들은 거치지 않게 된다.

다음은 @ResponseBody annotation의 사용 예이다.

@RequestMapping(value = "/welcome", method = RequestMethod.GET)
@ResponseBody
public String welcome() {
    return "Welcome!";
}

위에서 설명했던 @RequestBody에서와 같이, 핸들러 메소드의 리턴값은 HttpMessageConverter를 통해 HTTP Response Body로 변환된다.

11.2.6.HttpEntity<?>

HttpEntity는 @RequestBody/@ResponseBody 같이 Request/Response Body 메세지를 처리할 수 있을 뿐만아니라, HTTP Header 값도 함께 다룰 수 있다. 일반적으로 RestTemplate을 사용한 REST 클라이언트를 구현할 때, 편리하게 사용될 수 있다.

@RequestMapping("/handle")
public HttpEntity<String> handle() {
    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("MyResponseHeader", "MyValue");
    return new ResponseEntity<String>("Hello World", responseHeaders);
}

HttpEntity를 사용할 경우에도 역시 Request/Response Body 변환을 위해 HttpMessageConverter가 사용된다.

11.2.7.@ModelAttribute

@ModelAttribute는 컨트롤러에서 다음과 같이 두 가지 방법으로 사용할 수 있다.

  • 메소드 자체에 정의

    입력 폼 페이지에서 출력해 줄 reference data를 전달하고자 할 때. 기존 SimpleFormController의 referenceData() 메소드와 같은 역할

  • 메소드의 입력 argument에 정의

    메소드의 argument로 입력된 Command 객체에 이름을 부여하고자 할 때.

다음은 위에서 설명한 두가지 방법으로 @ModelAttribute를 사용한 예이다.

@Controller
@RequestMapping("/movie.do") 
public class MovieController {
    // 중략
    // 메소드 자체에 정의
    @ModelAttribute("genreList")
	public Collection<Genre> populateGenreList() throws Exception {
		return this.genreService.getDropDownGenreList();
	}

    // 메소드의 입력 argument에 정의
    @RequestMapping(params="method=add")
    public String add(@ModelAttribute("updatedMovie") Movie movie
                                , BindingResult result, SessionStatus status) throws Exception {
        // 중략
    }
}

11.2.8.@SessionAttributes

@SessionAttributes는 Session에 저장하여 관리할 Model Attribute를 정의할 때 사용한다. Session에 저장하고자 하는 Model Attribute의 이름이나 타입을 @SessionAttributes의 속성에 정의해준다.

다음은 @SessionAttributes를 사용하여 Session에 저장하여 관리할 Model을 이름으로 정의한 예이다. 타입으로 정의할 경우 'types'라는 속성을 이용한다.

@Controller
@RequestMapping("/movie.do")
@SessionAttributes(value={"movie","genre"})
public class MovieController {
	// 중략
}

11.2.9.@CookieValue

HTTP Cookie에 저장된 값을 핸들러 메소드에서 사용할 수 있도록 해주는 Annotation이다.

다음은 @CookieValue 사용하여 Cookie 값을 가져와 출력해보는 코드이다.

@RequestMapping("/displayHeaderInfo")
@ResponseBody
public String displayHeaderInfo(@CookieValue("JSESSIONID") String cookie,
		@RequestHeader("Accept-Encoding") String encoding,
		@RequestHeader("Accept") String accept) {
    StringBuffer sf = new StringBuffer();
    sf.append("JSESSIONID : " + cookie);
    sf.append("\n");
    sf.append("Accept-Encoding : " + encoding);
    sf.append("\n");
    sf.append("Accept : " + accept);
     
    return sf.toString();
}

11.2.10.@RequestHeader

HTTP Header에 저장된 값을 핸들러 메소드에서 사용할 수 있도록 해주는 Annotation이다.

위 @CookieValue 예제 코드에서 @RequestHeader가 사용된 모습을 확인할 수 있다.

11.3.Double Form Submission 방지

입력 폼 페이지에서 사용자가 새로 고침 버튼을 클릭하거나, 폼을 Submit하는 버튼을 여러번 클릭할 경우 같은 입력 폼 정보가 서버로 여러번 등록되는 문제가 발생할 수 있다. 이 장에서는 이러한 Double Form Submission을 어떻게 방지할 수 있는지를 알아보도록 하자.

Double Form Submission 방지는 다음과 같은 원리로 구현된다.

  • 반드시 AnnotationMethodHandlerAdapter의 synchronizeOnSession 속성을 true로 설정

    <bean id="annotationHandlerAdaptor"
            class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        property name="synchronizeOnSession" 
        value="true" /
    </bean>

  • Double submission을 방지하고자 하는 Form 객체를 model로 저장

    다음 예제와 같이 ModelAndView, ModelMap 등을 이용하여 저장한다.

    @RequestMapping(params = "param=addView")
    public ModelAndView addMovieView() {
        ModelAndView mnv = 
            new ModelAndView("/WEB-INF/jsp/annotation/sales/movie/movieForm.jsp");
        mnv.addObject("movie", new Movie());
        return mnv;
    }

  • 저장한 model을 @SessionAttributes로 정의

    다음 예제와 같이 컨트롤러 클래스 선언부에 @SessionAttributes("movie")로 정의한다.

    @Controller
    @RequestMapping("/movie.do")
    @SessionAttributes("movie")
    public class EditMovieController {
        // 중략
    }

  • 컨트롤러 메소드에서 폼 처리 완료 후 Session status 변경

    @RequestMapping(params = "param=add")
    public String addMovie(HttpServletRequest request, Movie movie, BindingResult result
            , SessionStatus status) throws Exception {
        movieService.addMovie(movie);
        status.setComplete();
        return "/listMovie.do";    
    }
  • status.setComplete()는 session에서 저장된 model을 삭제하는 이벤트 발생

  • 따라서, 이후에 다시 submit 요청이 온 경우 session에 저장된 model이 삭제되었기 때문에 아래와 같이 org.springframework.web.HttpSessionRequiredException발생

    org.springframework.web.HttpSessionRequiredException: 
           Session attribute 'dept' required - not found in session


출처 - http://dev.anyframejava.org/docs/anyframe/plugin/foundation/4.6.0/reference/html/ch11.html









'Framework & Platform > Spring' 카테고리의 다른 글

spring - annotation 1  (0) 2012.05.09
Sping MVC - Model  (0) 2012.05.09
Spring - @RequestMapping  (0) 2012.05.09
Spring MVC Deprecated API  (0) 2012.05.08
Spring MVC - MultiActionController 사용하기  (0) 2012.05.08
Posted by linuxism
,