@Controller 파라미터의 종류

지금부터 소개할 타입의 오브젝트와 애노테이션은 @Controller 의 메소드 파라미터로 자유롭게 사용할 수 있다. 단, 몇 가지 파라미터 타입은 순서와 사용 방법상의 제약이 있으니 주의해야 한다.

HttpServletRequest/HttpServletResponse/ServletRequest/ServletResponse

대개는 좀 더 상세한 정보를 담은 파라미터 타입을 활용하면 되기 때문에 필요 없겠지만, 그래도 원한다면 HttpServletRequest, HttpServletResponse, ServletRequest, ServletResponse 오브젝트를 파라미터로 사용할 수 있다.

HttpSession

HttpSession 오브젝트는 HttpServletRequest 를 통해 가져올 수도 있지만, HTTP 세션만 필요한 경우라면 HttpSession 타입 파라미터를 선언해서 직접 받는 편이 낫다. HttpSession 은 서버에 따라서 멀티스레드 환경에서 안전성이 보장되지 않는다. 서버에 상관없이 HttpSession 을 안전하게 사용하려면 핸들어 어댑터의 synchronizeOnSession 프로퍼티를 true 로 설정해 줘야 한다.

WebRequest/NativeWebRequest

WebRequest 는 HttpServletRequest 의 요청 정보를 대부분 그대로 갖고 있는, 서블릿 API 에 종속적이지 않은 오브젝트 타입니다. WebRequest 는 원래 서블릿과 포틀릿 환경 양쪽에서 모두 적용 가능한 범용적인 핸들러 인터셉터를 만들 때 활용하기 위해 만들어 졌다. 따라서 스프링 서블릿/MVC 의 컨트롤러에서라면 꼭 필요한 건 아니다.

NativeWebRequest 에는 WebRequest 내부에 감춰진 HttpServletRequest 와 같은 환경 종속적인 오브젝트를 가져올 수 있는 메소드가 추가되어 있다.

Locale

java.util.Locale 타입으로 DispatcherServlet 의 지역정보 리졸버(Locale Resolver) 가 결정한 Locale 오브젝트를 제공 받을 수 있다.

InputStream/Reader

HttpServletRequest 의 getInputStream() 을 통해서 받을 수 있는 콘텐트 스트림 또는 Reader 타입 오브젝트를 제공받을 수 있다.

OutputStream/Writer

HttpServletResponse 의 getOutputStream() 으로 가져올 수 있는 출력용 컨텐트 스트림 또는 Writer 타입 오브젝트를 받을 수 있다.

@PathVariable

@RequestMapping 의 URL 정의 부의 중괄호({}) 에 명시된 패스 변수를 받는다.

@Controller 는 URL 에서 파라미터에 해당하는 부분에 {}을 넣는 URI 템플릿을 사용할 수 있다. 컨트롤러 메소드 파라미터에는 @PathVariable 애노테이션을 이용해 URI 템플릿 중에서 어떤 파라미터를 가져올지를 결정할 수 있다.

예를 들면 다음과 같다.

1
2
3
4
@RequestMapping("/user/view/{id}")
public String view(@PathVariable("id") int id) {
    ...
}

URL의 중괄호({}) 에는 패스 변수를 넣는다. 이 이름을 @PathVariable 애노테이션의 값으로 넣어서 메소드 파라미터에 부여해 주면 된다. /user/view/10 이라는 URL 이라면, id 파라미터에 10이라는 값이 들어올 것이다.

파라미터의 타입은 URL 의 내용이 적절히 변환될 수 있는 것을 사용해야 한다. int 타입을 썻을 경우에는 반드시 해당 패스 변수 자리에 숫자 값이 들어 있어야 한다. 타입이 일치하지 않는 값이 들어 올 경우 별 다른 예외처리를 해주지 않는 다면 HTTP 400 - Bad Request 응답코드가 전달될 것이다.

@RequestParam

HTTP 요청 파라미터를 메소드 파라미터에 넣어 주는 애노테이션이다. 가져올 오쳥 파라미터의 이름을 @RequestParam 애노테이션의 기본 값으로 지정해주면 된다. 요청 파라미터의 값은 메소드 파라미터의 타입에 따라 적절하게 변환된다.

