@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가 결정한다.


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


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


스프링 2.x 에서는 AbstractController 를 상속받아 Controller를 구현하였으나 스프링 3.0부터는 Annotation(어노테이션)

이용하여 간단하게 Controller를 구현할 수 있다.

 

1. 기본 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class HomeController {
    // "/home.do" 경로를 입력하면 이 메소드로 진입한다.
    @RequestMapping("/home.do")
    public ModelAndView home() {
  
        // 클라이언트에게 보여줄 jsp 페이지를 설정한다.
        ModelAndView mav = new ModelAndView("home");        // home.jsp
        mav.addObject("hello", "hello Spring!");            // key & value
         
        return mav;
    }
 
    @RequestMapping("/room.do")
    public String room() {
        return "room";          // room.jsp
    }
}


@RequestMapping 어노테이션의 값에는 해당 메소드가 처리할 URL을 정의한다.

두번째 메소드의 리턴 타입은 String인데 이것은 viewName을 의미한다.

* 리턴 타입의 종류 

 

2. GET/POST 에 따른 처리 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class NewArticleController {
 
    @RequestMapping(value = "/article/newArticle.do", method = RequestMethod.GET)
    public String form() {
        return "article/newArticleForm";
    }
 
    @RequestMapping(value = "/article/newArticle.do", method = RequestMethod.POST)
    public String submit(@ModelAttribute("command") NewArticleCommand command) {
        return "article/newArticleSubmitted";
    }
 
    public void setArticleService(ArticleService articleService) {
        this.articleService = articleService;
    }
}

만약 위처럼 동일한 URL에 대해서 처리한다고 하면 아래처럼 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
@RequestMapping("/article/newArticle.do")
public class NewArticleController {
      
    @RequestMapping(method = RequestMethod.GET)
    public String form() {
        ...
    }
 
    @RequestMapping(method = RequestMethod.POST)
    public String submit(@ModelAttribute("command") NewArticleCommand command) {
        ...
    }
    ...
}

만약 @RequestMapping 에 method 설정을 하지 않으면 GET/POST 등 모든 HTTP 전송방식을 처리한다.



3. Controller 자동스캔
@Controller 은 @Component 와 마찬가지로 컴포넌트 스캔 대상이다.
<context:component-scan> 태그를 이용해서 아래처럼 선언해서 어노테이션이 적용된 컨트롤러를 자동으로 로딩할 수 있다.

1
<context:component-scan base-package="패키지네임" />


출처 - http://warmz.tistory.com/697


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


3.0 에서는 Controller 어노테이션을 이용해서 컨트롤러 클래스를 구현한단다. (과거에는 AbstractController 등을 썼음)

1. 기본구현
@Controller , @RequestMapping 어노테이션을 이용하면된다.

