2014년 2월 4일 화요일

Spring @Controller 리턴타입의 종류

자동추가 모델 오브젝트

@ModelAttribute 모델 오브젝트(커맨드 오브젝트)

메소드 파라미터 중에서 @ModelAttribute 를 붙인 모델 오브젝트나 @ModelAttribute 는 생략했지만 단순 타입이 아니라서 커맨드 오브젝트로 처리되는 오브젝트라면 자동으로 컨트롤러가 리턴하는 모델에 추가된다. 기본적으로 모델 오브젝트의 이름은 파라미터 타입 이름을 따른다. 이름을 직접 지정하고 싶다면 @ModelAttribute("모델이름") 으로 지정해 주면 된다. 따라서 코드에서 @ModelAttribute 모델 오브젝트를 모델맵에 직접 추가해줄 필요는 없다. 다음의 세 가지 메소드 선언은 모두 'user' 라는 이름으로 User 파라미터 오브젝트가 모델에 추가되게 해준다. [code language="java"] public void add(@ModelAttribute("user") User user) public void add(@ModelAttribute User user) public void add(User user) [/code]

Map/Model/ModelMap 파라미터

컨트롤러 메소드에 Map, Model, ModelMap 타입의 파라미터를 사용하면 미리 생성된 모델 맵 오브젝트를 전달받아서 오브젝트를 추가할 수 있다. 이런 파라미터에 추가한 오브젝트는 DispatcherServlet 을 거쳐 뷰에 전달되는 모델에 자동으로 추가된다. 컨트롤러에서 ModelAndView 를 별도로 만들어 리턴하는 경우에도 맵에 추가한 오브젝트는 빠짐없이 모델에 추가된다.

@ModelAttribute 메소드가 생성하는 오브젝트

@ModelAttribute 는 컨트롤러 클래스의 메소드 레벨에도 부여할 수 있다. 뷰에서 참고 정보로 사용되는 모델 오브젝트를 생성하는 메소드를 지정하기 위해 사용된다. 이를 이용하면 모델 오브젝트 생성을 전담하는 메소드를 만들 수 있다. @ModelAttribute 가 붙은 메소드는 컨트롤러 클래스 안에 정의하지만 컨트롤러 기능을 담당하지 않는다. 따라서 @RequestMapping 을 함께 붙이지 않아야 한다. @ModelAttribute 메소드가 생성하는 오브젝트는 클래스 내의 다른 컨트롤러 메소드의 모델에 자동으로 추가된다. 다음과 같이 메소드를 정의했다고 해보자. [code language="java"] @ModelAttribute("codes") public List codes() { return codeService.getAllCodes(); } [/code] codes() 메소드는 서비스 계층 오브젝트를 이용해 코드정보의 리스트를 받아서 리턴한다. 리턴되는 오브젝트는 @ModelAttribute에 지정한 "codes" 라는 이름으로 다른 컨트롤러가 실행될 때 모델에 자동 추가된다. 이렇게 @ModelAttribute 메소드가 필요한 이유는 무엇일까? 보통 폼이나 검색조건 페이지 등에서는 참조정보가 필요한 경우가 있다. 폼이나 검색조건 창에 select 태그를 써서 선택 가능한 목록을 보여주는 경우가 가장 대표적이다. 이때는 폼 데이터외에 참조정보의 목록을 모델에 넣어서 뷰로 보내줘야 한다. 물론 개별 컨트롤러에서 직접 모델에 추가해 줄 수도 있지만, 같은 클래스 내의 모든 컨트롤러 메소드에서 공통적으로 활용하는 정보라면 @ModelAttribute 메소드를 사용하는 것이 편리하다. 참조 정보가 많다면 @ModelAttribute 메소드를 하나 이상의 메소드에 적용할 수 있다.

BindingResult

@ModelAttribute 파라미터와 함께 사용하는 BindingResult 타입의 오브젝트도 모델에 자동으로 추가된다. 모델 맵에 추가될 때의 키는 "org.springframework.validation.BindingResult.모델이름" 이다. 모델이름이 user 라면 이에 대한 바인딩 결과를 담은 오브젝트는 org.springframework.validation.BindingResult.user 라는 이름의 키로 모델에 추가될 것이다. BindingResult 오브젝트가 모델에 자동으로 추가되는 이유는 스프링의 JSP 나 프리마커, 벨로시티 등의 뷰에서 사용되는 커스텀 태그나 매크로에서 사용되기 때문이다. 주로 잘못 입력된 폼 필드의 잘못 입력된 값을 가져오거나 바인딩 오류 메시지를 생성성할 때 사용된다. 일반적으로는 BindingResult 모델 오브젝트를 뷰에서 직접 사용할 필요는 없다. 핸들러 인터셉터를 이용해 바인딩 결과를 로깅하거나 분석할 때 사용할 수 있다.

