목차

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들을 사용하면 여러 정보들(요청과 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의 메소드 파라미터

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

메소드는 임의의 순서대로 파라미터를 사용할수 있다. 단, 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의 메소드 리턴 타입

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

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가지 메소드로 이루어졌다.

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과 동일한 메소드 이름을 선택했다.

(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";
		}
	}
}

참고자료