몇가지 예를 살펴보자

1
2
3
4
5
6
7
8
9
10
11
12
public String method1(@RequestParam("id") int id) { ... }
 
public String method2(@RequestParam("id") int id,
        @RequestParam("name") String name,
        @RequestParam("file") MultipartFile file) { ... }
 
public String method3(@RequestParam Map<String, String> params) { ... }
 
public String method4(
        @RequestParam(value="id", required=false, defaultValue="-1") int id) { ... }
 
public String method5(@RequestParam int id) { ... }

○ method1() 의 선언은 id 요청 파라미터를 int 타입으로 변환해서 메소드의 id 파라미터에 넣어준다.

○ @RequestParam 은 method2() 와 같이 하나 이상의 파라미터에 적용할 수 있다. 스프링의 내장 변환기가 다룰 수 있는 모든 타입을 지원한다.

○ method3() 과 같이 @RequestParam 에 파라미터 이름을 지정하지 않고 Map<String, String> 타입으로 선언하면 모든 요청 파라미터를 담은 맵으로 받을 수 있다. 파라미터 이름은 맵의 키에, 값은 맵의 값에 담겨 전달된다.

○ @RequestParam 을 사용했다면 해당 파라미터가 반드시 있어야 한다. 없다면 HTTP 400 - Bad Request 를 받게 될 것이다. 파라미터가 필수가 아니라 선택적으로 제공하게 하려면, required 엘리먼트를 false 로 설정해 주면 된다. 요청 파라미터가 존재하지 않을 때 사용할 디폴트 값도 지정할 수 있다. method4() 는 required 와 defaultValue 엘리먼트를 설정한 예이다.

 ○ method5() 는 메소드 파라미터의 이름과 요청 파라미터의 이름이 일치하기 때문에 @RequestParam 의 이름 엘리먼트를 생략한 예이다.

○ String, int 와 같은 단순 타입인 경우는 @RequestParam 을 아예 생략할 수도 있다. 이때는 메소드 파라미터와 같은 이름의 요청 파라미터 값을 받는다. 하지만 파라미터의 개수가 많고 종류가 다양해지면 코드를 이해하는 데 불편할 수도 있다. 단순한 메소드가 아니라면 명시적으로 @RequestParam 을 부여해 주는 것을 권장한다.

 @CookieValue

HTTP 요청과 함께 전달된 쿠키 값을 메소드 파라미터에 넣어 주도록 @CookieValue 를 사용할 수 있다.

애노테이션의 기본 값에 쿠키 이름을 지정해주면 된다.

몇 가지 예를 살펴보자

1
2
3
4
public String method1(@CookieValue("auth") String auth) { ... }
 
public String method2(
        @CookieValue(value="auth", required=false, defaultValue="NONE") String auth) {...}

○ method1() 은 auth 라는 이름의 쿠키 값을 메소드 파라미터 auth 에 넣어주는 메소드 선언이다. 메소드 파라미터 이름과 쿠키 값이 동일하다면 쿠키 이름은 생략할 수 있다. 

○ @CookieValue 도 @RequestParam 과 마찬가지로 지정된 쿠키 값이 반드시 존재해야만 한다. 지정한 쿠키 값이 없을 경우에도 예외가 발생하지 않게 하려면, @CookieValue 의 required 엘리먼트를 false 로 선언해줘야 한다. 또한 디폴트 값을 선언해서 쿠키 값이 없을 때 디폴트 값으로 대신하게 할 수 있다. method2() 는 required 와 defaultValue 엘리먼트를 적용한 예이다.

@RequestHeader

요청 헤더를 메소드 파라미터에 넣어 주는 애노테이션이다. 애노테이션의 기본 값으로 가져올 HTTP 헤더의 이름을 지정한다. 다음은 Host 와 Keep-Alive 헤더 값을 메소드 파라미터로 받도록 선언한 메소드다.

1
2
public void header(@RequestHeader("Host") String host,
        @RequestHeader("Keep-Alive") long keepAlive) { ... }

@CookieValue 와 마찬가지로 @RequestHeader 를 사용했다면 헤더 값이 반드시 존재해야 한다. 헤더 값이 존재하지 않아도 상관없도록 설정하거나 디폴트 값을 주려면, @CookieValue 와 마찬가지로 required 와 defaultValue 엘리먼트를 이용하면 된다.

