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를 정의

@Controller

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

다음은 @Controller를 사용하여 작성한 EditUserController 클래스 파일의 일부다.
@Controller
@RequestMapping("/user.do")
public class EditUserController {
    ...중략...
}

@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("/userList.do")
public class UserController {
    ... 중략 ...    
    @RequestMapping
    public Model getUserList() {
        List userList = userService.getUserList();
        ExtendedModelMap map = new ExtendedModelMap();
        map.addAttribute("userList", userList);
        return map;
    }
}
@RequestMapping은 구현하는 컨트롤러 종류에 따라 아래와 같은 방식으로 사용할 수 있다.
  • Form Controller 구현
  • Multi-action Controller 구현
기존에 SimpleFormController와 같은 Controller 클래스를 상속받아서 컨트롤러를 작성할 때는, 상위클래스에 정의된 메소드를 override하여 구현하기 때문에 입력 argument 타입과 return 타입이 이미 정해져있다. 이에 반해 @RequestMapping을 적용하여 작성하는 핸들러 메소드는 다양한 argument 타입과 return 타입을 사용할 수 있다.
  • Supported argument types
  • Supported return types

Form Controller 구현

  • 클래스 선언에 @RequestMapping을 사용하여 Request URL의 Mapping
  • 메소드에는 @RequestMapping의 'method', 'params'와 같은 상세 속성 정보를 정의하여 Request URL의 Mapping을 세분화
위와 같이 작성하면 기존에 SimpleFormController를 상속받아 작성하였던 폼을 처리하는 컨트롤러를 구현할 수 있다. 다음은 폼 처리 컨트롤러를 작성한 EditUserController 의 예이다.
@Controller
@RequestMapping("/user.do")
public class EditUserController {
	
    @RequestMapping(method = RequestMethod.GET)
    public ModelAndView addUserView() {
        ... 중략 ...
        return mnv;
    }

    @RequestMapping(method = RequestMethod.POST)
    public String addUser(HttpServletRequest request, @ModelAttribute("user")
        User user, BindingResult result, SessionStatus status) throws Exception {
    	... 중략 ...
    	return "/userList.do";
    }
}

Multi-action Controller 구현

@RequestMapping annotation을 사용하여 여러 HTTP Request를 처리할 수 있는 Multi-action 컨트롤러를 구현할 수 있다.
  • 메소드에 Request URL을 Mapping한 @RequestMapping을 정의
다음은 Multi-action 컨트롤러를 구현한 DeptController 의 예이다.
@Controller
public class DeptController {

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

    @RequestMapping("/getDept.do")
    public String getDept(@RequestParam("deptId")
    String deptId, ModelMap model) {
        ...중략...
        return "/jsp/dept/getDept.jsp";
    }    
}
        
@RequestMapping annotation에는 다음과 같은 상세 속성 정보를 부여할 수 있다.
name
Description
value "value='/getUser.do'"와 같은 형식의 매핑 URI이다. 디폴트 속성이기 때문에 value만 정의하는 경우에는 'value='은 생략할 수 있다.
예 : @RequestMapping(value = {"/addDept.do", "/updateDept.do" })
위의 경우 "/addDept.do", "/updateDept.do" 두 URL 모두 처리한다.
method GET, 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 값을 상속받는다.