ModelAndView

ModelAndView 는 컨트롤러가 리턴해야 하는 정보를 담고 있는 가장 대표적인 타입이다. 하지만 @Controller 에서는 @ModelAndView 를 이용하는 것보다 편리한 방법이 많아서 자주 사용되지는 않는다. 혹시 기존의 Controller 타입으로 작성한 컨트롤러 코드를 @MVC 방식으로 포팅할 경우라면 일단 ModelAndView 를 리턴하는 코드를 그대로 가져와서 적용한 뒤에 나중에 다듬는 방법을 쓸 수 있다. 예를 들어 스프링 2.5나 그 이전에서 자주 사용되던 Controller 타입의 컨트롤러 클래스가 다음과 같이 만들어졌다고 해보자. name 파라미터를 받아서 모델에 넣어주고 뷰 이름과 함께 ModelAndView 타입으로 리턴해주는 간단한 코드다. 모델은 맵을 먼저 만들어서 ModeAndView 생성자에 넣어도 되지만 아래 예처럼 ModelAndView 오브젝트를 만든 뒤에 addObject() 메소드로 추가해줘도 된다. [code language="java"] public class HelloController implements Controller { @Override public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { String name = request.getParameter("hello"); return new ModelAndView("hello.jsp").addObject("name", name); } } [/code] 이 컨트롤러 코드를 @MVC 스타일의 컨트롤러로 변경하는 건 아주 간단하다. 다음처럼 Controller 인터페이스 구현 선언을 없애고, @Controller 와 @RequestMapping 을 붙여주면 된다. [code language="java"] @Controller public class HelloController { @RequestMapping("/hello") public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { String name = request.getParameter("hello"); return new ModelAndView("hello.jsp").addObject("name", name); } } [/code] 물론 @Controller 답게 만들려면 파라미터로 HttpServletRequest 를 사용하는 대신 @RequestParam 으로 파라미터 값을 가져오는 편이 훨씬 깔끔하다. 이왕이면 메소드 이름도 의미 있는 것으로 바꾸자, throws Exception 도 선언이 꼭 필요한 경우가 아니라면 없애자, 마지막으로 Model 파라미터를 이용하도록 해보자. 다음과 같이 훨씬 간결한 코드로 작성할 수 있다. [code language="java"] @RequestMapping("/hello") public ModelAndView hello(@RequestParam String name, Model model) { model.addAttribute("name", name); return new ModelAndView("hello"); } [/code]

String

메소드의 리턴타입이 스트링이면 리턴 값은 뷰 이름으로 사용된다. 모델정보는 모델 맵 파라미터로 전달받아 추가해주는 방법을 사용해야 한다. Model 이나 ModelMap 파라미터를 전달받아서 여기에 모델 정보를 넣어주는 것이다.위의 컨트롤러 코드는 ModelAndView 오브젝트를 리턴하지만 필요한 것은 뷰 이름뿐이다. 따라서 다음과 같이 바꾸는 편이 더 깔끔하다. [code language="java"] @RequestMapping("/hello") public String hello(@RequestParam String name, Model model) { model.addAttribute("name", name); return "hello"; } [/code] 모델은 파라미터로 전달받은 Map, Model, ModelMap 에 넣어주고 리턴값은 뷰 이름을 스트링 타입으로 선언하는 방식은 흔히 사용되는 @Controller 메소드 작성 방법이다. 컨트롤러 코드에서 모델을 추가해주고 뷰 이름을 지정해 주는 방법 중에서 가장 깔끔하기 때문이다.

void

메소드의 리턴 타입을 아예 void 로 할 수도 있다. 이때는 RequestToViewNameResolver 전략을 통해 자동생성되는 뷰 이름이 사용된다. URL 과 뷰 이름을 일관되게 통일할 수만 있다면 void 형의 사용도 적극 고려해볼 만하다. 위의 메소드는 다시 다음과 같이 더 단순하게 바꿀 수 있다. [code language="java"] @RequestMapping("/hello") public void hello(@RequestParam String name, Model model) { model.addAttribute("name", name); } [/code] void 형 리턴타입이기 때문에 뷰 이름은 RequestToViewNameResolver 를 통해 자동생성된다. 디폴트로 등록된 RequestToViewNameResolver 는 URL 을 따라서 뷰 이름을 hello 로 만들어줄 것이다. 여기에 뷰 리졸버가 prefix, suffix 를 붙여주도록 하면 /WEB-INF/view/hello.jsp 같은 식으로 완성할 수 있다.

모델 오브젝트