Map/Model/ModelMap

다른 애노테이션이 붙어 있지 않다면 java.util.Map 그리고 스프링의 org.springframework.ui.Model 과 org.springframework.ui.ModelMap 타입의 파라미터는 모두 모델 정보를 담는 데 사용할 수 있는 오브젝트가 전달된다. 모델을 담을 맵은 메소드 내에서 직접 생성할 수도 있지만 그보다는 파라미터로 정의해서 핸들러 어댑터에서 미리 만들어 제공해주는 것을 사용하면 편리하다.

Model 과 ModelMap은 모두 addAttribute() 메소드를 제공해준다. 일반적인 맵의 put() 처럼 이름을 지정해서 오브젝트 값을 넣을 수도 있고, 자동 이름 생성 기능을 이용한다면 오브젝트만 넣을 수도 있다.

예를 들어 다음과 같이 ModelMap 에 User 타입 오브젝트를 넣는다면 타입정보를 참고해서 "user" 라는 모델 이름이 자동으로 부여된다.

1
2
3
4
5
@RequestMapping(...)
public void hello(ModelMap model) {
    User user = new User(1, "Spring");
    model.addAttribute(user);
}

ModelMap 과 Model 의 addAllAttribute() 메소드를 사용하면 Collection 에 담긴 모든 오브젝트를 자동 이름 생성 방식을 적용해서 모두 모델로 추가해 준다.

@ModelAttribute

@ModelAttribute 는 여기서 소개하는 것처럼 메소드 파라미터에도 부여할 수 있고 메소드 레벨에 적용할 수도 있다. 두 가지가 비슷한 개념이지만 사용 목적이 분명히 다르니 그 차이점에 주의해야 한다.

@ModelAttribute 는 모델 맵에 담겨서 뷰에 전달되는 모델 오브젝트의 한가지라고도 볼 수 있다. 기본적으로 @ModelAttribute 가 붙은 파라미터는 별도의 설정 없이도 자동으로 뷰에 전달된다.

클라이언트로부터 컨트롤러가 받은 요청정보 중에서, 하나 이상의 값을 가진, 오브젝트 형태로 만들 수 있는 구조적인 정보를 @ModelAttribute 모델이라고 부른다. @ModelAttribute 는 이렇게 컨트롤러가 전달받은 오브젝트 형태의 정보를 가리키는 말이다.

그렇다면 사용자가 제공하는 정보 중에서 단순히 @RequestParam 이 아니라 @ModelAttribute 를 사용해서 모델로 받는 것은 어떤 게 있을까? 사실 정보의 종류가 다른 건 아니다. 단지 요청 파라미터를 메소드 파라미터에서 1:1로 받으면 @RequestParam 이고, 도메인 오브젝트나 DTO 의 프로퍼티에 요청 파라미터를 바인딩해서 한번에 받으면 @ModelAttribute 라고 볼 수 있다. 하나의 오브젝트에 클라이언트의 요청 정보를 담아서 한 번에 전달되는 것이기 때문에 이를 커맨드 패턴에서 말하는 커맨드 오브젝트라고 부르기도 한다.

요청 파라미터가 많은 경우 @RequestParam 으로 모두 1:1로 받으면 코드가 아무래도 지저분 해진다. 대신 모든 요청 파라미터를 바인딩할 프로퍼티들을 가지는 하나의 클래스를 정의해 사용하면 코드는 무척이나 깔끔해 질 것이다.

예를 들어 아래 add() 메소드는 사용자 등록폼에서 전송된 모든 사용자 정보 파라미터를 바인딩할 수 있는 User 오브젝트를 사용해서 깔끔하게 처리하고 있다. 

1
2
3
4
5
@RequestMapping(value="/user/add", method=RequestMethod.POST)
public String add(@ModelAttribute User user) {
    userService.add(user);
    ...
}

@ModelAttribute 애노테이션도 생략 가능하다.

