MVC의 @RequestMapping은 어떻게 데이터를 원하는 형태로 전달받을까?

저니·2023년 5월 13일
0

Spring

목록 보기
2/2
post-thumbnail

개요

애노테이션을 활용한 스프링 MVC의 컨트롤러를 작성하게되면, 다음과 같은 @RequestMapping 애노테이션만으로 HTTP 리퀘스트를 원하는 URL로 받고, 과 @RequestBody를 통해 HTTP body 값을 원하는 객체로 매핑해준다.

@Controller
public class HelloController {
	@RequestMapping("/hello") 
	public String hello(@RequestBody User user){
		// 생략
	}
}

이게 어떻게 가능한 이유는, 스프링에서 내부적으로 URL에 따라 해당 요청을 처리하는 메서드를 찾고, 파라미터의 애노테이션을 분석해 매핑시켜주는 과정을 거치기 때문인데, 이 부분에 대해 알아보려고 한다.

@RequestMapping 핸들러 매핑

스프링에서는 @ReqestMapping 애너테이션이 붙은 핸들러를 매핑하기 위해 내부적으로 HandlerMapping 인터페이스를 구현한 RequestMappingHandlerMapping 을 사용한다. 이 핸들러를 통해 컨트롤러의 각 메서드가 독립적인 요청을 처리할 수 있도록 지원한다.

단, 메서드 레벨에서만 선언하고, 클래스 레벨에서는 선언하지 않는 경우, 클래스 자체가 매핑 대상이 되지 않으므로 빈 @RequestMapping이라도 부여해야 한다. 하지만 @Controller를 붙여서 Bean 스캔이 대상이 되게끔 지정했다면, 클래스 레벨의 @RequestMapping를 생략할 수도있다.

@RequestMapping에서 매핑의 기준이 되는 정보들은 다음과 같다.

element조건
value(default)URL 패턴
method요청 메서드
params파라미터
headersHTTP 헤더
consumesContent-Type 헤더
producesAccept 헤더
// 이런식으로 작성할 수 있다
@RequestMapping(value = "/api/hello", method = RequestMethod.POST)

// 위 POST 메서드는 아래 어노테이션으로도 가능하다.
// 어노테이션 내부를 보면 @RequestMapping(method = RequestMethod.POST) 이 붙어있다.
@PostMapping(value = "/api/hello")

@RequestMapping의 파라미터 매핑

RequestMappingHandlerMapping 으로 요청을 처리할 메서드를 찾은 후, DispatcherServlet은 RequestMappingHandlerAdapter 를 사용해 요청을 처리하게 하는데, 이 어댑터에선 메서드 파라미터에 정의된 객체로 HTTP 요청을 매핑시키기 위해서 ArgumentResolver를 사용한다 (애노테이션에 따라 메시지 컨버터를 사용하기도 한다. ex. @RequestBody)

ArgumentResolver

// 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

서블릿의 HttpServletRequestHttpServletResponse이며, 모든 요청과 반환 정보를 다 포함하고 있다. ServletRequest, ServletResponse 도 가능하다.

@RequestMapping("/hello")
public String example(HttpServletRequest request, HttpServletResponse responese){
	...
}

@RequestMapping("/hello")
public String example(ServletRequest request, ServletResponse responese){
	...
}

HttpSession

HttpServletRequest를 통해 가져올 수도 있지만, 세션만 필요한 경우라면 HttpSession 파라미터를 선언해서 바로 받을 수 있다.

HttpSession은 서버에 따라서 멀티스레드 환경에서 안정성이 보장되지 않기 때문에, 멀티스레드 안전하게 사용하려면 어댑터의 synchronizeOnSession 프로퍼티를 true 로 설정해준다.

Locale

DispatcherServlet의 LocaleResolver가 결정한 Locale 오브젝트를 받을 수 있다.

InputStream, Reader

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

OutputStream, Writer

HttpServletRequest의 getOutputStream 을 통해서 받을 수 있는 콘텐트 스트림 또는 Writer 타입 오브젝트를 받을 수 있다.

@PathVariable

@RequestMapping의 URL에 {} 로 들어가는 path 변수를 받는다.

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

path 변수 타입과 일치하지 않는 값이 들어오게 된다면 HTTP 400( Bad Request) 응답이 가게 된다.

@RequestParam

단일 요청 파라미터를 메서드 파라미터에 넣어주는 애노테이션이다. 스프링 내장 변환기가 다룰 수 있는 모든 타입을 지원한다.

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로 해준다.

@CookieValue

요청과 함께 전달된 쿠키 값을 받을 수 있다. 파라미터 이름과 쿠키 값이 같다면 생략할 수 있다. 쿠키도 마찬가지로 없다면 HTTP 400 에러를 받게 되고, 선택적으로 받고 싶다면 required element를 false로 해준다.

public String check(@CookieValue("auth") String auth) { ... }

@RequestHeader

헤더 정보를 넣어주는 애노테이션이다. 가져올 헤더의 이름을 지정해서 받는다.

public void header(@RequestHeader("Host") String host,
									 @RequestHeader("Keep-Alive") long keepAlive)

