이 포스팅은 김영한 강사님의 강의를 토대로 정리한 글입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
스프링에서 기본적으로 제공하고 실무에서도 많이 쓰이는 기본적인 기능들에 대해서 알아보겠다.
스프링 MVC 기본 기능
스프링에서 Welcome Page 만들기
서블릿에서 Welcome Page를 만드는 방법은 여기를 살펴보면 된다.
스프링 부트에 Jar를 사용하면 /resources/static/ 위치에 index.html 파일을 두면 Welcome Page로 처리해 준다.
스프링 공식 문서에선 이렇게 설명하고 있다.
로깅
스프링 부트 라이브러리를 사용하면, 스프링 부트 로깅 라이브러리(spring-boot-starter-logging)가 기본 제공하며, 다음 라이브러리들이다.
- SLF4 J
- Logback
로그 라이브러리는 Logback, Log4J, Log4J2 등등 수많은 라이브러리가 있는데, 이를 통합하여 인터페이스로 제공하는 것이 바로 SLF4 J 라이브러리다. 쉽게 얘기하면 SLF4J는 인터페이스고, 구현체로 Logback 같은 로그 라이브러리를 선택하면 된다.
로그는 다음 코드와 같이 선언하면 된다.
private final Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class);
@Slf4j // 롬복 사용
나는 항상 롬복을 사용해서 로깅을 찍는다.
그리고 로깅을 사용하는 예시 코드를 작성해 봤다.
@Slf4j
@RequestMapping("/error")
@Controller
public class MyExceptionHandler {
@GetMapping("/exception")
public String makeException() {
throw new IllegalArgumentException("error has occurred!");
}
@ExceptionHandler
@ResponseBody
public String exceptionHandler(IllegalArgumentException e) {
log.trace("error msg:{}", e.getMessage());
log.debug("error msg:{}", e.getMessage());
log.info("error msg:{}", e.getMessage());
log.warn("error msg:{}", e.getMessage());
log.error("error msg:{}", e.getMessage());
return "ok";
}
}
간단하게 "/error/exception"을 호출하면 예외를 던지는 메서드이고, 바로 아래 exceptionHandler에서 해당 예외를 잡는다.
그리고 log 메서드를 사용하여 왼쪽엔 format 형식을, 오른쪽엔 format의 "{}"에 개수와 순서에 맞게 적어주면 된다.
printf()나, String.format()과 비슷하며 단지 % s가 아닌 {}로 사용하는 것이다.
출력 결과 :
로깅을 사용하면, 스레드 정보, 클래스 이름과 같은 부가 정보를 함께 볼 수 있고, 출력 모양도 조정할 수 있다. 특히, 시스템 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크등, 로그를 별도의 위치에 남길 수 있다.
로그에 대한 더 자세한 내용
- LogBack : https://logback.qos.ch/
- SLF4 J : https://www.slf4j.org/
매핑 정보
- @RestController
- @Controller는 Return Type이 String인 경우, 뷰 이름으로 인식한다. 그래서 뷰를 찾고 뷰가 랜더링 된다.
- @RestController는 Return Type이 String 이어도 뷰 이름을 찾는 것이 아닌, HTTP 메시지 바디에 바로 반환한 문자열을 담는다. 따라서 return "ok"를 하면 다음과 같이 ok라는 문자열이 응답된다.
HTTP 메서드 매핑
@RequestMapping
@RequestMapping({"/hello-basic", "/hello-basic2"})
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
- 배열을 사용하여 "/hello-basic", "/hello-basic2"를 모두 매핑할 수도 있다. ( Or 조건)
- GET, HEAD, POST, PUT, PATCH, DELETE 모두 허용.
- 하지만 이 @RequestMapping은 어떠한 요청 메서드가 오더라도 항상 매핑이 되기 때문에 꼭 필요한 경우가 아니라면 이렇게 설계하는 것은 좋지 않다.
@GetMapping
// 1번 방법
@RequestMapping(value = "/hello-basic", method = RequestMethod.GET)
// 2번 방법
@GetMapping("/hello-basic")
public String mappingGet() {
log.info("mappingGet");
return "ok";
}
이렇게 메서드 매핑 애너테이션을 사용해서, 메서드를 제한할 수 있고, 1번 방법보다 2번이 더 직관직이고, 편리하다.
@PathVariable
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable Long userId, @PathVariable Long orderId) {
.......
}
URL 경로를 템플릿화 하여 @PathVariable을 사용하면 매칭되는 부분을 편리하게 사용할 수 있다.
Parameter 조건 매핑
/**
* 파라미터로 추가 매핑
* params="mode"
* params="!mode"
* params="mode=debug"
* params="mode!=debug"
* params={"mode=debug","data=good"}
*
* @return Body
*/
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
.....
}
특정 파라미터가 있거나 없는 조건을 추가할 수 있다. 잘 사용하진 않음.
Header 조건 매핑
/**
* 특정 헤더로 추가 매핑
* headers="mode"
* headers"!mode"
* headers="mode=debug"
* headers="mode!=debug"
* headers={"mode=debug","data=good"}
*/
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
......
}
파라미터 조건 매핑과 사용하는 방법은 비슷하지만 이건 HTTP 헤더를 사용한다.
Media Type 조건 매핑 : HTTP Request Content-Type, consumes
/**
* Content-Type 헤더 기반 추가 매핑
* content-Type은 스프링이 내부적으로 제공하는 기능이 있기에 headers 보다 consumes 사용이 바람직
* consumes 는 Request Body 의 Content-Type 의미
* consumes="application/json"
* consumes="!application/json"
* consume="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
@PostMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)
public String mappingConsumes() {
.......
}
클라이언트가 요청할 때 보내는 요청 메세지 바디부의 Content-Type을 의미한다.
즉
Media Type 조건 매핑 - HTTP 요청 Accept, produces
/**
* Accept 헤더 기반 Media Type
* produces="text/html"
* produces="!text/html"
* produces="text/*"
* produces="*\/*"
*/
@PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)
public String mappingProduces() {
......
}
서버가 클라이언트에게 제공하는 미디어 타입이다.
요청 파라미터 - @RequestParam
스프링이 제공하는 @RequestParam을 사용하면 요청 파라미터를 매우 편리하게 사용할 수 있다.
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(@RequestParam(required = false) String username,
@RequestParam(defaultValue = "0") int age)
{
log.info("username:{}, age:{}", username, age);
return "ok";
}
- @RequestParam : 파라미터 이름으로 바인딩한다.
- @RequestParam의 name(value) 속성이 파라미터 이름으로 사용.
- request.getParameter("username")
- 자동으로 타입 바인딩 가능
- @ResponseBody : 뷰 조회를 거치지 않고 HTTP message body에 바로 반환값 입력
@ResponseBody
@RequestMapping("/request-param-paramMap")
public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
log.info("username:{}. age:{}", paramMap.get("username"), paramMap.get("age"));
return "ok";
}
이와 같이 Map으로 다 받을 수도 있음. String엔 Parameter Key, Object엔 Parameter value가 대입됨.
@ModelAttribute
@ModelAttribute는 파라미터의 값을 자동으로 객체로 바인딩해 주고, Model에 추가까지 해준다.
@ResponseBody
@RequestMapping("/model-attribute")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("helloData:{}", helloData);
return "ok";
}
// 출력
2023-05-16 19:48:54.348 INFO 46803 --- [nio-8080-exec-2]
h.s.b.request.RequestParamController: helloData:HelloData(username=eun, age=20)
동작 방식 : Parameter에 @ModelAttribute가 있는 경우
- Parameter 객체를 생성한다.
- 요청 Parameter key로 Parameter 객체의 Property를 찾고, 해당 Property의 setter를 호출하여 파라미터 값을 바인딩한다.
- 예를 들어 Parameter key가 userId라면, setUserId()를 찾아서 호출하면서 값을 바인딩한다.
만약 age=abc와 같이 정수형 값으로 초기화되어야 하는데 문자가 들어가면 BindingException이 발생한다. 이런 바인딩 오류는 BindeingResult라는 것을 통해서 처리할 수 있다.
사실 @ModelAttribute도 @RequestParam과 같이 생략할 수도 있다. 적용되는 규칙은 다음과 같다.
하지만, 가독성이 떨어지기에 명시하는 게 좋다고 생각한다.
- 기본형 타입, String 또는 Number 래퍼 클래스 : @RequestParam 자동 적용
- 나머지(내가 만든 클래스의 객체) : @ModelAttribute 자동 적용, 하지만 argumentResolver로 지정해 둔 타입은 제외.
여기까지 모두 GET 메서드로 조회, 검색을 하거나 POST 방식으로 HTML Form으로 전송할 때 자주 쓰이는 애너테이션에 대해서 알아봤다. 요청 파라미터란 건 그냥 key=value 쌍의 형식인 데이터라고 생각하면 된다. 이 두 가지의 경우엔 @ModelAttribute, @RequestParam을 사용할 수 있다. 하지만 아래에서 알아볼 것은 API 통신에서 자주 쓰이는 방식들인데 지금까지 알아본 것과는 관계가 없으므로 연관을 짓지 말자. 연관 지으면 더 헷갈릴 뿐이다.
@HTTP Request Message - Simple TEXT
- HTTP message body에 데이터를 직접 담아서 요청
- HTTP API에서 주로 사용. JSON, XML, TEXT
- 데이터 형식은 주로 JSON이 많이 사용됨
- POST, PUT, PATCH가 쓰인다.
- 참고 : GET, DELETE는 메세지 바디가 없다. (엄연히 따지면 GET은 메세지 바디를 가질 순 있지만 권장되지 않는다.)
그냥 딱 이것만 기억하자.
parameter 데이터 형식이 아닌, HTTP 메세지 바디를 통해 데이터가 직접 넘어오는 경우엔 @RequestParam, @ModelAttribute와 저언혀 관계가 없다.
참고 : 왜 관계가 없을까?
그 이유는 바인딩 되는 방식 자체가 다르기 때문이다. 이전에 Servlet에서 JSON 데이터를 객체로 바꿀 때 Jackson 라이브러리를 사용했던 것과 TEXT를 inputStream 객체를 사용해서 String으로 가져올 수 있었다. 하지만 위에서 말했듯이 @ModelAttribute, @RequestParam은 객체의 프로퍼티나 key를 사용해서 바인딩 하였다. 이러한 바인딩 과정에서 명백히 차이가 있기 때문에, 구분을 지어뒀고 또 구분되어야 한다.
서블릿이 제공하는 방식 vs 스프링에서 제공하는 방식
// 서블릿 방식
@PostMapping("/request-body-string-servlet")
public void requestBodyString_Servlet(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody:{}", messageBody);
response.getWriter().write(messageBody);
}
// 스프링 방식
@PostMapping("/request-body-string-spring")
@ResponseBody
public String requestBodyString_Spring(@RequestBody String body) {
log.info("body:{}",body);
return "OK";
}
이외에도 스프링 MVC는 다음 파라미터를 지원한다.
HTTP Entity
- HTTP Entity : HTTP header, body 정보를 편리하게 조회할 수 있게 해줌.
- 요청 파라미터 조회하는 기능과 전혀 관계없음.
- HttpEntity는 응답에도 사용할 수 있다.
- 메세지 바디 정보를 직접 반환하고, 헤더 정보를 포함할 수 있다.(ex Status Code)
- view 조회를 거치지 않고 바로 반환된다.
- HTTP Entity를 상속받은 두가지의 객체가 있는데, 이는 더욱 많은 기능을 제공한다.
- RequestEntity : HttpMethod, url 정보가 추가. 요청에서 사용한다.
- ResponseEntity : Http 상태 코드를 설정 가능하다. 응답에서 사용함.
- return new ResponseEntity<>("good", HttpStatus.OK);
@RequestBody
- @RequestBody를 사용하면 HTTP 메세지 바디를 편리하게 조회할 수 있다.
- 바디 데이터가 아닌 헤더 데이터가 필요한 경우, @RequestHeader를 사용하면된다.
- @ResponseBody를 사용하면 응답 결과를 바로 메세지 바디에 담아서 반환이 가능하고 view를 거치지 않는다.
다시 말하지만, 이 두 가지는 절대 @ModelAttribute, @RequestParam과 관계 없다!
- 요청 파라미터를 조회할 때 : @RequestParam, @ModelAttribute를 상황에 맞게 선택해서 사용
- HTTP 메세지 바디를 직접 조회할 때 : @RequestBody
스프링 MVC 내부에서 HTTP 메세지 바디를 읽어서 문자나 객체로 변환해서 전달해주는데, 이때 HTTP 메시지 컨버터라는 기능을 사용한다. 이는 우리가 여기에서 번거러운 작업을 대신해준다.
@HTTP Request Message - JSON
JSON을 처리할 때 가장 많이 쓰이는 두 가지만 알아보겠다.
// 1. HttpEntity의 제네릭 타입을 보고 해당 객체를 바인딩해서 넣어줌으로써 getBody()를 통해 JSON 데이터를
// 편리하게 가져올 수 있다. 하지만 반드시 주의해야 할 점은 요청의 content-type이 application/json인지 확인이 필요.
@PostMapping("/request-body-json-v1")
public HttpEntity<HelloData> requestBody_1(HttpEntity<MemberForm> httpEntity) {
MemberForm memberForm = httpEntity.getBody();
log.info("username:{}, age:{}", memberForm.getUsername(), memberForm.getAge());
return new ResponseEntity<>(memberForm, HttpStatus.OK);
}
// @RequestBody로 바인딩 된 객체를 바로 가져올 수 있다.
// 응답에도 바로 객체를 반환하면 json 형식의 응답이 나간다.
@PostMapping("/request-body-json-2")
public HelloData requestBody_2(@RequestBody HelloData helloData) {
log.info("username:{}, age:{}", helloData.getUsername(), helloData.getAge());
return helloData;
}
*@RequestBody는 생략할 수 없다. 생략을 하게 된다면 위에서 말했듯이, 타입에 따라 @ModelAttribute 또는 @RequestParam이 적용되기 때문이다.
주의
HTTP 요청시, content-type이 application/json인지 반드시! 확인해야 한다.
- @RequestBody 요청
- content-type : json → HTTP 메세지 컨버터 동작 -> 객체
- @ResponseBody 응답(객체 타입)
- 객체 → HTTP 메세지 컨버터 동작 -> JSON 응답
자세한 건, 이후 HTTP Message Converter에서 정리.
HTTP RESPONSE - 정적 리소스, 뷰 템플릿
- 정적 리소스 : 웹 브라우저에 정적인 HTML, css, js를 제공할 때 정적 리소스 사용
- 뷰 템플릿 사용 : 웹 브라우저에 동적인 HTML을 제공할 때 사용
- HTTP 메세지 사용 : HTTP API를 제공하는 경우, HTML 파일 같은 것을 전달하는 게 아닌, 데이터를 전달하므로, HTTP 메세지 바디에 JSON 같은 형식으로 데이터를 보냄.
뷰 템플릿
뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달. 일반적으로 HTML을 동적으로 생성하는 용도로 사용.
Return type에 따른 설명은 이 공식 문서에서 아주 잘 정리되어 있다.
중요 HTTP 메세지 컨버터
뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아닌, HTTP API처럼 메세지 바디에 직접 데이터를 쓰거나 읽는 경우, HTTP 메세지 컨버터를 사용하면 편리하다.
스프링 MVC는 다음의 경우에만 HTTP 메세지 컨버터를 적용한다.
- HTTP 요청 시 : @RequestBody, HttpEntity(RequestEntity -> HTTP method, URL 정보 추가)
- HTTP 응답 시 : @RequestBody, HttpEntity(ResponseEntity -> Status Code 설정 가능)
HTTP MessageConverter Interface
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return (canRead(clazz, null) || canWrite(clazz, null) ?
getSupportedMediaTypes() : Collections.emptyList());
}
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
HTTP Message Converter는 HTTP 요청, 응답에 모두 사용된다.
- canReade(), canWrite() 메서드는 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 체크
- read(), write()는 지원한다면 메세지 컨버터를 이용해서 메세지를 읽고 쓴다.
여기서 클래스 타입이란 parameter type이나, return type을 의미한다.
참고로 JSON 응답이 오더라도 String으로 응답할 수도 있다.
중요. 메세지 컨버터
- ByteArrayHttpMessageConverter : byte[] 데이터 처리
- 클래스 타입: byte[], 미디어 타입 : */*
- 요청에서 (parameter) : @RequestBody byte[] data
- 응답에서 (return) : @ResponseBody return byte[] || Accept Header : application/octet-stream
- StringHttpMessageConverter : String 문자로 데이터 처리
- 클래스 타입: String, 미디어 타입 : */*
- 요청에서 (parameter) : @RequestBody String data
- 응답에서 (return) : @ResponseBody return "ok" || Accept Header : text/plain
- MappingJackson2HttpMessageConverter : application/json
- 클래스 타입 : 객체 또는 HashMap, 미디어 타입 application/json 관련
- 요청에서 (parameter) : @RequestBody HelloData data
- 응답에서 (return) : @ResponseBody return data || Accept Header : application/json 관련
응답은 @RestController에서 반환하는 타입보다 Accept의 우선순위를 먼저 적용시킨다는 점을 기억하자. 따라서 응답의 경우엔, 컨트롤러에서 크게 신경을 쓰지 않아도, Accept에 따라서 응답할 수 있다.
하지만 요청을 받을 때의 경우는 다르다.
나는 이 부분이 헷갈리기 쉽다고 생각해서, 몇가지 체크사항을 생각했고 포스트 맨으로 확인해보았다.
결과:
- parameter 또는 return-type이 String, byte[] 인 경우
- content-type에 상관없이 모두 String으로 받아올 수 있었다. JSON도 String으로 받아올 수 있다.
- return 타입이 String, byte인 경우엔 Accept 헤더가 없으면, 기본 쓰기 미디어 타입으로 응답하고, Accep 헤더가 있다면, 반환타입에 상관없이 Accept 타입이 우선으로 적용되어 응답이 되었다. (String을 반환하더라도, "Accept: application/json"이면, JSON응답)
- parameter 또는 return-type이 객체인 경우
- 요청 시, parameter가 객체라면 반드시 content-type이 json 관련이어야 했다.
- return 타입이 객체인 경우엔 반드시 Accept:"application/json" 관련이어야 정상 응답했다.
결론 :
parameter나 return type이 객체인 경우엔 반드시 content-type 또는 Accept 헤더가 "application/json"과 관련이 있어야 정상 작동한다. 하지만 나머지의 경우엔 타입에 관계없이 내가 테스트 한 범위에선 모두 바인딩이 가능했다.
또한 실제 응답 타입은 컨트롤러의 반환 타입보다 클라이언트의 Accept가 우선시 되어 응답되었다.
이렇게, 스프링은 매우 유연하게 값을 꺼내오고 작성할 수 있다. 과연 이런게 진짜 아무런 구현도 안되어있는데 이렇게 사용할 수 있을까? 그렇지 않다. 이제는 이러한 편리한 기능을 사용할 수 있게 해주는 구성 요소들을 알아보겠다.
요청 매핑 핸들러 어댑터 구조 [중요]
이렇게 편리한 기능을 사용할 수 있게 해주는 건 모두 애너테이션 기반의 컨트롤러(@Controller)에 있다.
더 정확하게 말하자면 이를 처리해주는 RequestMappingHandlerAdapter에 있는 것이다.
HandlerMethodArgumentResolver
RequestMappingHandlerAdapter는 애너테이션 기반 컨트롤러를 담당하는 어댑터이다. 이 어댑터는 사실 단순히 컨트롤러를 호출해주는 것에 그치지 않고, 컨트롤러 메서드가 필요로 하는 객체를 HandlerMethodArgumentResolver를 호출해서 객체를 생성한 후 파라미터로 전달해준다. @ModelAttribute, @RequestParam, @RequestBody, HttpEntity(RequestEntity) 모두 이 친구 덕분에 편리하게 바인딩될 수 있었다. 해당 컨트롤러가 받을 수 있는 parameter가 무엇이 있는지 확인하고 싶으면 공식 문서 링크를 확인하자.
(당연히, 애너테이션 기반이 아닌 컨트롤러는 이 어댑터가 작동하지 않기 때문에, ArgumentResolver가 동작하지도 않을 뿐더러, 그런 메서드를 구현할 수도 없다. 이런 특징 때문에 스프링 MVC 애너테이션 기반 컨트롤러가 매우 유연한 것이다.)
동작 방식
/**
* Strategy interface for resolving method parameters into argument values in
* the context of a given request.
*/
public interface HandlerMethodArgumentResolver {
/**
* Whether the given {@linkplain MethodParameter method parameter} is
* supported by this resolver.
* @param parameter the method parameter to check
* @return {@code true} if this resolver supports the supplied parameter;
*/
boolean supportsParameter(MethodParameter parameter);
/**
* Resolves a method parameter into an argument value from a given request.
* @param parameter the method parameter to resolve. This parameter must
* have previously been passed to {@link #supportsParameter} which must
* have returned {@code true}.
* @param binderFactory a factory for creating {@link WebDataBinder} instances
* @return the resolved argument value, or {@code null} if not resolvable
*/
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
주석에 매우 정확하게 기능들이 적혀있어서 중요한 주석만을 남겨두었다. 하지만 해석하기 귀찮은 분들을 위해 간단히 정리해보자면,
supportsParameter()를 사용해서 해당 파라미터 타입을 지원하는지 체크한다. 그리고 resolveArgument()가 실제 파라미터 객체를 생성한다. 이렇게 필요한 파라미터 객체들을 모두 생성하고 컨트롤러를 호출하여 객체들이 전달되는 것이다.
그럼 어떤게 있는지 사진으로 확인해보자.
이건 여러분이 가장 익술할 만한 @RequestParam을 처리하는 argumentResolver인데, 핵심 코드만 뽑아봤다.
/**
* Resolves method arguments annotated with @{@link RequestParam}, arguments of
* type {@link MultipartFile} in conjunction with Spring's {@link MultipartResolver}
* abstraction, and arguments of type {@code javax.servlet.http.Part} in conjunction
* with Servlet 3.0 multipart requests.
public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
implements UriComponentsContributor {
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
private final boolean useDefaultResolution;
/**
* Supports the following:
* @RequestParam-annotated method arguments.
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
// @RequestParam을 가지고 있는지 체크.
if (parameter.hasParameterAnnotation(RequestParam.class)) {
// 이건 해당 파라미터가 Map인 경우 key 값이 비어있는지에 대한 검사를 한다.
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
return (requestParam != null && StringUtils.hasText(requestParam.name()));
}
else {
return true;
}
}
}
}
// 실제로 파라미터를 생성하는 메서드이다. 부모인 AbstractNamedValueMethodArgumentResolver에서 구현되어 있다.
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
// 여기에서 파라미터 객체가 생성된다.
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
return arg;
}
HandlerMethodReturnValueHandler
이것도 역시 argumentResolver와 비슷한 역할을 하고, 정확히는 호출된 핸들러 메서드에서 반환된 값을 처리하는 인터페이스이다.
역시 자세한 반환 타입에 따른 처리 방식은 이 공식 문서를 확인하자.
public interface HandlerMethodReturnValueHandler {
/**
* Whether the given {@linkplain MethodParameter method return type} is
* supported by this handler.
*/
boolean supportsReturnType(MethodParameter returnType);
/**
* Handle the given return value ....
*/
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
supportsReturnType()은 이 핸들러의 반환타입을 지원하는지 체크하고, handleReturnValue()는 반환된 값을 처리하는 역할을 한다.
우리가 컨트롤러를 반환할 때 주로 String으로 많이 사용하니까 이 클래스의 코드만 간략히 확인해보자.
public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
@Nullable
private String[] redirectPatterns;
/**
* The configured redirect patterns, if any.
*/
@Nullable
public String[] getRedirectPatterns() {
return this.redirectPatterns;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
if (returnValue instanceof CharSequence) {
String viewName = returnValue.toString();
mavContainer.setViewName(viewName);
if (isRedirectViewName(viewName)) {
mavContainer.setRedirectModelScenario(true);
}
}
else if (returnValue != null) {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
}
}
/**
* Whether the given view name is a redirect view reference.
* The default implementation checks the configured redirect patterns and
* also if the view name starts with the "redirect:" prefix.
* @param viewName the view name to check, never {@code null}
* @return "true" if the given view name is recognized as a redirect view
* reference; "false" otherwise.
*/
protected boolean isRedirectViewName(String viewName) {
return (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:"));
}
}
여길 보면, 반환된 String 값으로 뷰를 찾기도 하고, redirect인지 파악하는 메서드도 여기에 정의 되어있다.
이렇게 해서, 실제 Handler Method 가 호출될 때 어떻게 필요한 객체들이 자동으로 바인딩 되어 전달되고, 반환값에 따른 여러 전략 처리들이 동작되는지 이해할 수 있었을 것이다. 우리가 파라미터로 받을 수 있는 객체 타입과 모든 반환 타입이 이 두가지 인터페이스를 구현했다. 참고로 애너테이션 기반이 아닌 컨트롤러는 이 두가지 인터페이스가 작동하지 않다고 보면 된다.
하지만 아직 알아보지 않은 것이 있는데 그건 HTTP Message Converter이다.
HTTP Message Converter
결론부터 말하자면 HTTP Message Converter의 위치는 ArgumentResolver, ReturnValueHandler가 필요한 경우에 불러서 동작하게 된다.
요청의 경우와 응답의 경우 별반 같은 구성요소를 사용하기에 한번에 묶어서 보여주겠다.
HttpEntityMethodProcessor
/**
* Resolves {@link HttpEntity} and {@link RequestEntity} method argument values
* and also handles {@link HttpEntity} and {@link ResponseEntity} return values.
*
public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
........
}
주석을 보면 HttpEntityMethodProcessor는 HttpEntity(Request,ResponseEntity) 모두 처리한다는 것을 알 수 있다.
즉 요청과 응답 모두 HttpEntity와 그 자손들이 사용된 경우엔 HttpEntityMethodProcessor가 동작하여 HttpMessagConverter를 사용한다.
RequestResponseBodyMethodProcessor
/**
* Resolves method arguments annotated with {@code @RequestBody} and handles return
* values from methods annotated with {@code @ResponseBody} by reading and writing
* to the body of the request or response with an {@link HttpMessageConverter}.
*/
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
........
}
이것도 주석을 보면 메서드 인자들에 @RequestBody 애너테이션이 적용되고, @ResponseBody가 적용된 메서드들로부터 반환된 값을 처리할 때 모두 RequestResponseBodyMethodProcessor가 동작하여 HttpMessageConverter를 사용하여 요청 또는 응답의 바디부에 읽고 쓰는 작업을 한다.
이게 정리해보고 나면, HandlerMethod가 들어간 애들은 무조건 애너테이션 기반과 관련이 되어있었던거 같다.
정리 : 애너테이션 기반 컨트롤러와 관련된 것들
- 1. 핸들러 매핑 : RequestMappingHandlerMapping 이 담당
- 2. 핸들러 어댑터 : RequestMappingHandlerAdapter 가 담당
- 3. parameter 처리할 때(요청)
- @RequestParam, @ModelAttribute : 지원되는 HandlerMethodArgumentResolver 중에서 동작
- @RequestBody : RequestResponseBodyMethodProcessor → HttpMessageConverter 동작
- HttpEntity(RequestEntity) : HttpEntityMethodProcessor → HttpMessageConverter 동작
- 4. return type(응답)
- String, Model, ModelAndView, etc.. : 지원되는 HandlerMethodReturnValueHandler 중에서 동작
- 아래의 경우엔, View를 거치지 않고 바로 메세지 바디에 데이터를 담아서 응답.
- @ResponseBody : RequestResponseBodyMethodProcessor → HttpMessageConverter 동작
- HttpEntity(ResponseEntity) : HttpEntityMethodProcessor → HttpMessageConverter동작
'스프링 > 스프링 MVC' 카테고리의 다른 글
검증2 BeanValidation (0) | 2023.05.21 |
---|---|
검증 validation (0) | 2023.05.19 |
스프링 MVC 구조 (0) | 2023.05.15 |
MVC 프레임워크 (0) | 2023.05.14 |
Servlet의 한계, 템플릿 엔진, MVC 패턴 (0) | 2023.05.13 |