@RequestParam, @ModelAttribute 애노테이션을 사용하면 메소드 선언이 길어지고 복잡해 보인다고 이를 무조건 생략하는 건 위험할 수 있다. 그래서 가능한 @ModelAttribute 나 @RequestParam 을 사용하는 것을 권장한다.

@ModelAttribute 가 해주는 기능이 한 가지가 더 있데, 그것은 컨트롤러가 리턴하는 모델에 파라미터로 전달한 오브젝트를 자동으로 추가해 주는 것이다. 이때 모델의 이름은 기본적으로 파라미터의 이름을 따른다. User 클래스라면 user 라는 이름의 모델이 자동으로 등록된다는 것이다. 다른 이름을 사용하고 싶다면 다음과 같이 @ModelAttribute 에 모델 이름을 지정해 줄 수도 있다.

1
2
3
public String update(@ModelAttribute("currentUser") User user) {
    ...
}

위와 같이 정의하면 update() 컨트롤러가 DispatcherServlet 에게 돌려주는 모델 맵에는 "currentUser" 라는 키로 User 오브젝트가 저장되어 있을 것이다.

Errors/BindingResult

@ModelAttribute 는 단지 오브젝트에 여러 개의 요청 파라미터 값을 넣어서 넘겨주는 게 전부가 아니다. @ModelAttribute 가 붙은 파라미터를 처리할 때는 @RequestParam 과 달리 Validation 작업이 추가적으로 진행된다. 변환이 불가능한 타입의 요청 파라미터가 들어 왔을 때 어떤 일이 일어 날까?

@RequestParam 의 경우 먼저 살펴보자.

@RequestParam은 스프링의 기본 타입 변환 기능을 이용해서 요청 파라미터 값을 메소드 파라미터 타입으로 변환한다. 타입 변환이 성공한다면 메소드의 파라미터로 전달되지만, 변환 작업을 실패한다면 특별한 예외처리를 해 놓지 않은 이상 디폴트 예외 리졸버를 통해 HTTP 400 - Bad Request 응답 상태로 전환돼서 클라이언트로 전달된다. 이런 경우 사용자에게 친절한 메시지를 보여주고 싶다면 org.springframework.beans.TypeMismatchException 예외를 처리하는 핸들러 예외 리졸버를 추가해 주면 된다.

이제 @ModelAttribute 의 경우를 살펴보자.

다음과 같이 메소드 선언이 있고 UserSearch 의 id 프로퍼티가 int 타입이라고 하자. 이때 /search/?id=abcd 라는 URL 이 들어 왓다면 어떻게 될까?

1
public String search(@ModelAttribute userSearch, BindingResult result) { ... }

UserSearch 의 setId() 를 이용해 id 값을 넣으려고 시도하다가 예외를 만나게 될 것이다. 하지만 이때는 작업이 중단되고 HTTP  400 응답 상태코드가 클라이언트로 전달되지 않는다. 타입 변환에 실패하더라도 작업은 계속 진행된다. 단지 타입 변환 중에 발생한 예외가 BindException 타입의 오브젝트에 담겨서 컨트롤러로 전달될 뿐이다. 그렇다면 @ModelAttribute 에서는 왜 요청 파라미터의 타입 변환 문제를 바로 에러로 처리하지 않는 것일까?

그 이유는 @ModelAttribute 는 요청 파라미터의 타입이 모델 오브젝트의 프로퍼티 타입과 일치하는 지를 포함한 다양한 방식의 검증 기능을 수행하기 때문이다. @ModelAttribute 입장에서는 파라미터 타입이 일치하지 않는다는 건 검증 작업의 한 가지 결과일 뿐이지, 예상치 못한 예외상황이 아니라는 뜻이다. 별도의 검증과정 없이 무조건 프로퍼티 타입으로 변환해서 값을 넣으려고 시도하는 @RequestParam 과는 그런 면에서 차이가 있다.