@Controller
public class HelloController {
@RequestMapping("/hello.do")
public String hello() {
            return "hello";
        }


@RequestMapping 어노테이션의 값에는 해당 메소드에서 처리할 URI가 무엇인지를 정의한다.
위의 경우 리턴값이 ModelAndView 가 아니라 String인데 이는 바로 이값이 ViewName이 된다.
이 뷰이름으로 ViewResolver 를 통해서 실제 view 파일을 가져와서 보여주겠지..


2. 전송방식... GET/POST
어차피... 이건 웹 어플리케이션이다. 전송하는 방식은 GET/POST 겠지.
같은 URL 이고 get/ post 의 다른 방식의 전송을 할때 다르게 처리가능하다.

@Controller
public class NewArticleController {
 
@RequestMapping(value="/article/newArticle.do", method = RequestMethod.GET)
public String form() {
return "article/newArticleForm";
}

@RequestMapping(value="/article/newArticle.do", method = RequestMethod.POST)
public String submit(@ModelAttribute("command") NewArticleCommand command) {
return "article/newArticleSubmitted";
}

public void setArticleService(ArticleService articleService) {
this.articleService = articleService;
}

}


만약 위처럼 처리하는 메소드가 동일한 URI에 대해서 처리하는것이라면 기본 URI를 지정할 수 도 있다.


@Controller
@RequestMapping("/article/newArticle.do")
public class NewArticleController {
 
@RequestMapping(method = RequestMethod.GET)
public String form() {
...
}

@RequestMapping(method = RequestMethod.POST)
public String submit(@ModelAttribute("command") NewArticleCommand command) {
...
}
...
}


만약 RequestMapping 어노테이션에 method 설정은 안하면 ,GET/POST 등 모든 HTTP 전송방식을 처리한다.



RequestMapping 에 대한 어노테이션 기능은 별도의 문서에 정리했다. 참고바란다.

3. 컨트롤러 메소드의 리턴타입?

매핑된 컨트롤러의 메소드가 해야할 일을 처리한다음... 뭔가를 리턴할것이다.
그 타입에 대한 설명이다.
@RequestMapping 메소드의 리턴타입

리턴타입 설명
ModelAndView 뷰 정보 및 모델정보를 담고 있는 ModelAndView 객체
Model 뷰에 전달할 객체 정보를 담고 있는 Model을 리턴한다. 이때 뷰 이름은 요청 URL로부터 결정된다.(RequestToViewNameTranslator 를 통해 뷰 결정)
 Map 뷰에 전달할 객체 정보를 담고 있는 Model을 리턴한다. 이때 뷰 이름은 요청 URL로부터 결정된다.(RequestToViewNameTranslator 를 통해 뷰 결정)
 String 뷰 이름을 리턴한다.
 View 객체View 객체를 직접 리턴, 해당 View 객체를 이용해서 뷰를 생성한다. 
 void메소드가 ServletResponse나, HttpServletResponse 타입의 파라미터를 갖는 경우 메소드가 직접 응답을 처리한다고 가정한다. 그렇지 않을 경우 요청 URL로 부터 결정된 뷰를 보여준다.(RequestToViewNameTranslator를 통해 뷰 결정) 
 @ResponseBody 어노테이션적용
메소드에서 @ResponseBody 어노테이션이 적용된 경우, 리턴객체를 HTTP응답으로 전송한다. HttpMessageConverter를 이용해서 객체를 HTTP 응답 스트림으로 변환한다. 


4. 컨트롤러 클래스 자동스캔
@Controller 어노테이션은 @Component 어노테이션과 마찬가지로 컴포넌트 스캔대상이다. 
<context:component-scan> 태그를 이용해서 아래처럼 선언해서 (xml) 어노테이션이 적용된 컨트롤러를 자동으로 로딩할 수 있다.

<context:component-scan base-package="madvirus.spring.chap06.controller" />


출처 - http://ezsnote.tistory.com/entry/Controller-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98

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


@RequestMapping 어노테이션은 컨트롤러가 처리할 요청URL을 명시하는데 사용되며, 클래스나 메서드에 적용될 수 있다.


첫째로, @RequestMapping 어노테이션을 클래스에는 적용하지 않고 메서드에만 적용할 경우에는 


각각의 메서드가 처리할 요청 URL을 명시하게 된다.


import! org.springframework.stereotype.Controller;

import! org.springframework.ui.ModelMap;

import! org.springframework.web.bind.annotation.RequestMapping;

import! org.springframework.web.bind.annotation.ReqeustParam;


@Controller

pubilc class SearchAnnotController {

@RequestMapping("/annot/search/internal.do")

public String searchInternal(@RequestParam("query") String query, 

@RequestParam("pageNo") int pageNo, ModelMap modelMap) {

return search(internalSearchService, query, pageNo, modelMap);

}

@RequestMapping("/annot/search/external.do")

public String searchExternal(@RequestParam("query") String query, 

@RequestParam("pageNo") int pageNo, ModelMap modelMap) {

return search(externalSearchService, query, pageNo, modelMap);

}

}


다수의 메서드에 @RequestMapping 어노테이션을 적용하면, 

MultiActionController와 같이 한 개의 컨트롤러에서 다수의 요청을 처리할 수 있게 된다.


@RequestMapping 어노테이션의 method 엘리먼트를 이용하여 처리할 수 있는 HTTP METHOD 목록을 지정할 수도 있다.

@RequestMapping(value="/anno/search/external.do", method=RequestMethod.POST)

public String searchExternal(...) {

....

}

@RequestMapping 어노테이션을 클래스 타입에 적용하게 되면, 해당 컨트롤러 클래스는 지정한 URL만을 처리할 수 있게 된다.

이 경우 메서드에 적용되는 @RequestMapping 어노테이션은 더 이상 URL을 명시할 수 없으며, 

method엘리먼트와 params 엘리먼트만을 지정할 수 있게 된다.


예를 들어, SimpleFormController처럼 GET 요청이 들어오면 입력 폼을 출력하고 POST 요청이 들어오면 폼 전송을 처리하고 싶다면,

다음과 같이 클래스 타입에는 Mapping URL을 명시한 @RequestMapping 어노테이션을 적용하고, 각 메서드에서는 method

엘리먼트를 명시한 @RequestMapping 어노테이션을 적용하면 된다.

import! org.springframework.web.bind.annotation.RequestMapping;

import! org.springframework.web.bind.annotation.ReqeustMethod;


@Controller

@RequestMapping("/annot/login.do")

public class LoginAnnotController {
...

@RequestMapping(method = RequestMethod.GET)

public String setupForm(ModelMap map) {

...

}

@RequestMapping(method = RequestMethod.POST)

public String processSubmit(@ModelAttribute("login")

LoginCommand loginCommand, BindingResult erros, ModelMap model) {

...

}

}


출처 - http://blog.daum.net/_blog/BlogTypeView.do?blogid=0Ps1S&articleno=6&_bloghome_menu=recenttext#ajax_history_home


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


@MVC 의 가장 큰 특징은 핸들러 매핑과 핸들러 어댑터의 대상이 오브젝트가 아니라 메소드라는 점이다. Controller 와 같이 하나의 메소드를 가진 인터페이스로 정의되는 컨트롤러는 특별한 방법을 사용하지 않는 한, URL 당 하나의 컨트롤러 오브젝트가 매핑되고 컨트롤러당 하나의 메소드만 DispatcherServlet 의 호출을 받을 수 있다. 반면에 @MVC 에서는 모든 것이 메소드 레벨로 세분화 됐다. 메소드 레벨에서 컨트롤러를 작성할 수 있게 된 기술적인 배경에는 애노테이션을 이용한 프로그래밍 모델이 있다. 애노테이션은 타입 레벨뿐 아니라 메소드 레벨에도 적용이 가능하기 때문이다.

@MVC 의 핸들러 매핑을 위해서는 DefaultAnnotationHandlerMapping 이 필요하다 DefaultAnnotationHandlerMapping 은 디폴트 핸들러 매핑전략이므로 다른 핸들러 매핑 빈을 명시적으로 등록하지 않았다면 기본적으로 사용할 수 있다. 다른 핸들러 매핑 빈을 등록했을 경우에는 디폴트 핸들러 매핑전략이 적용되지 않으므로 DefaultAnnotationHandlerMapping 도 함께 빈으로 등록해줘야 한다.

클래스/메소드 결합 매핑정보

DefaultAnnotationHandlerMapping 의 핵심은 매핑정보로 @RequestMapping 애노테이션을 활용한다는 점이다. 그런데 @RequestMapping 은 타입 레벨뿐 아니라 메소드 레벨도 붙일 수 있다. 스프링은 이 두 가지 위치에 붙은 @RequestMapping 의 정보를 결합해서 최종 매핑정보를 생성한다. 기본적인 결합 방법은 타입 레벨의 @RequestMapping 정보를 기준으로 삼고, 메소드 레벨의 @RequestMapping 정보는 타입 레벨의 매핑을 더 세분화하는 데 사용하는 것이다. 타입 레벨의 @RequestMapping 이 메소드 레벨 @RequestMapping 의 공통 정보라고 볼 수 있다.

1. @RequestMapping 애노테이션

@RequestMapping 애노테이션에는 다음과 같은 엘리먼트를 지정할 수 있다. 모든 엘리먼트는 생략 가능하다.

String[] value(): URL 패턴

디폴트 엘리먼트인 value 는 스트링 배열 타입으로 URL 패턴을 지정하도록 되어 있다. 대부분의 핸들러 매핑은 요청정보 중에서 URL 만을 사용한다. URL은 가장 기본이 되는 매핑정보다. URL 에 부가적인 조건을 더해서 세분화된 매핑을 할 수 있다. URL 패턴은 다른 핸들러 매핑에서 사용하는 URL 패턴처럼 ANT 스타일의 와일드 카드를 사용할 수 있다. 예를 들면 다음과 같은 URL 패턴을 사용할 수 있다.

1
2
3
4
@RequestMapping("/hello")
@RequestMapping("/main*")
@RequestMapping("/view.*")
@RequestMapping("/admin/**/user")

와일드카드(*)를 사용한 부분은 타입과 메소드의 매핑정보 결합을 통해 좀 더 구체적으로 만들 수 있다.

@RequestMapping 에는 다음과 같이 {} 를 사용하는 URI 템플릿을 사용할 수도 있다. 이 때 {} 위치에 해당하는 내용을 컨트롤러 메소드에서 파라미터로 전달받을 수 있다. {}에 들어가는 이름을 패스변수(Path Variable)라고 부르며, 하나 이상 등록할 수 있다.

1
@RequestMapping("/user/{userid}")

URL 패턴은 배열이다. 따라서 다음과 같이 하나 이상의 URL 패턴을 지정할 수 있다.

1
@RequestMapping({"/hello", "/hi"})

URL 패턴에서 기억해야 할 중요한 사실은 디폴트 접미어 패턴이 적용된다는 점이다. 예를 들어 다음과 같이 @RequestMapping을 적용했다고 하자.

1
@RequestMapping("/hello")

이렇게 확장자가 붙지 않고 /로 끝나지도 않은 URL 패턴에는 디폴트 접미어 패턴이 적용돼서 실제로는 다음 세개의 URL 패턴을 적용했을 때와 동일한 결과가 나온다.

1
@RequestMapping({"/hello","/hello/","/hello.*"})

따라서 /hello 라고 정의한 것만으로 /hello.do, /hello.html 과 같이 확장자가 붙은 URL 이나, /hello/ 처럼 /로 끝나는 URL도 자동으로 매핑된다.

RequestMethod[] method() : HTTP 요청 메소드

RequestMethod 는 HTTP 메소드를 정의한 이늉이다. GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE 7개의 HTTP 메소드가 정의되어 있다. @RequestMapping 에 HTTP 요청 메소드를 추가해 주면 같은 URL 이라고 하더라도 요청 메소드에 따라 다른 메소드를 매핑해줄 수 있다. 예를 들어 다음과 같이 GET 과 POST 를 구분할 수 있다.

1
2
@RequestMapping(value="/user/add", method=RequestMethod.GET)
@RequestMapping(value="/user/add", method=RequestMethod.POST)

HTTP 요청 메소드도 배열타입이므로 하나 이상을 지정할 수 있다. 때로는 타입 레벨에는 URL 만 주고 메소드 레벨에는 HTTP 요청 메소드만 지정하는 방법을 사용하기도 한다. 이럴 때는 다음과 같이 URL을 생략할 수도 있다.

1
@RequestMapping(method=RequestMethod.GET)

String[] params(): 요청 파라미터

같은 URL을 사용하더라도 HTTP 요청 파라미터에 따라 별도의 작업을 해주고 싶을 때가 있다. 이 때는 컨트롤러 코드에서 파라미터를 검사해서 기능을 분리하는 대신 @RequestMapping에 매핑을 위한 요청 파라미터를 지정해줄 수 있다. 파라미터는 '타입=값' 형식으로 매핑해주면 된다.

다음 두 가지 매핑 선언을 살펴보자

1
2
@RequestMapping(value="/user/edit", params="type=admin")
@RequestMapping(value="/user/edit", params="type=member")

두 개의 매핑 모두 /user/edit 이라는 URL 을 갖고 있지만 파라미터가 다르다. /user/edit?type=admin 이라는 URL로 요청을 받으면 첫 번째 매핑이 적용되고, /user/edit?type=member 라는 URL 이 들어 오면 두 번째 매핑이 적용된다.

여기에 다음과 같이 파라미터 없이 URL 만 적용된 @RequestMapping 이 하나 더 추가되어 있다고 생각해 보자.

1
@RequestMapping("/user/edit")

이 때 /user/edit?type=admin 이라는 URL로 요청을 보내면 어떤 매핑이 적용될 까? 매핑 기준으로 보자면 위의 첫 번째 매핑과 마지막에 추가한 URL 만 있는 매핑 조건을 동시에 만족한다. 이럴 때는 구분이 좀 더 상세한 쪽이 선택된다. 따라서 첫 번째 매핑에 해당된다. 이 원리는 모든 경우에 다 동일하게 적용된다. 매핑조건을 만족하는 경우가 여러 개 있을 때는 좀 더 많은 조건을 만족시키는 쪽이 우선된다. URL 에서도 더 긴 조건을 만족하는 쪽이 우선된다.

이렇게 세 개의 @RequestMapping 선언을 했을 경우에 type=admin, type=member 와 같은 파라미터를 갖지 않으면서 URL 이 /user/edit 인 요청은 모두 세번 째 @RequestMapping 으로 매핑된다. 이렇게 디폴트 매핑을 만들고 특별한 조건을 가진 경우만 별도의 매핑을 추가할 수가 있다.

params 에 지정한 파라미터는 꼭 URL 에 포함된 것만 비교하는 건 아니다. 폼에서 POST 로 전송한 폼 파라미터도 비교 대상이다.

특정 파라미터가 존재하지 않아야 한다는 조건을 지정할 수도 있다. 이때는 "!" 를 파라미터 이름 앞에 붙여주면 된다. 다음 선언은 type 이라는 파라미터가 아예 존재하지 않는 경우에만 매핑되도록 선언한 것이다.

1
@RequestMapping(value="/user/edit", params="!type")

params 도 배열도 선언되어 있으므로 하나 이상을 지정할 수 있다.

String[] headers : HTTP 헤더

마지막 매핑 항목은 HTTP 헤더 정보다. 지정 방식은 params 와 비슷하게 '헤더이름=값' 형식을 사용한다. 다음 매핑 선언은 헤더의 content-type 이 text/html, text/plain 등으로 되어 있는 경우에만 매핑해준다.

1
@RequestMapping(value="/view", headers="content-type=text/*")

2. 타입 레벨 매핑과 메소드 레벨 매핑의 결합

타입 레벨에 붙는 @RequestMapping 은 타입 내의 모든 매핑용 메소드의 공통 조건을 지정할 때 사용한다. 그리고 메소드 레벨에서 조건을 세분화해주면 된다. 메소드 레벨의 매핑은 타입 레벨의 매핑을 상속받는다고 보면 된다.

예를 들어 컨트롤러 메소드가 각각 /user/add, /user/edit, /user/delete 라는 URL에 매핑돼야 한다고 해보자. 이 때는 아래와 같이 타입레벨에는 세 URL 의 공통 부분인 /user 까지만 매핑해 주고 메소드 레벨에서 나머지 /add, /edit, /delete 를 선언해 주면 이 두가지 조건이 결합돼서 최종 URL 매핑 조건이 결정된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("/user")
public class UserController {
     
