김영한님의 스프링 MVC 1편 강의를 듣고 정리하는 글입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
1. 개요
스프링 MVC에서는 HTTP 헤더, 쿼리파라미터, 메시지 바디 등 개발자가 일일이 변환할 필요 없이 스프링 mvc가 원하는 타입(InputStream, byte[], String, 객체 등)으로 요청과 응답을 변환해 주는 기능을 제공한다.
이로 인해 개발자는 메시지를 변환할 필요 없이 비즈니스 로직에만 집중할 수 있어 HTTP 요청/응답 관련된 세부적인 처리를 스프링 MVC가 처리한다.
위 강의를 수강하면서 스프링 MVC가 메시지를 처리하는 다양한 방법과 원리를 이해할 수 있었다.
스프링 MVC에서 제공하는 HTTP 요청/응답 - 처리 방식
요청 처리 - header
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String headers(
HttpServletRequest request,
HttpServletResponse response,
HttpMethod httpMethod,
Locale locale,
@RequestHeader MultiValueMap<String, String> headerMap,
@RequestHeader("host") String host,
@CookieValue(value = "myCookie", required = false) String cookie
) {
log.info("request={}", request);
log.info("response={}", response);
log.info("httpMethod={}", httpMethod);
log.info("locale={}", locale);
log.info("headerMap={}", headerMap);
log.info("header host={}", host);
log.info("myCookie={}", cookie);
return "ok";
}
}
요청 처리 - 쿼리파라미터, form 데이터
@Slf4j
@Controller
public class RequestParamController {
@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
log.info("username={}, age={}", username, age);
response.getWriter().write("ok");
}
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge
) {
log.info("username={}, age={}", memberName, memberAge);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam String username,
@RequestParam int age
) {
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(
String username,
int age
) {
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age
) {
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
@RequestParam(defaultValue = "guest") String username,
@RequestParam(defaultValue = "-1") int age
) {
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamDefault(@RequestParam Map<String, Object> paramMap) {
log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
return "ok";
}
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
}
요청 처리 - 단순 텍스트
@Slf4j
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v1")
public void requestBodyString(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("ok");
}
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
responseWriter.write("ok");
}
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV3(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
return "ok";
}
}
요청 처리 - JSON
@Slf4j
@Controller
public class RequestBodyJsonController {
private ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
response.getWriter().write("ok");
}
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
HelloData helloData = httpEntity.getBody();
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return helloData;
}
}
응답 처리 - veiw template
@Controller
public class ResponseViewController {
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1() {
ModelAndView mav = new ModelAndView("response/hello");
mav.addObject("data", "hello!");
return mav;
}
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model) {
model.addAttribute("data", "hello");
return "response/hello";
}
@RequestMapping("/response/hello")
public void responseViewV3(Model model) {
model.addAttribute("data", "hello");
}
}
응답처리 - 단순텍스트, JSON
Slf4j
@Controller
public class ResponseBodyController {
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok");
}
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
return new ResponseEntity<>("ok", HttpStatus.OK);
}
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
@GetMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return new ResponseEntity<>(helloData, HttpStatus.OK);
}
@ResponseStatus(HttpStatus.OK)
@GetMapping("/response-body-json-v1")
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
2. HTTP 메시지 컨버터
뷰 템플릿으로 HTML을 동적으로 생성해서 응답하는 게 아니라, REST API(JSON, XML 등) 형식의 데이터를 HTTP 메시지 바디에 읽거나 쓰는 경우 'HTTP Message Converter' 를 사용하여 손쉽게 body의 데이터를 조작할 수 있다.
스프링 mvc는 다음과 같은 경우 HTTP 메시지 컨버터를 적용한다.
- HTTP 요청: @RequestBody, HttpEntity(RequestEntity)
- HTTP 응답: @ResponseBody, HttpEntity(ResponseEntity)
a. HTTPMessageConverter
- 스프링에서 인테페이스로 제공한다.
- 해당 인터페이스는 요청과 응답에 모두 사용된다.
- canRead(), canWrite(): 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 검사
- read(), write() 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능
package org.springframework.http.converter;
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> var1, @Nullable MediaType var2);
boolean canWrite(Class<?> var1, @Nullable MediaType var2);
List<MediaType> getSupportedMediaTypes();
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return !this.canRead(clazz, (MediaType)null) && !this.canWrite(clazz, (MediaType)null) ? Collections.emptyList() : this.getSupportedMediaTypes();
}
T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;
void write(T var1, @Nullable MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}
b. 스프링 부트 기본 메시지 컨버터
- ByteArrayHttpMessageConverter
- byte[] 데이터 처리
- 클래스 타입: byte[],
- 미디어 타입: */*
- 요청ex) @RequestBody byte[]
- 응답ex) @ResponseBody return byte[] / 응답 미디어타입: application/octet-stream
- StringHttpMessageConverter
- String 데이터 처리
- 클래스 타입: String
- 미디어타입: */*
- 요청ex) @RequestBody String data
- 응답ex) @ResponseBody return "ok" / 응답 미디어타입: text/plain
- MappingJackson2HttpMessageConverter
- json 형식 데이터 처리
- 클래스 타입 객체 또는 Map
- 미디어타입: application/json 관련
- 요청ex) @RequestBody HelloData
- 응답ex) @ResponseBody return helloData / 응답 미디어타입 application/json
우선순위대로 1, 2, 3 나열했으며, 만족하지 않으면 다음 우선순위의 컨버터로 넘어간다.
3. HTTP 메시지 컨버터와 MVC 구조
HTTP 메시지 컨버터는 스프링 MVC에서 어디쯤에서 사용되는지 알아보자.
a. 스프링 MVC 구조
1. 핸들러 조회
2. 핸들러를 처리할 수 있는 핸들러 어뎁터 조회
3. handle() 메서드 호출
4. handler 호출
5. ModelAndView 반환
6. ViewResolver 호출
7. view 반환
8 render
스프링 mvc 구조는 다음과 같이 동작한다.
HTTP 메시지 컨버터는 어노테이션 기반(@RequestMapping)의 컨트롤러에서 동작하기 때문에, 어노테이션 기반 핸들러를 처리하는 RequestMappingHadnlerAdapter 위에서 동작한다.
b. RequestMappingHandlerAdapter 동작 방식
c. ArgumentResolver(요청 파라미터 처리)
- 어노테이션 기반 컨트롤러에서는 메서드 파라미터로 HttpservletRequest, HttpServletResponse, @RequestBody, @RequestParam, @ModelAttribute 등 다양한 형식의 파라미터를 수용할 수 있었다.
- 유연하게 파라미터를 처리할 수 있는 이유는 30개가 넘는 ArgumentResolver를 기본으로 제공하기 때문이다.
- supportsParamter()를 호출해서 해당 파라미터를 지원하는지 체크하고, 지원하면 resolverArgument()를 호출해서 실제 객체를 생성한다. 생성된 객체를 컨트롤러 호출 시 넘어감.
package org.springframework.web.method.support;
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter var1);
@Nullable
Object resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4) throws Exception;
}
d. ReturnValueHandler(응답값 처리)
- ArgumentResolver랑 비슷하지만, ReturnValueHandler는 응답값을 변환하고 처리한다.
- 응답값을 유연하게 처리하기 위해 10개가 넘는 ModelAndView, @ResponseBody, HttpEntity, String 등을 제공한다.
public interface HandlerMethodReturnValueHandler {
boolean supportsReturnType(MethodParameter var1);
void handleReturnValue(@Nullable Object var1, MethodParameter var2, ModelAndViewContainer var3, NativeWebRequest var4) throws Exception;
}
e. HTTP 메시지 컨버터의 위치
요청 시
- @RequetsBody를 처리하는 ArgumentResolver에서 HTTP 메시지 컨버터를 사용해서 필요한 객체 생성.
응답 시
- @ResponseBody를 처리하는 ReturnValueHandler에서 HTTP 메시지 컨버터를 호출해서 결과를 받음.
3. 마무리
스프링에서 제공하는 기능은 매우 막강하며, 필요한 것들은 대부분 구현되어 있을 것이다. 하지만 기능 확장이 필요하다면 WebMvcConfigurer를 검색해서 기능을 확장해 나갈 수 있다.
포스팅만 봐서 스프링 MVC 전체 구조에 대해서 이해하기는 쉽지 않을 것이다. 이해가 필요하다면 해당 강의를 꼭 듣기를 추천한다.
'Programming > Spring' 카테고리의 다른 글
[Spring] Spring에서 비동기 @Async 이해하기 (0) | 2024.08.23 |
---|---|
[Spring] Spring 단위 테스트 작성 (JUnit5, Mockito) (0) | 2024.04.04 |
[Spring] 스프링 MVC 1편(MVC 구조 이해) (0) | 2024.04.02 |
[Spring] 스프링 MVC 1편(MVC 프레임워크 만들기) (4) | 2024.03.14 |
[Spring] 스프링 MVC 1편(서블릿, JSP MVC 패턴) (1) | 2024.02.27 |