뷰 이름은 RequestToVewNameResolver 로 자동생성하는 것을 사용하고 코드를 이용해 모델에 추가할 오브젝트가 하나뿐이라면, Model 파라미터를 받아서 저장하는 대신 모델 오브젝트를 바로 리턴해도 된다.스프링은 리턴타입이 미리 지정된 타입이나 void 가 아닌 단순 오브젝트라면 이를 모델 오브젝트로 인식해서 모델에 자동으로 추가해준다. 이때 모델 이름은 리턴값의 타입 이름을 따른다. 다음 메소드를 살펴보자. view() 메소드는 파라미터로 전달된 id 값을 이용해 User 오브젝트를 가져와서 바로 리턴해준다.메소드 리턴 타입은 User 타입으로 선언해뒀다. 이때는 리턴한 User 타입 오브젝트가 user 라는 이름으로 모델에 추가될 것이다. 뷰 이름은 제공할 기회가 없었으니 디폴트 RequestToViewNameResolver 를 통해 "view" 로 결정될 것이다. [code language="java"] @RequestMapping("/view") public User view(@RequestParam int id) { return userService.getUser(id); } [/code] 클래스 이름과 다른 모델 이름을 사용하고 싶다면, 메소드 레벨의 @ModelAttribute 를 사용해 모델 이름을 직접 지정해줄 수 있다.

Map/Model/ModelMap

메소드의 코드에서 Map 이나 Model, ModelMap 타입의 오브젝트를 직접 만들어서 리턴해주면 이 오브젝트는 모델로 사용된다. 컨트롤러 코드에서 필요한 모델 맵은 파라미터로 받아서 사용하면 편리하다. 직접 Map/Model/ModelMap 타입의 오브젝트를 코드에서 생성하는 건 비효율적이다. 따라서 맵을 리턴 타입으로 사용하는 일은 많지 않을 것이다. 이 리턴 타입의 특징은 분명하게 기억해둬야 한다. 단일 모델 오브젝트를 직접 리턴하는 방식을 사용하다가 실수할 수 있기 때문이다. 예를 들어 서비스 계층의 메소드에서 맵 타입으로 결과를 돌려주는 경우가 있다. SpringJDBC 의 API 를 보면 Map 타입의 결과를 돌려주는 메소드가 제법된다. 그런데 단일 오브젝트를 리턴하면 모델로 자동등록된다고 해서 다음과 같이 코드를 작성하면 안된다. [code language="java"] @RequestMapping("/view") public Map view(@RequestParam int id) { Map userMap = userService.getUserMap(id); return userMap; } [/code] 즉, 이코드에 의해 map 이라는 이름을 가진 맵 오브젝트가 모델에 추가될 것이라고 기대하면 안된다. Map 타입의 리턴 값은 그 자체로 모델맵으로 인식해서 그 안의 엔트리 하나하나를 개별적인 모델로 다시 등록해 버리기 때문이다. 따라서 Map 타입의 단일 오브젝트 리턴은 근본적으로 피하는 게 좋다. 위와 같은 코드는 원래 의도대로 하려면 다음과 같이 Model 오브젝트를 파라미터로 직접 받아서 코드에 추가해 줘야 한다. [code language="java"] @RequestMapping("/view") public void view(@RequestParam int id, Model model) { model.addAttribute("userMap", userService.getUserMap(id)); } [/code]

View

스트링 리턴 타입은 뷰 이름으로 인식한다고 했다. 그런데 뷰 이름 대신 뷰 오브젝트를 사용하고 싶다면 다음과 같이 리턴 타입을 View 로 선언하고 뷰 오브젝트를 리턴하면 된다.
public class UserController {

    @Autowired
    MarshallingView userXmlView;

    @RequestMapping("/user/xml")
    public View view(@RequestParam int id) {
        ....
        return this.userXmlView;
    }

}

@ResponseBody

@ResponseBody 는 @RequestBody 와 비슷한 방식으로 동작한다. @ResponseBody 가 메소드 레벨에 부여되면, 메소드가 리턴하는 오브젝트는 뷰를 통해 결과를 만들어내는 모델로 사용되는 대신, 메시지 컨버터를 통해 바로 HTTP 응답의 메시지 본문으로 전환된다. 아래 hello() 메소드에 @ResponseBody 가 없다면 스트링 타입의 리턴 값은 뷰 이름으로 인식될 것이다. 하지만 @ResponseBody 가 붙었으므로 스트링 타입을 지원하는 메시지 컨버터가 이를 변환해서 HttpServletResponse 의 출력스트림에 넣어 버린다.
@RequestMapping("/hello")
@ResponseBody
public String hello() {
    return "Hello Spring";
}
@ResponseBody 가 적용된 컨트롤러는 리턴 값이 단일 모델이고 메시지컨버터가 뷰와 같은 식으로 동작한다고 이해할 수 있다. 근본적으로 @RequestBody, @ResponseBody 는 XML 이나 JSON 과 같은 메시지 기반의 커뮤니케이션을 위해 사용된다

댓글 없음:

댓글 쓰기