    @RequestMapping("/add")
    public String add(...) {}
     
    @RequestMapping("/edit")
    public String edit(...) {}
     
    @RequestMapping("/delete")
    public String delete(...) {}
 
}

타입 레벨의 URL 패턴에 * 나 ** 를 사용했을 때도 URL을 결합할 수 있다. 타입 레벨에 /user 대신 /user/* 를 사용했을 경우 메소드 레벨에 /add 가 선언되어 있으면 /user/add 로 결합된다. 타입레벨에 /user/** 로 되어 있다면 메소드 레벨의 /add 는 /user/**/add 로 결합된다. 

메소드 레벨의 시작 /를 생략할 수도 있다. 타입 레벨이 /user, /user/, /user/* 중 하나로 되어 있는 경우 메소드 레벨에 add 라는 URL 조건을 주면 모두 /user/add 로 매핑된다.

타입레벨과 메소드 레벨의 URL을 결합하는 대신 다음과 같이 URL은 타입레벨에서만 정의하고 메소드 레벨에서는 다른 매핑조건을 추가해 줄 수도 있다. 

1
2
3
4
5
6
7
8
9
10
@RequestMapping("/user/add")
public class UserController {
     
    @RequestMapping(method=RequestMethod.GET)
    public String form(...) {}
     
    @RequestMapping(method=RequestMethod.POST)
    public String submit(...) {}
 
}

3. 메소드레벨 단독 매핑

메소드 레벨의 매핑조건에 공통점이 없는 경우라면 타입 레벨에서는 조건을 주지 않고 메소드 레벨에서 독립적으로 매핑정보를 지정할 수도 있다. 이때 타입 레벨에는 조건이 없는 @RequestMapping을 붙여 두면 된다. 이마저 생략하면 아예 클래스 자체가 매핑 대상이 되지 않으니 내용이 없는 빈 @RequestMapping 이라도 꼭 부여해줘야 한다.

이방법을 이용하면 다음과 같이 메소드마다 다른 URL 로 선언할 수가 있다.

1
2
3
4
5
6
7
8
9
10
@RequestMapping
public class UserController {
     
    @RequestMapping("/hello")
    public String hello(...) {}
     
    @RequestMapping("/main)
    public String main(...) {}
     
}

컨트롤러 클래스에 @Controller 애노테이션을 붙여서 빈 자동스캔 방식으로 등록되게 했다면, 이때는 클래스 레벨의 @RequestMapping 을 생략할 수도 있다. 스프링이 @Controller 애노테이션을 보고 애노테이션 방식을 사용한 클래스라고 판단하기 때문이다. 클래스 레벨에 매핑조건을 주지 않는다면 아래와 같이 @RequestMapping을 생략할 수 있다.

1
2
3
4
5
6
7
8
9
10
@Controller
public class UserController {
     
    @RequestMapping("/hello")
    public String hello(...) {}
     
    @RequestMapping("/main)
    public String main(...) {}
     
}

4. 타입 레벨 단독 매핑

핸들러 매핑은 원래 핸들어 오브젝트를 결정하는 전략이다. 애노테이션의 영향으로 매핑 방법이 메소드 레벨까지 세분화되기는 했지만 다른 타입 컨트롤러와의 일관성을 위해 애노테이션 방식의 핸들러 매핑에서도 일단 오브젝트까지만 매핑하고, 최종 실행할 메소드는 핸들러 어댑터가 선정한다.

그렇기 때문에 @RequestMapping 을 타입 레벨에 단독으로 사용해서 다른 타입 컨트롤러를 위한 매핑을 위해 사용할 수도 있다. 예를 들면 아래와 같이 Controller 인터페이스를 구현한 컨트롤러 클래스에 @RequestMapping 을 붙여줄 수 있다. 원칙적으로 핸들로 매핑과 핸들러 어댑터는 독립적으로 조합될 수 있기 때문에 적용 가능한 방법이다.

1
2
3
4
5
6
@RequestMapping("/hello")
public class HelloController implements Controller {
     
 
     ...
}

다음과 같이 클래스 레벨의 URL 패턴이 /* 로 끝나는 경우에는 메소드 레벨의 URL 패턴으로 메소드 이름이 사용되게 할 수 있다. 아래와 같이 클래스 레벨에서는 /* 로 끝나게 하고 메소드 레벨에서는 빈 @RequestMapping 애노테이션만 부여해주면 메소드 이름이 URL 대신 적용돼서 각각 /user/add 와 /user/edit 으로 매핑된다.

1
2
3
4
5
6
7
8
9
@RequestMapping("/user/*")
public class UserController  {
     
     @RequestMapping
     public String add(...) {}
      
     @RequestMapping
     public String edit(...) {}
}

타입 상속과 매핑

@RequestMapping 이 적용된 클래스를 상속해서 컨트롤러로 사용하는 경우 슈퍼클래스의 매핑정보는 어떻게 될까? 상속과 관련해서는 기본 원칙 두 가지만 기억하고 있으면 된다. @RequestMapping 정보는 상속된다. 단, 서브 클래스에서 @RequestMapping 을 재정의하면 슈퍼클래스의 정보는 무시된다.

지금까지 타입 레벨의 @RequestMapping 을 설명하면서 클래스의 경우만 예로 들었다. 타입과 메소드 사이의 기본적인 원칙은 인터페이스에도 동일하게 적용된다. 인터페이스의 @RequestMapping 은 인터페이스를 구현한 클래스의 매핑정보로 사용된다. 같은 인터페이스 안에서 타입 레벨과 메소드 레벨 사이의 관계와 매핑조건 결합은 지금까지 설명한 내용이 모두 적용된다.

컨트롤러 클래스를 인터페이스를 구현해서 만드는 경우는 많지 않다. 특정 인터페이스의 구현조차 강요하지 않는 애노테이션 방식을 사용하면서 굳이 인터페이스를 사용할 이유는 없다. 컨트롤러를 다른 빈이 DI를 통해 참조하는 경우도 거의 없다. 하지만 스프링 MVC 를 기반으로 확장 MVC 프레임워크를 설계한다면 얼마든지 의미 있는 인터페이스를 적용할 수 있다. 이 경우 @RequestMapping 을 인터페이스 레벨에 부여한다면, 타입 상속(즉, 인터페이스 구현)를 통해 공통 매핑정보를 일괄적으로 적용할 수 있다.

인터페이스 구현에 의한 @RequestMapping 정보 상속은 클래스 상속과 거의 비슷하지만 몇 가지 차이점이 있으니 추의해야 한다.

1. 매핑정보 상속의 종류

상위 타입과 메소드의 @RequestMapping 상속

슈퍼클래스에만 @RequestMapping 을 적용하고 이를 그대로 상속한 서브 클래스에는 아무런 @RequestMapping 을 사용하지 않았을 경우를 생각해 보자.

클래스 상속의 경우는 단순하다. 슈퍼클래스의 모든 매핑정보를 그대로 서브클래스가 물려받는다. 아래 예를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping("/user")
public class Super  {
     
     @RequestMapping("/list")
     public String list(...) {}
      
}
 
public class Sub extends Super {
     
}

매핑정보는 Super 클래스에만 있고 Sub 클래스에는 없다. Sub 클래스는 Super 이 list() 메소드를 상속받는다. 동시에 Super 클래스와 list() 메소드에 붙은 @RequestMapping 정보도 그대로 상속받는다. Sub 클래스를 컨트롤러로 등록한다면, /user/list URL은 list() 메소드로 매핑된다.

이번에는 아래와 같이 메소드를 오버라이드한 경우를 생각해 보자. 서브 클래스에서 메소드를 오버라이드했더라도 메소드에 @RequestMapping을 붙이지 않는다면 슈퍼클래스 메소드의 매핑정보는 그대로 상속된다. 따라서 Sub 클래스의 list() 메소드는 여전히 /user/list 에 매핑된다.

1
2
3
4
public class Sub extends Super {
    public String list() { ... }
     
}

인터페이싀 경우도 마찬가지다. 인터페이스 타입과 메소드 레벨에 정의한 @RequestMapping 의 매핑정보는 구현 클래스에 그대로 상속된다.

상위 타입의 @RequestMapping 과 하위 타입 메소드의 @RequestMapping 결합

슈퍼클래스에는 타입에만 @RequestMapping 이 선언되어 있고, 서브 클래스에는 타입레벨에는 아무 매핑정보가 없고 메소드에만 @RequestMapping 이 있는 경우를 생각해 보자. 이 때도 슈퍼클래스의 타입 레벨 @RequestMapping 이 상속된다는 원칙이 적용된다. 결국 서브 클래스의 타입 레벨에 @RequestMapping 이 정의되어 있는 것처럼 각 메소드의 매핑정보가 결합돼서 최종적인 메소드 레벨의 매핑조건을 만들어 낸다.

상위 타입 메소드의 @RequestMapping 과 하위 타입의 @RequestMapping 결합

앞의 경우와 반대되는 것이다. 슈퍼클래스에는 메소드에만 @RequestMapping 이 있고, 서브 클래스에는 반대로 클래스 레벨에 @RequestMapping 이 부여된 경우다. 이 때도 마찬가지로 @RequestMapping 정보가 그대로 상속된 후에 결합된다.

인터페이스의 경우도 동일한 방식으로 적용된다. 단, 인터페이스를 구현하는 메소드에 URL 이 없는 빈 @RequestMapping 을 붙이면 인터페이스 메소드의 매핑정보가 무시되니 주의해야 한다.

하위 타입과 메소드의 @RequestMapping 재정의

상속 또는 구현을 통해 만들어진 하위 타입에 @RequestMapping 을 부여하면 상위 타입에서 지정한 @RequestMapping 매핑정보를 대체해서 적용된다.

인터페이스도 동일하다.

서브클래스 메소드의 URL 패턴 없는 @RequestMapping 재정의

하위 타입의 @RequestMapping 은 항상 상위 타입의 @RequestMapping 정보를 대신한다. 그런데 희한하게도 클래스 상속에서 오버라이드한 하위 메소드에 한해서는 URL 조건이 없는 @RequestMapping 을 붙였을 경우에 상위 메소드의 @RequestMapping 의 URL 조건이 그대로 상속된다. 버그일 수 있으므로 하위 타입의 메소드에 @RequestMapping 을 붙일 때는 항상 매핑조건을 지정해서 상위 메소드의 매핑 정보를 대체하도록 만들어야 한다고 기억해두자.


@RequestMapping 정보가 클래스 상속이나 인터페이스 구현을 통해 상속된다는 사실을 잘 활용하면 매우 편리한 매핑 전략을 만들어낼 수도 있다. 타입만 바꿀 수 있는 제너릭스 인터페이스를 사용하거나 추상 클래스에 매핑정보를 미리 넣어두고 이를 상속하는 개별 컨트롤러 클래스에서 필요한 부가 매핑정보만 넣도록 만들면, 컨트롤러마다 들어가는 공통적인 매핑 작업을 대폭 줄일 수 있다. 

반면에 체계적인 정책과 잘 준비된 가이드라인을 가지고 개발하지 않고 개발자마다 제멋대로 @RequestMapping 을 남발해서 적용하면 이해하고 관리하기 매우 힘든 코드를 낳게 될 테니 주의해야 한다. 자유도가 높은 애노테이션 방식의 @MVC 를 사용한다면 반드시 강력한 표준 정책을 만들어둘 것을 권장한다.

2. 제네릭스와 매핑정보 상속을 이용한 컨트롤러 작성

@RequestMapping 을 상속과 구현을 통해 잘 활용하면 반복적인 설정을 피하고 간결한 코드를 얻어 낼 수 있다. 특히 자바 5 이상의 타입 파라미터를 이용한 제네릭스를 활용해서 상위 타입에는 타입 파라미터와 메소드 레벨의 공통 매핑정보를 지정해 놓고, 이를 상속받는 개발 컨트롤러에는 구체적인 타입과 클래스 레벨의 기준 매핑정보를 지정해 주는 기법을 사용할 수 있다.

기본 정보의 입출력을 다루는 컨트롤러에는 도메인 오브젝트별로 CRUD 와 검색 기능을 가진 메소드가 중복돼서 등장한다. 값의 검증이나 뷰 로직 등은 컨트롤러에서 독립적으로 만들 수 있으므로, 이런 컨트롤러들은 서비스 계층의 CRUD 메소드로 요청을 위임해주는 것이 전부인 경우도 적지 않다. 각 컨트롤러마다 모델의 타입만 달라질 뿐 기본적인 구성과 코드는 동일한 코드가 중복돼서 만들어지기 마련이다. CRUD 용 컨트롤러라면 모델은 보통 도메인 오브젝트를 사용할 것이다.

타입만 달라지는 중복된 코드라면 제네릭스 타입 파라미터를 가진 슈퍼클래스로 공통적인 코드를 뽑아내는 것이 좋다. 동시에 매핑정보의 일부, 즉 URL 일부가 증복되는 것도 슈퍼클래스에 미리 정의해 둘 수 있다.

User 컨트롤러가 아래와 같이 정의되어 있다고 생각해보자. 일단은 컨트롤러 메소드의 파라메터에 어떻게 User 가 들어갈 수 있는지, 리턴 값의 타입은 어떻게 선택한 것인지는 신경쓰지 말자. 아무튼 이런 식의 컨트롤러 구성은 도메인 오브젝트(여기서는 User)만 바뀐 채로 여러 컨트롤러에서 반복될 것이다. 코드의 내용도 대부분 비슷하다. UserService 의 add(User) 메소드를 호출하는 식의 위임코드가 전부인 경우가 대부분이다. 컨트롤러의 역할 중에서 파라미터 파싱, 요청 정보 검증, 뷰 선택 로직 등은 모두 컨트롤러 밖으로 분산시킬 수 있기 때문이다. 그래서 이런 식의 CRUD 용 컨트롤러는 타입 파라미터를 가진 제네릭스 추상 클래스를 만들어 활용하기에 적합한 후보다.

1
2
3
4
5
6
7
8
9
10
11
public class UserController {
     
    private UserService service;
     
    public void add(User user) { ... }
    public void update(User user) { ... }
    public User view(Integer id) { ... }
    public void delete(Integer id) { ... }
    public List<User> list() { ... }
     
}

UserController 에 적용할 수 있는 제네릭 추상 클래스를 만들어 보자. 아래 코드는 모든 CRUD 컨트롤러에서 상속받을 수 있게 만들어진 제네릭 추상 클래스다.

1
2
3
4
5
6
7
8
9
10
11
public abstarct class GenericController<T, K, S> {
     
    protected S service;
     
    public void add(T entity) { ... }
    public void update(T entity) { ... }
    public T view(K id) { ... }
    public void delete(K id) { ... }
    public List<T> list() { ... }
     
}

개별 컨트롤러는 GenericController 클래스를 상속해서 만들면 된다. 아래는 GenericController 를 이용해 UserController 를 만든 것이다.

1
2
3
4
5
public class UserController extends GenericController<User, Integer, UserService> {
     
     
     
}

그런데 아직 UserController 에는 매핑정보가 없다. 메소드 레벨의 컨트롤러를 정의하고 있으니 @RequestMapping 을 이용한 URL 매핑이 필요할 것이다.

일단 URL을 작성하는 일관된 패턴이 있어야 한다. 도메인 오브젝트 이름을 먼저 넣고 다음에 메소드 이름이 들어가는 식이라면 /user/add, /user/update, /user/view 처럼 매핑 URL을 정의할 수 있다.

그렇다면 이 매핑정보는 어디에, 어떻게 넣어야 할까? 바로 이런 경우에 @RequestMapping 상속을 활용하면 된다. GenericController 에는 도메인 오브젝트가 바뀌더라도 변하지 않는 메소드 레벨의 매핑정보를 다음과 같이 넣는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class GenericController<T, K, S> {
     
    protected S service;
     
    @RequestMapping("/add")
    public void add(T entity) { ... }
     
    @RequestMapping("/update")
    public void update(T entity) { ... }
     
    @RequestMapping("/view")
    public T view(K id) { ... }
     
    @RequestMapping("/delete")
    public void delete(K id) { ... }
     
    @RequestMapping("/list")
    public List<T> list() { ... }
     
}

그리고 이를 상속한 UserController 에는 아래와 같이 클래스 레벨에 @RequestMapping 을 부여하면 URL 매핑과 컨트롤러 로직이 모두 적용된 컨트롤러를 완성할 수 있다. 물론 login() 처럼 개별 컨트롤러에 추가한 컨트롤러 메소드에는 직접 매핑정보를 넣어줘야 한다.

1
2
3
4
5
6
7
@RequestMapping("/user")
public class UserController extends GenericController<User, Integer, UserService> {
     
    @RequestMapping("/login")
    public String login(String userId, String passsword) { ... }
     
}

상위 타입에선 메소드에, 하위 타입에선 클래스에 @RequestMapping 을 붙여서 이를 결합시키는 매핑정보 상속 방법을 응용한 것이다. 

제네릭을 적용한 컨트롤러를 본격적으로 사용할 생각이라면 비슷한 방법을 서비스 계층과 DAO까지 확장해서 제네릭 DAO, 제네릭 서비스도 함께 적용하는 것이 편리하다. 스프링을 기반으로 제네릭스를 모든 계층에 적용해서 고속 개발이 가능하도록 만든 프레임워크를 살펴보고 싶다면 OSAF (http://whiteship.me/category/OSAF)의 소스코드를 참고해보기 바란다. 스프링 2.5 를 기반으로 만들어지긴 했지만, @RequestMapping 매핑을 사용하는 컨트롤러에 제네릭스를 적용하는 방법에 대해 많은 아이디어를 얻을 수 있을 것이다.


출처 - http://springsource.tistory.com/11






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

Sping MVC - Model  (0) 2012.05.09
Spring - @Controller  (0) 2012.05.09
Spring MVC Deprecated API  (0) 2012.05.08
Spring MVC - MultiActionController 사용하기  (0) 2012.05.08
spring controller 종류  (0) 2012.05.08
Posted by linuxism
,