Spring

[Spring] 스프링 MVC 기본 기능 - HTTP 요청, 응답 처리하기

이덩우 2023. 7. 14. 01:57

지난 포스팅에서는 스프링 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";
    }
}

요청 파라미터를 처리하는 다양한 방식을 살펴보자

  1. 가장 기본적인 방식으로 RequestServlet을 직접 받아와서 getParameter를 사용하는 방식이다.
  2. 서블릿을 굳이 사용해야될까? 스프링 MVC 가 제공하는 @RequestParam 애노테이션을 활용하면 바로 가져올 수 있다.
  3. 인자로 받아와서 사용할 변수명을 맞추면 가져올 파라미터의 키 값을 생략할 수 있다.
  4.  String, int, Integer 등의 단순타입이라면 @RequestParam도 생략 가능하다.
  5. 요청 파라미터로 필수적으로 받아와야할 정보를 명시할 수 있다. 
  6. 데이터를 따로 따로 받아오지 않고 Map 형식으로 받아올 수 있다.
  7. 실제 개발 환경에서는 파라미터로 넘어온 값들을 객체 단위로 만들어서 사용하게 될텐데, 기존에는 객체를 생성하고 파라미터로 받아온 인자를 setter를 통해 객체에 저장하고하는 과정을 거칠 것이다.
    하지만 스프링은 이 과정을 완전히 자동화해주는 @ModelAttribute 기능을 제공한다.
    getter, setter  메소드를 가지는 변수를 프로퍼티라고 하는데, @ModelAttribute를 사용하면 자동으로 요청 파라미터에 맞는 프로퍼티가 있는지 찾고, 해당 파라미터의 setter를 호출해서 값을 입력한다. --> 바인딩
  8. @ModelAttribute는 생략 가능하다.

중요! 스프링은 파라미터에서 애노테이션 생략시 다음과 같은 규칙을 적용한다.

  1. String, int, Integer 같은 단순 타입이 적혀있다면, @RequestParam이 생략 되어 있다고 처리
  2. 나머지 (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";
    }
}

먼저 메시지 바디에 단순 텍스트가 담겨서 요청이 들어오는 상황을 살펴보자.

  1. 기본적인 방법이다. 서블릿 request 객체를 받아오고 InputStream, StreamUtils를 사용해 바이트 코드를 String 타입으로 저장한다. 
  2. 서블릿으로 받아오고 InputStream, StreamUtils 를 사용해 문자열로 변환하는 과정이 귀찮다! 스프링은 인자로 InputStream을 받을 수 있다. request객체에서 InputStream을 사용하는 단계 하나를 줄일 수 있다.
  3. InputStream 자체를 인자로 받아 한결 편해졌지만, InputStream을 쓰는 것 자체도 귀찮다. 스프링은 아주 편리한 HttpEntity라는 기능을 제공한다. HTTP 메세지 바디를 읽어서 원하는 문자형이나 객체로 변환까지 다 해서 전달해준다! 이는 응답 시에도 동일하게 사용할 수 있다. HttpEntity나 이를 상속받은 ResponseHttpEntity를 반환하게 되면, @Controller를 사용하는 상황에도 뷰를 거치지 않고 바로 메시지 바디로 응답을 찍어서 보낼 수 있다.
  4. 최종본이다. 가장 많이 사용하는 방식이고, @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 데이터가 담겨 요청으로 넘어오는 상황이다. 이전 문자 데이터랑 동일한 흐름이다!

  1. 서블릿 객체, InputStream, StreamUtils를 사용해 문자타입으로 받는다. 이후 Json <-> 객체 간 변환이 가능하게 해주는 ObjectMapper를 사용해 Json데이터 내부의 파라미터들을 객체에 저장한다.
  2. 서블릿 쓰기 귀찮으니까 @RequestBody 애노테이션을 통해 String 데이터를 받아오고, 곧바로 ObjectMapper를 사용해 데이터를 처리한다.
  3. 하지만 결국 변환할 객체 타입이 내가 만든 객체타입이라면, @RequestBody로 받아올 때부터 굳이 String으로 받아오지 않고 해당 객체 타입으로 받아올 수 있다.
  4. 물론 앞서 배운 것처럼 HttpEntity를 사용해도 된다.
  5. @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>
  1. 기본적인 ModelAndView를 반환하는 스타일이다. ModelAndView 객체를 만들어 뷰 템플릿 경로를 설정해주고, 원하는 데이터를 모델에 담아 반환한다.
  2. 반환타입을 String으로 설정하고 파라미터로 Model을 받아와 데이터를 추가하고, 뷰 템플릿 경로를 반환하는 스타일이다.
  3. 아주 특수한 경우로, 반환 타입을 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;
    }
}
  1. 서블릿 response 객체를 이용해 문자열을 반환하는 방식이다.
  2. 이전에 알아봤던 HttpEntity를 상속받은 ResponseEntity를 사용해 <String> 타입으로 반환하는 모습이다. 언급했듯이 HttpEntity를 사용하면 뷰를 거치지 않고 메시지 바디로 바로 응답하게 된다.
  3. @ReponseBody 애노테이션을 같이 활용해 뷰를 거치지 않고 메시지 바디로 곧장 응답하는 모습이다.
  4.  ResponseEntity를 사용하지만 타입을 객체로 지정했다. 참고로 HttpEntity를 상속받은 RequestEntity나 ResponseEntity를 사용하면 HTTP 상태코드까지 지정해 반환할 수 있다. --> JSON으로 반환된다.
  5. @ResponseBody를 사용해 직접적으로 객체를 메시지바디로 반환시켜주는 모습이다. 이때 HTTP 상태코드를 같이 지정해 반환해주기 위해서 @ResponseStatus 애노테이션을 같이 활용했다. --> JSON으로 반환된다.