사용자가 직접 입력하는 폼에서 들어오는 정보라면 반드시 검증이 필요하다. 버튼이나 링크에 미리 할당된 URL에 담겨 있는 파라미터와 달리 사용자가 입력하는 값에는 다양한 오류가 있을 수 있기 때문이다. 사용자가 입력한 폼의 데이터를 검증하는 작업에는 타입 확인뿐 아니라 필수정보의 입력여부, 길이제한, 포멧, 값의 허용범위 등 다양한 검증 기준이 적용될 수 있다. 이렇게 검증 과정을 거친 뒤 오류가 발견됐다고 하더라도 HTTP 400 과 같은 예외 응답상태를 전달하면서 작업을 종료하면 안된다. 어느 웹사이트에 가서 회원가입을 하는 중에 필수 항목을 하나 빼먹었다고 호출스택 정보와 함께 HTTP 400 에러 메시지가 나타난다면 얼마나 황당하겠는가?

그래서 사용자의 입력 값에 오류가 있을 때 이에 대한 처리를 컨트롤러에게 맡겨야 한다. 그러려면 메소드 파라미터에 맞게 요청정보를 추출해서 제공해주는 책임을 가진 어댑터 핸들러는 실패한 변환 작업에 대한 정보를 컨트롤러에게 제공해 줄 필요가 있다. 컨트롤러는 이런 정보를 참고해서 적절한 에러 페이지를 출력하거나 친절한 에러 메시지를 보여주면서 사용자가 폼을 다시 수정할 기회를 줘야 한다.

바로 이 때문에 @ModelAttribute 를 통해 폼의 정보를 전달받을 때는 org.springframework.validation.Errors 또는 org.springframework.validation.BindingResult 타입의 파라미터를 같이 사용해야 한다. Erros 나 BindingResult 파라미터를 함께 사용하지 않으면 스프링은 요청 파라미터의 타입이나 값에 문제가 없도록 애플리케이션이 보장해준다고 생각한다. 단지 파라미터의 개수가 여러 개라 커맨드 오브젝트 형태로 전달받을 뿐이라고 보는 것이다. 따라서 이때는 타입이 일치하지 않으면 BindingException 예외가 던져진다. 이 예외는 @RequestParam 처럼 친절하게 HTTP 400 응답 상태코드로 변환되지도 않으니 절절하게 예외처리를 해주지 않으면 사용자는 지저분한 에러메시지를 만나게 될 것이다.

폼에서 사용자 정보를 등록받는 add() 와 같은 메소드라면 반드시 아래와 같이 정의해야 한다.

1
2
@RequestMapping(value="add", method=RequestMethod.POST)
public String add(@ModelAttribute User user, BindingResult bindingResult) { ... }

BindingResult 대신 Errors 타입으로 선언해도 좋다. 이 두가지 오브젝트에는 User 오브젝트에 파라미터를 바인딩하다가 발생한 변환 오류와 모델 검증기를 통해 검증하는 중에 발견한 오류가 저장된다. 파라미터로 전달받은 bindingResult 를 확인해서 오류가 없다고 나오면, 모든 검증을 통과한 것이므로 안심하고 user 오브젝트 내용을 DB에 등록하고 성공 페이지로 넘어가면 된다. 반대로 bindingResult 오브젝트에 오류가 담겨 있다면 다시 등록폼을 출력해서 사용자가 잘못된 정보를 수정하도록 해야 한다. 스프링의 폼을 처리하는 커스텀 태그를 활용하면 BindingResult 에 담긴 오류 정보를 적절한 메시지로 변환해서 화면에 출력해줄 수 있다.

BindingResult 나 Errors 를 사용할 때 주의할 점은 파라미터의 위치다. 이 두 가지 타입의 파라미터는 반드시 @ModelAttribute 파라미터 뒤에 나와야 한다. 자신의 바로 앞에 있는 @ModelAttribute 파라미터의 검증 작업에서 발생한 오류만을 전달해 주기 때문이다.

SessionStatus

컨트롤러가 제공하는 기능 중에 모델 오브젝트를 세션에 저장했다가 다음 페이지에서 다시 활용하게 해주는 기능이 있다. 이 기능을 사용하다가 더 이상 세션 내에 모델 오브젝트를 저장할 필요가 없을 경우에는 코드에서 직접 작업 완료 메소드를 호출해서 세션안에 저장된 오브젝트를 제거해 줘야 한다. 이때 필요한 것이 스프링의 org.springframework.web.bind.support.SessionStatus 오브젝트다. 파라미터로 선언해 두면 현재 세션을 다룰 수 있는 SessionStatus 오브젝트를 제공해 준다. 세션안에 불필요한 오브젝트를 방치하는 것은 일종의 메모리 누수이므로 필요 없어지면 확실하게 제거해 줘야 한다.

