지난 포스팅에서는 스프링 MVC 프레임워크의 전체적인 구조, 아키텍쳐에 대해서 알아봤다.
이번 포스팅에서는 본격적으로 스프링 MVC를 다루는 방법에 대해서 알아보자!
지난 포스팅 : https://dong-woo.tistory.com/83
[Spring] 스프링 MVC 구조 이해하기
이전 포스팅에서는 프론트 컨트롤러 패턴을 도입한 MVC 프레임워크를 버전 1부터 5까지, 점진적으로 발전시키며 개발해봤다. 결과적으로 V5에서는 어댑터 패턴을 도입해, 보다 유연한 MVC 프레임
dong-woo.tistory.com
- 로깅
- 운영 시스템에서는 System.out.println()과 같은 시스템 콘솔을 사용해서 필요한 정보를 출력하지 않는다.
- 별도의 로깅 라이브러리를 사용해 로그를 출력한다.
- 로그 라이브러리는 정말 많은데, 그것을 통합해서 인터페이스로 제공하는 것이 SLF4J 라이브러리이다.
- 구현체로는 Logback을 많이 사용한다.
@Slf4j
@RestController
public class LogTestController {
//private final Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/log-test")
public String logTest() {
String name = "Sping";
System.out.println("name = " + name);
log.trace("trace log={}", name);
log.debug("debug log={}", name);
log.info("info log={}", name);
log.warn("warn log={}", name);
log.error("error log={}", name);
return "ok";
}
}
- 주석처리한 부분처럼 로거를 선언해서 사용해도 되고, 롬복 라이브러리의 @Slf4j 애노테이션을 등록하고 사용해도 된다.
- 결과를 보면, 원하는 로그 레벨을 직접 지정해 기록할 수 있다.
- 로그 레벨의 순서는 trace -> debug -> info -> warn -> error 순서이다.
- 즉 trace 수준으로 설정해놓으면 모든 로그를 다 출력하고, info 수준으로 설정해놓으면 info부터 error까지 출력한다.
- 기본 설정값은 info 이다.
- 이와 같이 출력 수준을 직접 지정할 수 있다는 점에서 sout과 차이가 크다. sout은 로그 레벨과 상관없이 항상 출력된다.
- 보통 개인 프로젝트는 trace로 설정해도 상관 없다.
- 실무 개발 서버에서는 보통 debug 수준으로 설정한다.
- 실제 운영 서버에서는 info 수준으로 설정한다.
- @RestController, @Controller 차이
@Controller
- @Controller의 목적은 "뷰" 혹은 "모델뷰"를 반환하는 것이다.
- 그래서 컨트롤러 호출 이후에 뷰를 찾고 렌더링 하는 과정이 일어난다.
- 보통 반환 타입을 ModelAndView나 String으로 사용했는데, 이는 모두 viewPath를 반환해주기 위함이다.
- 따라서 서버사이드 템플릿 엔진과 함께 동적인 Html을 생성한 뒤 반환하는 과정이 생긴다.
- 만약 반환타입이 void면 어떨까?
- 이런경우는 보통 @ResponseBody 애노테이션을 함께 사용해 뷰를 반환하지 않는다.
- @ResponseBody란 직접 메시지 바디로 응답을 보내겠다는 의미이다.
@RestController
- @RestController의 목적은 뷰를 반환하는 것이 아니라, 응답 메세지 바디에 직접 데이터를 반환하는 것이다.
- 따라서 서버사이드 템플릿 엔진과 함께 사용되지 않는다. (== 뷰를 반환하지 않는다.)
차이점
- 둘의 용도가 다른다는 것은 알겠다.
- 근데 @Controller에 @ResponseBody 애노테이션을 함께 사용하면 @RestController와 다른게 뭘까?
- 없다! 같은 의미로 사용된다.
- 다만 @Controller를 사용하면, 내부의 여러 메소드 중 뷰 반환과 메세지 바디 직접 반환을 동시에 사용할 수 있다.
- 요청 매핑 (@RequestMapping, @GetMapping ..)
기본, HTTP 메소드 축약
@Slf4j
@RestController
public class MappingController {
@RequestMapping("/hello-basic")
public String helloBasic() {
log.info("helloBasic");
return "OK";
}
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("helloBasic");
return "OK";
}
@GetMapping("/mapping-get-v2")
public String mappingGetV2() {
log.info("helloBasic");
return "OK";
}
}
- 위와 같이 @RequestMapping 애노테이션을 사용해 해당 URL 호출이 오면 메소드가 실행되도록 매핑한다.
- 하지만 첫 번째의 경우 모든 HTTP 메소드에 대해서 요청을 허락할 것이다.
- 원하는 결과는 이렇지 않을 것이다! 애노테이션의 추가적인 인자로 HTTP 메소드를 두 번째처럼 지정해줄 수 있다.
- 그것조차 귀찮으면 세 번째 메소드처럼 HTTP 메소드 + Mapping을 붙인 애노테이션을 사용하면 된다!
- GET, POST, PATCH, DELETE 등 모든 메소드 처리가 다 된다.
PathVariable 사용
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "Ok";
}
// 이 방식 많이 사용한다!
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
log.info("mappingPath userId = {}, orderId = {}", userId, orderId);
return "OKOK";
}
- 최근 HTTP API는 위와 같이 리소스 경로에 식별자를 넣는 스타일을 선호한다.
- PathVariable, 즉 URL의 부분을 변수로 받을 수 있다.
- @PathVariable의 이름과 파라미터 이름이 같다면 애노테이션을 생략할 수 있다.
특정 조건 추가 (매핑 안되면 오류반환)
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "Good!";
}
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
log.info("mappingHeader");
return "OKOKOK";
}
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "Ok";
}
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
- 요청 매핑에 특정한 조건을 줄 수 있다.
1. 파라미터로 넘어온 값이 일치하는지 확인할 수 있다.
2. HTTP 헤더에 지장한 키-값이 있는지 확인할 수 있다.
3. HTTP 요청 메시지의 컨텐트 타입, 즉 미디어 타입을 지정할 수 있다. 일치하지 않으면 415 상태코드(Unsuppoerted Media Type)을 반환한다. --> consumes 사용
4. HTTP 요청 메시지의 Accept 헤더를 기반으로 일치하는지 확인한다. 맞지 않으면 406 상태코드 (Not Acceptable)을 반환한다.
- HTTP 요청 처리
기본, 헤더 조회
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String headers(HttpServletResponse 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("host = {}", host);
log.info("myCookie = {}", cookie);
return "OKOK";
}
}
- 헤더 정보의 특정한 요소들을 파라미터로 받아와서 데이터를 사용할 수 있다.
요청 파라미터
@Slf4j
@Controller
public class RequestParamCotroller {
@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
String name = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
log.info("username = {}, age = {}", name, age);
response.getWriter().write("OKOK");
}
@ResponseBody
@RequestMapping("request-param-v2")
public String requestParamV2(@RequestParam("username") String username,
@RequestParam("age") int age) {
log.info("username = {}, age = {}", username, age);
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) { // String, int, Integer 등의 단순타입이면 @RequestParam도 생략 가능)
// 하지만 애노테이션마저 없으면 조금 과하다는 생각도 든다.
log.info("username = {}, age = {}", username, age);
return "OK";
}
@ResponseBody
@RequestMapping("request-param-required")
public String requestParamV5(@RequestParam(required = true) String username, // 기본값이 true
@RequestParam(required = false) Integer age) { // int -> Integer 변환해야 null 가능
log.info("username = {}, age = {}", username, age);
return "OK";
}
@ResponseBody
@RequestMapping("request-param-map")
public String requestParamMap(@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";
}
}
요청 파라미터를 처리하는 다양한 방식을 살펴보자
- 가장 기본적인 방식으로 RequestServlet을 직접 받아와서 getParameter를 사용하는 방식이다.
- 서블릿을 굳이 사용해야될까? 스프링 MVC 가 제공하는 @RequestParam 애노테이션을 활용하면 바로 가져올 수 있다.
- 인자로 받아와서 사용할 변수명을 맞추면 가져올 파라미터의 키 값을 생략할 수 있다.
- String, int, Integer 등의 단순타입이라면 @RequestParam도 생략 가능하다.
- 요청 파라미터로 필수적으로 받아와야할 정보를 명시할 수 있다.
- 데이터를 따로 따로 받아오지 않고 Map 형식으로 받아올 수 있다.
- 실제 개발 환경에서는 파라미터로 넘어온 값들을 객체 단위로 만들어서 사용하게 될텐데, 기존에는 객체를 생성하고 파라미터로 받아온 인자를 setter를 통해 객체에 저장하고하는 과정을 거칠 것이다.
하지만 스프링은 이 과정을 완전히 자동화해주는 @ModelAttribute 기능을 제공한다.
getter, setter 메소드를 가지는 변수를 프로퍼티라고 하는데, @ModelAttribute를 사용하면 자동으로 요청 파라미터에 맞는 프로퍼티가 있는지 찾고, 해당 파라미터의 setter를 호출해서 값을 입력한다. --> 바인딩 - @ModelAttribute는 생략 가능하다.
중요! 스프링은 파라미터에서 애노테이션 생략시 다음과 같은 규칙을 적용한다.
- String, int, Integer 같은 단순 타입이 적혀있다면, @RequestParam이 생략 되어 있다고 처리
- 나머지 (Argument resolver로 지정해준 타입 외)는 @ModelAttribute가 생략 되어 있다고 처리
Argument resolver로 지정해준 타입은, 인자로 사용하도록 미리 예약되어 있는 (HttpServletRequest request) 같은 애들이다.
메시지 바디 직접 조회
@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<>("OKsdasdas");
}
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {
log.info("messagebody = {}", messageBody);
return "OK";
}
}
먼저 메시지 바디에 단순 텍스트가 담겨서 요청이 들어오는 상황을 살펴보자.
- 기본적인 방법이다. 서블릿 request 객체를 받아오고 InputStream, StreamUtils를 사용해 바이트 코드를 String 타입으로 저장한다.
- 서블릿으로 받아오고 InputStream, StreamUtils 를 사용해 문자열로 변환하는 과정이 귀찮다! 스프링은 인자로 InputStream을 받을 수 있다. request객체에서 InputStream을 사용하는 단계 하나를 줄일 수 있다.
- InputStream 자체를 인자로 받아 한결 편해졌지만, InputStream을 쓰는 것 자체도 귀찮다. 스프링은 아주 편리한 HttpEntity라는 기능을 제공한다. HTTP 메세지 바디를 읽어서 원하는 문자형이나 객체로 변환까지 다 해서 전달해준다! 이는 응답 시에도 동일하게 사용할 수 있다. HttpEntity나 이를 상속받은 ResponseHttpEntity를 반환하게 되면, @Controller를 사용하는 상황에도 뷰를 거치지 않고 바로 메시지 바디로 응답을 찍어서 보낼 수 있다.
- 최종본이다. 가장 많이 사용하는 방식이고, @RequestBody 애노테이션을 사용하면 요청 메시지 바디만 바로 원하는 타입으로 변환되어 받아올 수 있다. 하지만 메시지 바디 정보만 받아오기에, 혹시 헤더 정보가 필요하다면 HttpEntity를 사용하면 된다.
@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("OKOK!!");
}
@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 good!";
}
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) throws IOException {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "OK good!";
}
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV3(HttpEntity<HelloData> httpEntity) throws IOException {
HelloData data = httpEntity.getBody();
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "OK good!";
}
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData helloData) throws IOException {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return helloData;
}
}
다음은 메시지 바디에 JSON 데이터가 담겨 요청으로 넘어오는 상황이다. 이전 문자 데이터랑 동일한 흐름이다!
- 서블릿 객체, InputStream, StreamUtils를 사용해 문자타입으로 받는다. 이후 Json <-> 객체 간 변환이 가능하게 해주는 ObjectMapper를 사용해 Json데이터 내부의 파라미터들을 객체에 저장한다.
- 서블릿 쓰기 귀찮으니까 @RequestBody 애노테이션을 통해 String 데이터를 받아오고, 곧바로 ObjectMapper를 사용해 데이터를 처리한다.
- 하지만 결국 변환할 객체 타입이 내가 만든 객체타입이라면, @RequestBody로 받아올 때부터 굳이 String으로 받아오지 않고 해당 객체 타입으로 받아올 수 있다.
- 물론 앞서 배운 것처럼 HttpEntity를 사용해도 된다.
- @RequestBody를 통해 객체타입으로 얻어온 메시지 바디를 그대로 응답으로 반환시키는 모습이다.
정말 중요한 점은 @RequestBody는 생략 불가하다는 점이다!! 생략해버리면 @RequestParam이나 @ModelAttribute로 인식해버린다.
- HTTP 응답 처리
정적 리소스
- 정적 리소스는 말 그대로 변함이 없는 정적 리소스이기 때문에 파일 디렉토리상의 위치를 웹 브라우저에 입력해도 HTML 파일이 나오게 된다.
- 기본적으로 resources/static 경로에 파일을 저장한다.
- 웹브라우저에서 경로에 파일.html을 넣어서 실행하면 된다.
뷰 템플릿
- 뷰 템플릿을 거쳐 HTML이 생성되고, 뷰가 응답을 만들어서 전달한다.
- 일반적으로는 동적인 HTMl을 생성하는 용도로 사용하지만, 뷰 템플릿으로 만들 수 있는 것이라면 뭐든 가능하다.
- 경로는 resources/templates 안에 저장한다. 컨트롤러와 뷰 템플릿을 한번 만들어보자!
@Controller
public class ResponseViewController {
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1() {
ModelAndView mav = new ModelAndView("/response/hello")
.addObject("data", "hello!!");
return mav;
}
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model) {
model.addAttribute("data", "hihihi!!");
return "/response/hello";
}
@RequestMapping("/response/hello")
public void responseViewV3(Model model) {
model.addAttribute("data", "hihihi!!");
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="${data}">empty</p>
</body>
</html>
- 기본적인 ModelAndView를 반환하는 스타일이다. ModelAndView 객체를 만들어 뷰 템플릿 경로를 설정해주고, 원하는 데이터를 모델에 담아 반환한다.
- 반환타입을 String으로 설정하고 파라미터로 Model을 받아와 데이터를 추가하고, 뷰 템플릿 경로를 반환하는 스타일이다.
- 아주 특수한 경우로, 반환 타입을 void로 설정하고 @RequestMapping 경로를 뷰 템플릿 경로로 설정하면 자동으로 @RequestMapping의 URL 경로가 뷰 경로로 설정된다.
메시지 바디에 직접 응답
참고로 위의 정적 리소스나 뷰 템플릿을 활용해도 HTTP 응답 메시지 바디 안에 HTML 데이터 자제가 담겨서 전달된다!
메시지 바디를 거치지 않고 HTML을 반환한다 --> X
이번에 알아볼 것은 메시지 바디에 HTML이 아닌 JSON이나 String같은 형식으로 데이터를 실어 보내는 상황을 알아보자.
@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)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
- 서블릿 response 객체를 이용해 문자열을 반환하는 방식이다.
- 이전에 알아봤던 HttpEntity를 상속받은 ResponseEntity를 사용해 <String> 타입으로 반환하는 모습이다. 언급했듯이 HttpEntity를 사용하면 뷰를 거치지 않고 메시지 바디로 바로 응답하게 된다.
- @ReponseBody 애노테이션을 같이 활용해 뷰를 거치지 않고 메시지 바디로 곧장 응답하는 모습이다.
- ResponseEntity를 사용하지만 타입을 객체로 지정했다. 참고로 HttpEntity를 상속받은 RequestEntity나 ResponseEntity를 사용하면 HTTP 상태코드까지 지정해 반환할 수 있다. --> JSON으로 반환된다.
- @ResponseBody를 사용해 직접적으로 객체를 메시지바디로 반환시켜주는 모습이다. 이때 HTTP 상태코드를 같이 지정해 반환해주기 위해서 @ResponseStatus 애노테이션을 같이 활용했다. --> JSON으로 반환된다.
이제 궁금증이 무수히 생긴다!
나는 그저 객체를 반환했을 뿐인데, 어떻게 자동으로 JSON 형식으로 반환이 됐을까?
이 모든 중간과정을 HTTP 메시지 컨버터라는 녀석이 해준다.
- HTTP 메시지 컨버터
HTTP 메시지 컨버터란?
@ResponseBody를 사용하면 메시지 바디에 내용을 직접 반환한다.
이 때는 뷰 리졸버가 동작하지 않는데, 대신에 HTTP 메시지 컨버터가 동작한다!
기본 문자처리는 StringHttpMessageConverter, 기본 객체처리는 MappingJackson2HttpMessageConverter가 동작한다. 추가적으로 byte 처리 등 여러 메시지 컨버터가 기본으로 등록되어 있다.
스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용한다.
- HTTP 요청 : @RequestBody 사용, HttpEntity(RequestEntity) 사용
- HTTP 응답 : @ResponesBody 사용, HttpEntity(ResponseEntity) 사용
컨버터가 동작할 때는 클래스 타입과 미디어 타입에 대한 요구사항이 맞아야 해당 컨버터가 선택된다.
- ByteArrayHttpMessageConverter
클래스 타입 : byte[], 미디어타입 : * / * - StringHttpMessageConverter
클래스 타입 : String, 미디어타입 : * / * - MappingJackson2HttpMessageConverter
클래스 타입 : 객체 혹은 HashMap, 미디어타입 : application/json 관련
예시를 하나 들어보자. 요청으로 들어온 메시지 바디의 Content-type이 application/json이다.
근데 매핑되는 클래스 타입이 String이면 JSON 데이터가 문자열로 들어온다.
똑같이 요청으로 들어온 메시지 바디의 Content-type이 application/json이다.
근데 매핑되는 클래스 타입이 객체라면 해당 JSON 데이터는 객체 타입으로 변환되어 들어온다.
하지만 요청으로 들어온 메시지 바디의 타입이 텍스트인데, 클래스 타입이 객체 타입이라면?
어느 곳에도 적용될 수 없다!
우선순위는 String이 Json보다 높다.
따라서 String으로 처리할 수 있으면 먼저 처리한다.
이후 요청도 JSON, 클래스 타입이 객체 혹은 HashMap인 경우가 JsonMessageConverter로 선택된다.
이 과정은 응답으로 내볼 때도 마찬가지이다!
클래스가 반환하고자하는 타입과, Http 요청의 Accept 미디어타입이 맞아야 사용할 수 있는 것이다.
RequestMappingHandlerAdapter 구조
그렇다면 HTTP 메시지 컨버터는 스프링 MVC 어디쯤에서 사용될까? 기존의 아키텍처에는 HTTP 메시지 컨버터가 보이지 않는다.
모든 비밀은 애노테이션 기반의 컨트롤러, 즉 @RequstMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter에 있다!
ArgumentResolver & ReturnValueHandler
포스팅 초반에 언급되었던 ArgumentResolver가 등장했다.
애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다. 서블릿, Model, @RequestParam, @RequestBody등 이렇게 많은 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.
애노테이션 기반의 컨트롤러를 처리하는 RequestMappingHandlerAdapter는 바로 이 ArgumentResolver를 호출해서 컨트롤러가 필요로 하는 파라미터의 값을 가져온다. 이후 컨트롤러를 호출하면서 값을 넘겨주는 것이다.
반대로 컨트롤러에서 데이터를 반환하는 경우를 생각해보면, ModelAndView, @ResponseBody, String, HttpEntity등 여러가지 타입으로 반환해도 처리가 가능했다. 이는 ReturnValueHandler 덕분에 가능했던 것이다!
그렇다면 HTTP 메시지 컨버터는 어디에?
HTTP 메시지 컨버터를 사용하는 상황은 두 가지가 있었다.
- @RequestBody, HttpEntity를 사용해 원하는 타입으로 요청 메시지 바디를 받아올 때
- @ResponseBody, HttpEntity를 사용해 컨트롤러가 반환할 때
요청과 응답시 원하는 데이터 타입으로 받을 수 있었던 이유는 모두 이 HTTP 메시지 컨버터 덕분이었다.
- 정리
- 다양한 기능을 익혔다. 너무 많은 내용이니 복습하는 시간을 가지자.
- 다음 포스팅은 실제 이 기능들을 적용한 웹 애플리케이션을 만들어 볼 것이다!
출처 : 인프런, 김영한의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술
'Spring' 카테고리의 다른 글
[Spring] 타임리프(Thymeleaf)와 스프링의 통합 (0) | 2023.07.19 |
---|---|
[Spring] 스프링 MVC로 웹 페이지 만들기 (0) | 2023.07.15 |
[Spring] 스프링 MVC 구조 이해하기 (1) | 2023.07.11 |
[Spring] 프론트 컨트롤러를 도입한 MVC 프레임워크 만들기 (1) | 2023.07.09 |
[Spring] 서블릿, JSP, MVC 패턴으로 회원 관리 웹 애플리케이션 만들기 (0) | 2023.07.06 |