이제 궁금증이 무수히 생긴다!

나는 그저 객체를 반환했을 뿐인데, 어떻게 자동으로 JSON 형식으로 반환이 됐을까?

이 모든 중간과정을 HTTP 메시지 컨버터라는 녀석이 해준다.

 


 

- HTTP 메시지 컨버터

 

 HTTP 메시지 컨버터란?

@ResponseBody 동작원리

@ResponseBody를 사용하면 메시지 바디에 내용을 직접 반환한다.

이 때는 뷰 리졸버가 동작하지 않는데, 대신에 HTTP 메시지 컨버터가 동작한다!

기본 문자처리는 StringHttpMessageConverter, 기본 객체처리는 MappingJackson2HttpMessageConverter가 동작한다. 추가적으로 byte 처리 등 여러 메시지 컨버터가 기본으로 등록되어 있다.

 

스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용한다.

  • HTTP 요청 : @RequestBody 사용, HttpEntity(RequestEntity) 사용
  • HTTP 응답 : @ResponesBody 사용, HttpEntity(ResponseEntity) 사용

컨버터가 동작할 때는 클래스 타입과 미디어 타입에 대한 요구사항이 맞아야 해당 컨버터가 선택된다.

  1. ByteArrayHttpMessageConverter 
    클래스 타입 : byte[], 미디어타입 : * / *
  2. StringHttpMessageConverter
    클래스 타입 : String, 미디어타입 : * / *
  3. 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 메시지 컨버터가 보이지 않는다.

Spring MVC 구조

모든 비밀은 애노테이션 기반의 컨트롤러, 즉 @RequstMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter에 있다!

 

ArgumentResolver & ReturnValueHandler

동작 방식

포스팅 초반에 언급되었던 ArgumentResolver가 등장했다.

애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다. 서블릿, Model, @RequestParam, @RequestBody등 이렇게 많은 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.

 

애노테이션 기반의 컨트롤러를 처리하는 RequestMappingHandlerAdapter는 바로 이 ArgumentResolver를 호출해서 컨트롤러가 필요로 하는 파라미터의 값을 가져온다. 이후 컨트롤러를 호출하면서 값을 넘겨주는 것이다.

 

반대로 컨트롤러에서 데이터를 반환하는 경우를 생각해보면, ModelAndView, @ResponseBody, String, HttpEntity등 여러가지 타입으로 반환해도 처리가 가능했다. 이는 ReturnValueHandler 덕분에 가능했던 것이다!

 

그렇다면 HTTP 메시지 컨버터는 어디에?

HTTP 메시지 컨버터의 위치

 

HTTP 메시지 컨버터를 사용하는 상황은 두 가지가 있었다.

  1. @RequestBody, HttpEntity를 사용해 원하는 타입으로 요청 메시지 바디를 받아올 때
  2. @ResponseBody, HttpEntity를 사용해 컨트롤러가 반환할 때

요청과 응답시 원하는 데이터 타입으로 받을 수 있었던 이유는 모두 이 HTTP 메시지 컨버터 덕분이었다.

- 정리

  • 다양한 기능을 익혔다. 너무 많은 내용이니 복습하는 시간을 가지자.
  • 다음 포스팅은 실제 이 기능들을 적용한 웹 애플리케이션을 만들어 볼 것이다!

 

 

 

 

출처 : 인프런, 김영한의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술