@RequestBody

이 애노테이션이 붙은 파라미터는 HTTP 요청의 body 부분이 그대로 전달된다. 일반적인 GET/POST 의 요청 파라미터라면 @RequestBody 를 사용할 일이 없을 것이다. 반면에 XML 이나 JSON 기반의 메시지를 사용하는 요청의 경우에는 이 방법이 매우 유용하다.

AnnotationMethodHandlerAdapter 에는 HttpMessageConverter 타입의 메시지 변환기가 여러 개 등록되어 있다. @RequestBody 가 붙은 파라미터가 있으면 HTTP 요청의 미디어 타입과 파라미터의 타입을 먼저 확인한다. 메시지 변환기 중에서 해당 미디어 타입과 파라미터 타입을 처리할 수 있는 것이 있다면, HTTP 요청의 본문 부분을 통째로 변환해서 지정된 메소드 파라미터로 전달해 준다.

String HttpMessageConverter 타입 변환기는 스트링 타입의 파라미터와 모든 종류의 미디어 타입을 처리해 준다. 따라서 다음과 같이 정의한다면 메시지 본문 부분이 모두 스트링으로 변환돼서 전달될 것이다.

1
public void message(@RequestBody String body) { ... }

XML 본문을 가지고 들어오는 요청은 MarshallingHttpMessageConverter 등을 이용해서 XML 이 변환된 오브젝트로 전달 받을 수 있다. JSON 타입의 메시지라면 MappingJacksonHttpMessageConverter 를 사용할 수 있다. @RequestBody 는 보통 @ResponseBody 와 함께 사용된다.

@Value

빈의 값 주입에서 사용하던 @Value 애노테이션도 컨트롤러 메소드 파라미터에 부여할 수 있다. 사용 방법은 DI 에서 프로퍼티나 필드, 초기화 메소드 파라미터에 @Value 를 사용하는 것과 동일하다. 주로 시스템 프로퍼티나 다른 빈의 프로퍼티 값, 또는 좀 더 복잡한 SpEL 을 이용해 클래스의 상수를 읽어 오거나 특정 메소드를 호출한 결과 값, 조건식 등을 넣을 수 있다.

다음은 시스템 프로퍼티에서 OS 이름을 가져와 osName 파라미터에 넣어주는 메소드 선언이다. 스프링이 컨텍스트에 자동으로 등록해주는 시스템 프로퍼티 빈인 systemProperties 로부터 os.name 을 키로 가진 값을 가져와 osName 프로퍼티 변수에 넣어 준다.

1
2
@RequestMapping(...)
public String hello(@Value("#{systemProperties['os.name']}") String osName) { ... }

컨트롤러도 일반적인 스프링 빈이기 때문에 @Value를 메소드 파라미터 대신 컨트롤러 필드에 아래와 같이 DI 해주는 것이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
public class UserController {
     
    @Value("#{systemProperties['os.name']}")
    private String osName;
     
    @RequestMapping(...)
    public String add(...) {
        String osName = this.osName;
        ...
    }
 
}

@Value 로 정의하는 상수 값이 컨트롤러의 여러 메소드에서 필요로 한다면, 필드에 DI 해두는 편이 낫다.

@Valid

@Valid 는 JSR-303 의 빈 검증기를 이용해서 모델 오브젝트를 검증하도록 지시하는 지시자다. 모델 오브젝트의 검증 방법을 지정하는데 사용하는 애노테이션이다. 보통 @ModelAttribute 와 함께 사용한다.

@Controller 리턴타입의 종류

자동추가 모델 오브젝트

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

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

다음의 세 가지 메소드 선언은 모두 'user' 라는 이름으로 User 파라미터 오브젝트가 모델에 추가되게 해준다.

1
2
3
public void add(@ModelAttribute("user") User user)
public void add(@ModelAttribute User user)
public void add(User user)

Map/Model/ModelMap 파라미터

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

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