Supported argument types

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

  • ServletRequest 또는 HttpServletRequest 등을 메소드 내부에서 직접 사용해야 하는 경우
    @RequestMapping(params = "param=add")
    public String addUser(HttpServletRequest request, @ModelAttribute("user")
        User user, BindingResult result, SessionStatus status) throws Exception {
        ...중략...
        String message = messageSource.getMessage(
        			"user.error.exist", new String[] {user.getUserId() },
        			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 addUser(Locale locale, @ModelAttribute("user")
        User user, BindingResult result, SessionStatus status) throws Exception {
        ...중략...
        String message = messageSource.getMessage(
        			"user.error.exist", new String[] {user.getUserId()}, locale);
       }
  • java.io.InputStream 또는 java.io.Reader

  • Request의 content를 직접 처리할 경우 (Servlet API가 제공하는 raw InputStream/Reader)
    @RequestMapping(params = "param=add")
    public String addUser(InputStream is, @ModelAttribute("user")
        User user, 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 addUser(OutputStream os, @ModelAttribute("user")
      User user, 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("/deleteDept.do")
    public String deleteDept(@RequestParam("deptId") String deptId) {
    	deptService.deleteDept(deptId);
    	return "/deptList.do";
    }
  • java.util.Map 또는 org.springframework.ui.Model 또는 org.springframework.ui.ModelMap

  • Web View로 데이터를 전달해야 하는 경우 위 타입의 argument를 정의하고, 메소드 내부에서 View로 전달할 데이터를 추가함
    @RequestMapping("/getDept.do")
    public String getDept(@RequestParam("deptId") String deptId, Map map) {
        Dept dept = deptService.getDept(deptId);
        
        // 여기서 dept는 "dept"라는 이름으로 @SessionAttributes를 통해 session에 저장되어 관리될 수 있다.
        map.put("dept", dept);
        
        return "/jsp/dept/getDept.jsp";
    }
    @RequestMapping("/getDept.do")
    public String getDept(@RequestParam("deptId") String deptId, Model model) {
        Dept dept = deptService.getDept(deptId);
        
        // 여기서 dept는 "dept"라는 이름으로 @SessionAttributes를 통해 session에 저장되어 관리될 수 있다.
        model.addAttribute("dept", dept);
        
        return "/jsp/dept/getDept.jsp";
    }
    @RequestMapping("/getUser.do")
    public String getUser(@RequestParam("userId") String userId, ModelMap model) {
        User user = userService.getUser(userId);
        
        // 여기서 user는 "user"라는 이름으로 @SessionAttributes를 통해 session에 저장되어 관리될 수 있다.
        model.addAttribute("user", user);
        return "/jsp/user/getUser.jsp";
    }
  • Command 또는 form 객체

  • HTTP Request로 전달된 parameter를 binding한 객체, model attribute로 다음 View에서 사용 가능
    @SessionAttributes를 통해 session에 저장되어 관리될 수 있다.
    @ModelAttribute annotation을 적용하면 사용자 임의로 이름을 부여할 수 있다.
    @RequestMapping("/addDept.do")
    public String updateDept(Dept dept, SessionStatus status) throws Exception {
    	...메소드 내부에서 dept 객체에 대한 처리를 하고, 
    	   "dept"라는 이름의 model attribute로 다음 View에 전달됨....
    	return "/deptList.do";
    }
    @RequestMapping("/addDept.do")
    public String updateDept(@ModelAttribute("updatedDept") Dept dept, 
    	SessionStatus status) throws Exception {
    	...메소드 내부에서 dept 객체에 대한 처리를 하고, 
    	   "updatedDept"라는 이름의 model attribute로 다음 View에 전달됨....
    	return "/deptList.do";
    }
  • org.springframework.validation.Errors 또는 org.springframework.validation.BindingResult

  • 바로 이전의 입력파라미터인 Command 또는 form 객체의 validation 결과 값을 저장하는 객체로 해당 command 또는 form 객체 바로 다음에 위치해야 함에 유의하도록 한다.
    @RequestMapping(params = "param=add")
    public String addUser(HttpServletRequest request, @ModelAttribute("user")
    	User user, BindingResult result, SessionStatus status) throws Exception {
    		
    	new UserValidator().validate(user, result);
    	if (result.hasErrors()) {
    		return "/jsp/user/userForm.jsp";
    	} else {
    		...중략...
    		return "/userList.do";
    	}
    }
  • org.springframework.web.bind.support.SessionStatus

  • 폼 처리가 완료되었을 때 status를 처리하기 위해서 argument로 설정.
    SessionStatus.complete()를 호출하면 컨트롤러 클래스에 @SessionAttributes로 정의된 Model객체를 session에서 지우도록 이벤트를 발생시킨다.
    @RequestMapping(params = "param=add")
    public String addUser(HttpServletRequest request, @ModelAttribute("user")
    	User user, BindingResult result, SessionStatus status) {
    	...중략...
    	userService.addUser(user);
    	status.setComplete();
    	return "/userList.do";
    }

Supported return types

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

  • View와 Model 정보를 모두 포함한 객체를 리턴하는 경우.
    여기서 Model은 argument로 입력된 command객체와 @ModelAttribute annotation이 적용된 메소드(예제 : EditUserController 의 populateDeptList())의 결과 값을 포함한다.
    @RequestMapping(params = "param=addView")
    public ModelAndView addUserView() {
        ModelAndView mnv = new ModelAndView("/jsp/user/userForm.jsp");
        mnv.addObject("user", new User());
        return mnv;
    }
  • Map

  • Web View로 전달할 데이터만 리턴하는 경우.
    내부적으로 argument로 입력된 command객체와 @ModelAttribute annotation이 적용된 메소드(예제 : EditUserController 의 populateDeptList())의 결과 값이 함께 전달된다.
    @RequestMapping("/userList.do")
    public Map getUserList() {
        List userList = userService.getUserList();
        ModelMap map = new ModelMap(userList);//userList가 "userList"라는 이름으로 저장됨.
        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 인터페이스의 구현클래스에는 BindingAwareModelMapExtendedModelMap 이 있다.
    내부적으로 argument로 입력된 command객체와 @ModelAttribute annotation이 적용된 메소드(예제 : EditUserController 의 populateDeptList())의 결과 값이 함께 전달된다.
    View name은 위에서 설명한 바와 같이 RequestToViewNameTranslator에 의해 내부적으로 생성된다.
    @RequestMapping("/userList.do")
    public Model getUserList() {
        List userList = userService.getUserList();
        ExtendedModelMap map = new ExtendedModelMap();
        map.addAttribute("userList", userList);
        
        return map;
    }
  • String

  • View name만 리턴하는 경우.
    내부적으로 argument로 입력된 command객체와 @ModelAttribute annotation이 적용된 메소드(예제 : EditUserController 의 populateDeptList())의 결과 값이 함께 전달된다.
    @RequestMapping(value = {"/addDept.do", "/updateDept.do" })
    public String updateDept(Dept dept, SessionStatus status) throws Exception {
    
        ...중략 ...
        
        return "/deptList.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'로 결정됨.
    }

@RequestParam

@RequestParam annotation은 HTTP Request parameter를 컨트롤러 메소드의 argument로 바인딩하는데 사용되며 ServletRequest.getParameter(java.lang.String name) 와 같은 역할을 한다.
다음은 @RequestParam annotation의 사용 예이다.
@RequestMapping("/updateUser.do")
public String updateUser(@RequestParam("userId") String userId,
    @RequestParam("userGrade") int grade, 
    @RequestParam("realImageFile") MultipartFile picturefile) {
    ...중략...
    return "/userList.do";
}
@RequestParam을 적용한 파라미터는 반드시 HTTP Request에 존재해야 한다. 그렇지 않은 경우 다음과 같이 org.springframework.web.bind.MissingServletRequestParameterException이 발생한다.
org.springframework.web.bind.MissingServletRequestParameterException:
                                       Required java.lang.String parameter 'deptId' is not present
그러나 아래와 같이 @RequestParam의 required 속성을 false로 설정할 경우 HTTP Request에 파라미터가 존재하지 않아도 Exception이 발생하지 않는다.
@RequestMapping("/deleteUser.do")
public String deleteUser(@RequestParam(value="userId", required="false")String userId){
	...중략...
}

@ModelAttribute

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

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

  • 컨트롤러 메소드의 입력 argument에 정의

  • 메소드의 argument로 입력된 Command 객체에 이름을 부여하고자 할 때.
다음은 위에서 설명한 두가지 방법으로 @ModelAttribute를 사용한 EditUserController 의 예이다.
@Controller
@RequestMapping("/user.do")
public class EditUserController {
    // 컨트롤러 메소드에 정의
    @ModelAttribute("deptList")
    public List populateDeptList() throws Exception {
        return deptService.getDeptList();
    }
    
    @RequestMapping(params = "param=add")
    public String addUser(@ModelAttribute("user")
        User user, BindingResult result, SessionStatus status) throws Exception {
        ...중략...
    }
}

@SessionAttributes

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

다음은 @SessionAttributes를 사용하여 session에 저장하여 관리할 model을 정의한 예이다.
@Controller
@RequestMapping("/user.do")
@SessionAttributes(value = {"user", "dept"})
public class EditUserController {
	...중략...
}

Resources