애노테이션을 활용한 스프링 MVC의 컨트롤러를 작성하게되면, 다음과 같은 @RequestMapping 애노테이션만으로 HTTP 리퀘스트를 원하는 URL로 받고, 과 @RequestBody를 통해 HTTP body 값을 원하는 객체로 매핑해준다.
@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello(@RequestBody User user){
// 생략
}
}
이게 어떻게 가능한 이유는, 스프링에서 내부적으로 URL에 따라 해당 요청을 처리하는 메서드를 찾고, 파라미터의 애노테이션을 분석해 매핑시켜주는 과정을 거치기 때문인데, 이 부분에 대해 알아보려고 한다.
스프링에서는 @ReqestMapping
애너테이션이 붙은 핸들러를 매핑하기 위해 내부적으로 HandlerMapping 인터페이스를 구현한 RequestMappingHandlerMapping
을 사용한다. 이 핸들러를 통해 컨트롤러의 각 메서드가 독립적인 요청을 처리할 수 있도록 지원한다.
단, 메서드 레벨에서만 선언하고, 클래스 레벨에서는 선언하지 않는 경우, 클래스 자체가 매핑 대상이 되지 않으므로 빈 @RequestMapping이라도 부여해야 한다. 하지만 @Controller를 붙여서 Bean 스캔이 대상이 되게끔 지정했다면, 클래스 레벨의 @RequestMapping를 생략할 수도있다.
@RequestMapping에서 매핑의 기준이 되는 정보들은 다음과 같다.
element | 조건 |
---|---|
value(default) | URL 패턴 |
method | 요청 메서드 |
params | 파라미터 |
headers | HTTP 헤더 |
consumes | Content-Type 헤더 |
produces | Accept 헤더 |
// 이런식으로 작성할 수 있다
@RequestMapping(value = "/api/hello", method = RequestMethod.POST)
// 위 POST 메서드는 아래 어노테이션으로도 가능하다.
// 어노테이션 내부를 보면 @RequestMapping(method = RequestMethod.POST) 이 붙어있다.
@PostMapping(value = "/api/hello")
RequestMappingHandlerMapping
으로 요청을 처리할 메서드를 찾은 후, DispatcherServlet은 RequestMappingHandlerAdapter
를 사용해 요청을 처리하게 하는데, 이 어댑터에선 메서드 파라미터에 정의된 객체로 HTTP 요청을 매핑시키기 위해서 ArgumentResolver를 사용한다 (애노테이션에 따라 메시지 컨버터를 사용하기도 한다. ex. @RequestBody)
// RequestMappingHandlerAdapter.java에 정의된 기본 ArgumentResolver
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);
// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
resolvers.add(new ErrorsMethodArgumentResolver());
resolvers.add(new SessionStatusMethodArgumentResolver());
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
if (KotlinDetector.isKotlinPresent()) {
resolvers.add(new ContinuationHandlerMethodArgumentResolver());
}
// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all
resolvers.add(new PrincipalMethodArgumentResolver());
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));
return resolvers;
}
이젠 @RequestMapping이 붙은 메서드에서 사용할 수 있는 파라미터의 종류에 대해 알아보자
서블릿의 HttpServletRequest
, HttpServletResponse
이며, 모든 요청과 반환 정보를 다 포함하고 있다. ServletRequest
, ServletResponse
도 가능하다.
@RequestMapping("/hello")
public String example(HttpServletRequest request, HttpServletResponse responese){
...
}
@RequestMapping("/hello")
public String example(ServletRequest request, ServletResponse responese){
...
}
HttpServletRequest를 통해 가져올 수도 있지만, 세션만 필요한 경우라면 HttpSession 파라미터를 선언해서 바로 받을 수 있다.
HttpSession은 서버에 따라서 멀티스레드 환경에서 안정성이 보장되지 않기 때문에, 멀티스레드 안전하게 사용하려면 어댑터의 synchronizeOnSession
프로퍼티를 true
로 설정해준다.
DispatcherServlet의 LocaleResolver가 결정한 Locale 오브젝트를 받을 수 있다.
HttpServletRequest의 getInputStream
을 통해서 받을 수 있는 콘텐트 스트림 또는 Reader
타입 오브젝트를 받을 수 있다.
HttpServletRequest의 getOutputStream
을 통해서 받을 수 있는 콘텐트 스트림 또는 Writer
타입 오브젝트를 받을 수 있다.
@RequestMapping의 URL에 {}
로 들어가는 path 변수를 받는다.
@RequestMapping("/user/view/{id}")
public String view(@PathVariable("id") int id) {
...
}
path 변수 타입과 일치하지 않는 값이 들어오게 된다면 HTTP 400( Bad Request) 응답이 가게 된다.
단일 요청 파라미터를 메서드 파라미터에 넣어주는 애노테이션이다. 스프링 내장 변환기가 다룰 수 있는 모든 타입을 지원한다.
public String view(@RequestParam("id") int id, @RequestParam("name") String name) {
...
}
// 단순 자바 타입인 경우엔 생략도 가능하다
public String view(int id, String name) {
...
}
// 또는 `Map`을 활용해 여러개를 입력받을 수도 있다.
public String view(@RequestParam Map<String, String> params) {
...
}
@RequestParam을 사용했다면 해당 파라미터가 반드시 있어야만 하고, 없다면 HTTP 400 에러를 받게 된다.
선택적으로 받고 싶다면 required
element를 false로 해준다.
요청과 함께 전달된 쿠키 값을 받을 수 있다. 파라미터 이름과 쿠키 값이 같다면 생략할 수 있다. 쿠키도 마찬가지로 없다면 HTTP 400 에러를 받게 되고, 선택적으로 받고 싶다면 required
element를 false로 해준다.
public String check(@CookieValue("auth") String auth) { ... }
헤더 정보를 넣어주는 애노테이션이다. 가져올 헤더의 이름을 지정해서 받는다.
public void header(@RequestHeader("Host") String host,
@RequestHeader("Keep-Alive") long keepAlive)
다른 애노테이션이 붙어 있지 않으면 모델 정보를 담는데 사용하는 오브젝트가 전달된다. 스프링은 이에 담긴 모든 오브젝트를 자동 이름 생성 방식을 적용해서 모두 모델로 추가해준다
public void hello(ModelMap model) {
User user = new User(1, "Spring");
model.addAttribute(user);
}
// request parameter들을 객체에 담아서 바로 받기
public String view(@ModelAttribute UserSearch search) {
...
}
// @RequestParam 처럼 생략도 가능하다
public String view(UserSearch search) {
...
}
클라이언트로부터 컨트롤러가 받는 요청정보 중에서, 하나 이상의 값을 가진 오브젝트 형태로 만들 수 있는 구조적인 정보를 @ModelAttribute 모델이라고 부른다. 요청 파라미터를 메서드 파라미터에서 1:1로 받는 @RequestParam과는 다르게, @ModelAtribute는 도메인 오브젝트나 DTO의 프로퍼티에 요청 파라미터를 바인딩해준다.
바인딩되는 과정은 다음과 같다.
PropertyEditor
, Converter
, Formatter
를 통해 변환을 한다.BindingResult
오브젝트에 바인딩 오류를 저장한다.또한 @ModelAttribute는 모델 오브젝트를 자동으로 모델 맵에 추가해준다.
@ModelAttribute가 붙은 파라미터를 처리할 때는 @RequestParam과 달리 Validation 작업이 추가적으로 진행된다. 변환이 불가능하면, Errors
와 BindingResult
에 해당 오류가 저장된다.
@RequestMapping("/example")
public String example(@ModelAttribute User user, BindingResult bindingResult){
...
}
@RequestParm이 바인딩이 불가능하면 바로 작업이 중단되고 400 - Bad Request
가 전달되는데, 왜 모델 바인딩에는 중단되지도 않고 결과가 Errors
와 BindingResult
에 저장되는 것일까?
400 - Bad Request
가 나면 사용자 입장에서 황당할 수 있다. 따라서 컨트롤러에게 에러 처리에 대해 맡기는 것이다.주의할 점은 다음과 같다.
Errors
나 BindingResult
파라미터를 함께 사용하지 않으면 스프링이 바인딩에 문제가 없도록 애플리케이션이 보장해준다고 생각한다. 이 때는 문제가 생기면 BindingException
예외가 던져진다. 이 때는 따로 400 - Bad Request
로 변환되지도 않으니, 적절하게 예외처리를 해줘야한다.세션 내에 모델 오브젝트가 저장할 필요가 없을 때, 오브젝트를 세션에서 제거해주는 작업을 SessionStatus
를 통해 할 수 있다.
@RequestMapping**(**"/example"**)
public** **String** **example(SessionStatus** sessionStatus**){**
sessionStatus**.**setComplete**();
}**
HTTP 요청의 본문이 그대로 전달된다.
RequestMappingHandlerAdapter
에 있는 HttpMessageConverter
가 미디어 타입 or 파라미터 타입을 확인한 후 지정된 메서드 파라미터로 변환해준다.
public void message(@RequestBody String body) { ... }
XML 또는 JSON 기반의 메시지를 사용하는 요청의 경우에 매우 유용하며, MessageConverter
에 의해 해당 타입으로 변환이 된다.
// RequestMappingHandlerAdapter.java
private void initMessageConverters() {
if (!this.messageConverters.isEmpty()) {
return;
}
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter()); <-- 모든 종류의 미디어 타입을 String 타입으로 변환해준다.
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); <-- 여기서 JSON, XML 등의 메시지 컨버터를 모두 가져온다
}
// AllEncompassingFormHttpMessageConverter 생성자 내부
public AllEncompassingFormHttpMessageConverter() {
// ...
addPartConverter(new JsonbHttpMessageConverter()); <-- 미디어 타입이 Json일 때, 모델 오브젝트로 변환해준다.
// ...
}
@RequestBody가 붙은 파라미터가 있으면, 스프링은 다음과 같은 작업을 수행한다.
빈의 값 주입에서 사용하던 @Value 애노테이션도 메서드 파라미터에 부여할 수 있다. 주로 시스템 프로퍼티나, 다른 빈의 프로퍼티 값, 특정 메서드를 호출한 결과 값, 조건식 등을 넣을 수 있다.
@RequestMapping(...)
public String hello(@Value("#{systemProperties['os.name']}" String osName) {
...
}
// 컨트롤러도 일반적인 스프링 빈이기 때문에 @Value를 메서드 파라미터 대신 컨트롤러 필드에 DI 해주는 것이 가능하다.
public class HelloController {
@Value("#{systemProperties['os.name']}") String osName
@RequestMapping(...)
public String hello(@Value("#{systemProperties['os.name']}" String osName) {
String osName = this.osName;
}
}
빈 검증기를 이용해서 모델 오브젝트를 검증하도록 지시하는 지시자다. 보통 @ModelAttribute와 함께 사용한다.