@ModelAttribute 는 컨트롤러 클래스의 메소드 레벨에도 부여할 수 있다. 뷰에서 참고 정보로 사용되는 모델 오브젝트를 생성하는 메소드를 지정하기 위해 사용된다. 이를 이용하면 모델 오브젝트 생성을 전담하는 메소드를 만들 수 있다.

@ModelAttribute 가 붙은 메소드는 컨트롤러 클래스 안에 정의하지만 컨트롤러 기능을 담당하지 않는다. 따라서 @RequestMapping 을 함께 붙이지 않아야 한다. @ModelAttribute 메소드가 생성하는 오브젝트는 클래스 내의 다른 컨트롤러 메소드의 모델에 자동으로 추가된다.

다음과 같이 메소드를 정의했다고 해보자.

1
2
3
4
@ModelAttribute("codes")
public List<Code> codes() {
    return codeService.getAllCodes();
}

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() 메소드로 추가해줘도 된다.

1
2
3
4
5
6
7
8
9
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);
    }
 
}

이 컨트롤러 코드를 @MVC 스타일의 컨트롤러로 변경하는 건 아주 간단하다. 다음처럼 Controller 인터페이스 구현 선언을 없애고, @Controller 와 @RequestMapping 을 붙여주면 된다.

1
2
3
4
5
6
7
8
9
10
@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);
    }
 
}

물론 @Controller 답게 만들려면 파라미터로 HttpServletRequest 를 사용하는 대신 @RequestParam 으로 파라미터 값을 가져오는 편이 훨씬 깔끔하다. 이왕이면 메소드 이름도 의미 있는 것으로 바꾸자, throws Exception 도 선언이 꼭 필요한 경우가 아니라면 없애자, 마지막으로 Model 파라미터를 이용하도록 해보자.

다음과 같이 훨씬 간결한 코드로 작성할 수 있다.

1
2
3
4
5
    @RequestMapping("/hello")
public ModelAndView hello(@RequestParam String name, Model model) {
    model.addAttribute("name", name);
    return new ModelAndView("hello");
}

String

메소드의 리턴타입이 스트링이면 리턴 값은 뷰 이름으로 사용된다. 모델정보는 모델 맵 파라미터로 전달받아 추가해주는 방법을 사용해야 한다. Model 이나 ModelMap 파라미터를 전달받아서 여기에 모델 정보를 넣어주는 것이다.위의 컨트롤러 코드는 ModelAndView 오브젝트를 리턴하지만 필요한 것은 뷰 이름뿐이다. 따라서 다음과 같이 바꾸는 편이 더 깔끔하다.

1
2
3
4
5
@RequestMapping("/hello")
public String hello(@RequestParam String name, Model model) {
    model.addAttribute("name", name);
    return "hello";
}

모델은 파라미터로 전달받은 Map, Model, ModelMap 에 넣어주고 리턴값은 뷰 이름을 스트링 타입으로 선언하는 방식은 흔히 사용되는 @Controller 메소드 작성 방법이다. 컨트롤러 코드에서 모델을 추가해주고 뷰 이름을 지정해 주는 방법 중에서 가장 깔끔하기 때문이다.

void

메소드의 리턴 타입을 아예 void 로 할 수도 있다. 이때는 RequestToViewNameResolver 전략을 통해 자동생성되는 뷰 이름이 사용된다. URL 과 뷰 이름을 일관되게 통일할 수만 있다면 void 형의 사용도 적극 고려해볼 만하다. 위의 메소드는 다시 다음과 같이 더 단순하게 바꿀 수 있다.

1
2
3
4
@RequestMapping("/hello")
public void hello(@RequestParam String name, Model model) {
    model.addAttribute("name", name);
}

void 형 리턴타입이기 때문에 뷰 이름은 RequestToViewNameResolver 를 통해 자동생성된다. 디폴트로 등록된 RequestToViewNameResolver 는 URL 을 따라서 뷰 이름을 hello 로 만들어줄 것이다. 여기에 뷰 리졸버가 prefix, suffix 를 붙여주도록 하면 /WEB-INF/view/hello.jsp 같은 식으로 완성할 수 있다.

모델 오브젝트