Map, Model, ModelMap

다른 애노테이션이 붙어 있지 않으면 모델 정보를 담는데 사용하는 오브젝트가 전달된다. 스프링은 이에 담긴 모든 오브젝트를 자동 이름 생성 방식을 적용해서 모두 모델로 추가해준다

public void hello(ModelMap model) {
	User user = new User(1, "Spring");
	model.addAttribute(user);
}

@ModelAttribute

// request parameter들을 객체에 담아서 바로 받기
public String view(@ModelAttribute UserSearch search) {
	...
}

// @RequestParam 처럼 생략도 가능하다
public String view(UserSearch search) {
	...
}

클라이언트로부터 컨트롤러가 받는 요청정보 중에서, 하나 이상의 값을 가진 오브젝트 형태로 만들 수 있는 구조적인 정보를 @ModelAttribute 모델이라고 부른다. 요청 파라미터를 메서드 파라미터에서 1:1로 받는 @RequestParam과는 다르게, @ModelAtribute는 도메인 오브젝트나 DTO의 프로퍼티에 요청 파라미터를 바인딩해준다.

바인딩되는 과정은 다음과 같다.

  1. 파라미터 타입의 오브젝트를 디폴트 생성자를 이용해 만든다. 만약, @SessionAttributes에 의해 세션에 저장된 모델 오브젝트가 있다면, 새로운 오브젝트를 생성하는 대신 세션에 저장되어 있는 오브젝트를 가져온다.
  2. 준비된 모델 오브젝트의 프로퍼티에 웹 파라미터를 바인딩해준다.
    • 이 때 프로퍼티가 스트링 타입이 아니라면 PropertyEditorConverterFormatter를 통해 변환을 한다.
    • 이 때 실패하면 BindingResult 오브젝트에 바인딩 오류를 저장한다.
  3. 모델 값을 검증한다.

또한 @ModelAttribute는 모델 오브젝트를 자동으로 모델 맵에 추가해준다.

Errors, BindingResult

@ModelAttribute가 붙은 파라미터를 처리할 때는 @RequestParam과 달리 Validation 작업이 추가적으로 진행된다. 변환이 불가능하면, Errors와 BindingResult에 해당 오류가 저장된다.

@RequestMapping("/example")
public String example(@ModelAttribute User user, BindingResult bindingResult){
	...
}

@RequestParm이 바인딩이 불가능하면 바로 작업이 중단되고 400 - Bad Request 가 전달되는데, 왜 모델 바인딩에는 중단되지도 않고 결과가 Errors와 BindingResult에 저장되는 것일까?

  1. 그 이유는 @RequestParam과는 다르게 모델 오브젝트의 프로퍼티와 타입이 일치하는지 뿐만 아니라, 다른 여러 검증 작업 (필수정보의 입력 여부, 길이 제한, 포맷, 값의 허용범위 등) 을 수행하기 때문이다. 즉, 바로 중지가 된다면, 다른 검증 에러들이 담기지 않게되기 때문이다.
  2. 또한 웹사이트에서 회원가입을 하는 것을 예로 들 수 있다. 특정 항목이 잘못되었다고 400 - Bad Request 가 나면 사용자 입장에서 황당할 수 있다. 따라서 컨트롤러에게 에러 처리에 대해 맡기는 것이다.

주의할 점은 다음과 같다.

  1. 모델 어트리뷰트를 사용할 때는 Errors나 BindingResult 파라미터를 함께 사용하지 않으면 스프링이 바인딩에 문제가 없도록 애플리케이션이 보장해준다고 생각한다. 이 때는 문제가 생기면 BindingException 예외가 던져진다. 이 때는 따로 400 - Bad Request 로 변환되지도 않으니, 적절하게 예외처리를 해줘야한다.
  2. 또 사용시 반드시 주의할 점은, @ModelAttribute 파라미터 바로 뒤에 둬야한다는 점이다. 그 이유는 자신의 앞에 있는 @ModelAttribute 파라미터의 검증 작업에서 발생한 오류만을 전달해주기 때문이다.

SessionStatus

세션 내에 모델 오브젝트가 저장할 필요가 없을 때, 오브젝트를 세션에서 제거해주는 작업을 SessionStatus를 통해 할 수 있다.

@RequestMapping**(**"/example"**)
public** **String** **example(SessionStatus** sessionStatus**){**
	sessionStatus**.**setComplete**();
}**

@RequestBody

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가 붙은 파라미터가 있으면, 스프링은 다음과 같은 작업을 수행한다.

  1. HTTP 요청의 미디어 타입과 파라미터 타입을 먼저 확인한다.
  2. 메시지 변환기 중에서, 해당 미디어 타입과 파라미터 타입을 처리할 수 있는 것이 있다면, HTTP 요청의 Body 부분을 통째로 변환해서 지정된 메소드 파라미터로 전달해준다.

@Value

빈의 값 주입에서 사용하던 @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;
	}
	
}

@Valid

빈 검증기를 이용해서 모델 오브젝트를 검증하도록 지시하는 지시자다. 보통 @ModelAttribute와 함께 사용한다.

참고

0개의 댓글