뷰 이름은 RequestToVewNameResolver 로 자동생성하는 것을 사용하고 코드를 이용해 모델에 추가할 오브젝트가 하나뿐이라면, Model 파라미터를 받아서 저장하는 대신 모델 오브젝트를 바로 리턴해도 된다.스프링은 리턴타입이 미리 지정된 타입이나 void 가 아닌 단순 오브젝트라면 이를 모델 오브젝트로 인식해서 모델에 자동으로 추가해준다. 이때 모델 이름은 리턴값의 타입 이름을 따른다.

다음 메소드를 살펴보자. view() 메소드는 파라미터로 전달된 id 값을 이용해 User 오브젝트를 가져와서 바로 리턴해준다.메소드 리턴 타입은 User 타입으로 선언해뒀다. 이때는 리턴한 User 타입 오브젝트가 user 라는 이름으로 모델에 추가될 것이다. 뷰 이름은 제공할 기회가 없었으니 디폴트 RequestToViewNameResolver 를 통해 "view" 로 결정될 것이다.

1
2
3
4
@RequestMapping("/view")
public User view(@RequestParam int id) {
    return userService.getUser(id);
}

클래스 이름과 다른 모델 이름을 사용하고 싶다면, 메소드 레벨의 @ModelAttribute 를 사용해 모델 이름을 직접 지정해줄 수 있다.

Map/Model/ModelMap

메소드의 코드에서 Map 이나 Model, ModelMap 타입의 오브젝트를 직접 만들어서 리턴해주면 이 오브젝트는 모델로 사용된다. 컨트롤러 코드에서 필요한 모델 맵은 파라미터로 받아서 사용하면 편리하다. 직접 Map/Model/ModelMap 타입의 오브젝트를 코드에서 생성하는 건 비효율적이다. 따라서 맵을 리턴 타입으로 사용하는 일은 많지 않을 것이다.

이 리턴 타입의 특징은 분명하게 기억해둬야 한다. 단일 모델 오브젝트를 직접 리턴하는 방식을 사용하다가 실수할 수 있기 때문이다. 예를 들어 서비스 계층의 메소드에서 맵 타입으로 결과를 돌려주는 경우가 있다. SpringJDBC 의 API 를 보면 Map 타입의 결과를 돌려주는 메소드가 제법된다. 그런데 단일 오브젝트를 리턴하면 모델로 자동등록된다고 해서 다음과 같이 코드를 작성하면 안된다.

1
2
3
4
5
@RequestMapping("/view")
public Map view(@RequestParam int id) {
    Map userMap = userService.getUserMap(id);
    return userMap;
}

즉, 이코드에 의해 map 이라는 이름을 가진 맵 오브젝트가 모델에 추가될 것이라고 기대하면 안된다. Map 타입의 리턴 값은 그 자체로 모델맵으로 인식해서 그 안의 엔트리 하나하나를 개별적인 모델로 다시 등록해 버리기 때문이다. 따라서 Map 타입의 단일 오브젝트 리턴은 근본적으로 피하는 게 좋다.

위와 같은 코드는 원래 의도대로 하려면 다음과 같이 Model 오브젝트를 파라미터로 직접 받아서 코드에 추가해 줘야 한다.

1
2
3
4
@RequestMapping("/view")
public void view(@RequestParam int id, Model model) {
    model.addAttribute("userMap", userService.getUserMap(id));
}

View

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

1
2
3
4
5
6
7
8
9
10
11
12
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 의 출력스트림에 넣어 버린다.

1
2
3
4
5
@RequestMapping("/hello")
@ResponseBody
public String hello() {
    return "Hello Spring";
}

@ResponseBody 가 적용된 컨트롤러는 리턴 값이 단일 모델이고 메시지컨버터가 뷰와 같은 식으로 동작한다고 이해할 수 있다. 근본적으로 @RequestBody, @ResponseBody 는 XML 이나 JSON 과 같은 메시지 기반의 커뮤니케이션을 위해 사용된다


from 토비의 스프링 3   

블로그 이미지

532

처음 프로그래밍을 시작한 개발자들은 Hello World!를 기억 할 것이다. Hello world가 Hell World가 될 줄 몰랐던 한 개발자의 이야기. 게시글의 출처를 표기하며, 출처를 남기지 않고 펌을 하는것에 대해 법적 책임을 묻지 않습니다.

,