프로그래밍 공방

[Spring] 6-2. 스프링 MVC - 기본 기능 본문

개발/스프링

[Spring] 6-2. 스프링 MVC - 기본 기능

hyosupsong 2021. 11. 2. 01:51

참고
* Jar을 사용하면 항상 내장 서버(톰캣등)을 사용하고, webapp 경로도 사용하지 않는다(내장 서버 사용에 최적화 되어 있는 기능) War를 사용하면 내장 서버도 사용 가능하지만, 주로 외부 서버에 배포하는 목적으로 사용
* Jar 사용하면 /resources/static 위치에 index.html 파일을 두면 Welcome 페이지로 처리해준다
  (스프링 부트가 지원하는 정적 컨텐츠 위치)

@RequestMapping

@RestController
@RequestMapping("/test")
public class MappingCcontroller {

    // method 설정을 통해 특정 HTTP 메서드 요청만 허용하도록 할 수 있다
    // @RequestMapping("/hello", method = RequestMethod.GET)
    @RequestMapping("/hello")
    public String helloTest() {
        return "ok";
    }
}

- RequestMapping은 해당 url 호출이 오면 메서드가 실행되도록 매핑하며 class에도 @RequestMapping이 선언되어 있는 경우 중첩된 url로 매핑된다
  ex) 위의 예시 같은 경우 /test/hello
- 대부분의 속성을 배열[] 로 제공하므로 다중 설정이 가능하다( {"/hello-test", "/hello-test2"} )
- 따로 method 를 설정하지 않은 경우 HTTP 메서드를 모두 허용한다(GET, HEAD, POST, PUT, PATCH, DELETE)

HTTP 메서드 매핑 축약

특정한 HTTP 메서드만 허용해야 하는 경우 HTTP 메서드를 축약한 애노테이션을 사용하는 것이 더 직관적이다
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- @PatchMapping

 실제 구현은 아래와 같이 되어 있다

@org.springframework.web.bind.annotation.RequestMapping(method = {org.springframework.web.bind.annotation.RequestMethod.GET})
public @interface GetMapping { /* ... */ }

@PathVariable

@GetMapping("/users/{userId}")
public String pathTest(@PathVariable("userId") String data) {
    return "ok";
}

// @PathVariable의 이름과 파라미터 이름이 같으면 생략할 수 있다
@GetMapping("/users/{userId}")
public String pathTest(@PathVariable String userId) {
    return "ok";
}

// 아래와 같이 다중으로 사용할 수도 있다
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String pathTest(
    @PathVariable String userId,
    @PathVariable Long orderId
) {
    return "ok";
}

- @RequestMapping은 URL 경로를 템플릿화 할 수 있는데, @PathVariable을 사용하면 매칭되는 부분을 편하게 조회할 수 있다
- @PathVariable의 이름과 파라미터 이름이 같으면 생략할 수 있다

특정 파라미터 조건 매핑

/**
 * 파라미터로 추가 매핑
 * params="mode",
 * params="!mode"
 * params="mode=debug"
 * params="mode!=debug" (! = )
 * params = {"mode=debug","data=good"}
 */
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
    return "ok";
}

- 특정 파라미터가 있거나 없는 조건을 추가할 수 있다

특정 헤더 조건 매핑

/**
 * 특정 헤더로 추가 매핑
 * headers="mode",
 * headers="!mode"
 * headers="mode=debug"
 * headers="mode!=debug" (! = )
 */
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
    return "ok";
}

- 파라미터 매핑과 비슷하지만, HTTP 헤더를 사용한다

미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume

/**
 * Content-Type 헤더 기반 추가 매핑 Media Type
 * consumes="application/json"
 * consumes="!application/json"
 * consumes="application/*"
 * consumes="*\/*"
 * MediaType.APPLICATION_JSON_VALUE
 */
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
    return "ok";
}

- HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑한다
  (맞지 않으면 HTTP 415 상태 코드(Unsupported Media Type)을 반환)

미디어 타입 조건 매핑 - HTTP 요청 Accept, produce

/**
 * Accept 헤더 기반 Media Type
 * produces = "text/html"
 * produces = "!text/html"
 * produces = "text/*"
 * produces = "*\/*"
 */
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
    return "ok";
}

- HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑한다
  (맞지 않으면 HTTP 406 상태 코드(Not Acceptable)을 반환)

HTTP 요청 - 기본, 헤더 조회

@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
 ) {
        return "ok";
    }
}

- HttpServletRequest
- HttpServletResponse
- HttpMethod : HTTP 메서드를 조회
- Locale : Locale 정보를 조회
- @RequestHeader MultiValueMap : 모든 헤더를 MultiValueMap 형식으로 조회
- @RequestHeader("host") : 특정 HTTP 헤더를 조회
   - 필수 값 여부 : required / 기본 값 속성 : defaultValue
- @CookieValue(value = "myCookie", required = false) : 특정 쿠키를 조회
   - 필수 값 여부 : required / 기본 값 : defaultValue
* 참고 : MultiValueMap : Map과 유사하지만 하나의 키에 여러 값을 받을 수 있다

HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form

클라이언트에서 서버로 요청 데이터를 전달하는 3가지 방법

1. GET - 쿼리 파라미터
- /url?name=kim&age31 형태로 전달
- 메시지 바디 없이 URL 쿼리 파라미터에 데이터를 포함해서 전달
- 검색, 필터, 페이징 등에서 많이 사용하는 방식

2. POST - HTML Form
- content-type : application/x-www-form-urlencoded
- 메시지 바디에 쿼리 파라미터 형식으로 전달 (name=kim&age31)
- 회원 가입, 상품 주문, HTML Form 사용

3. HTTP message body에 데이터를 직접 담아서 요청
- HTTP API 에서 주로 사용, JSON, XML, TEXT
- 데이터 형식은 주로 JSON 사용
- POST, PUT, PATCH

요청 파라미터 조회 - 쿼리 파라미터, HTML Form

@Controller
public class RequestParamController {
    // 반환 타입이 없으면서 이렇게 응답에 값을 직접 집어넣으면, view 조회X
    @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-option")
    public String requestParamOption(
        @RequestParam(required = true, defaultValue = "guest") String username,
        @RequestParam(required = false, defaultValue = "-1") Integer age
    ) {
        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";
    }
}

requestParamV1
- request.getParameter() : HttpServletRequest가 제공하는 방식으로 요청 파라미터를 조회

requestParamV2
- @RequestParam 사용
- @RequestParam의 name(value) 속성이 파라미터 이름으로 사용
   - @RequestParam("username") -> request.getParameter("username")

requestParamV3
- @RequestParam String username
- HTTP 파라미터 이름이 변수 이름과 같으면 name 생략 가능

requestParamV4
- String, int, Integer 등의 단순 타입이면 @RequestParam 도 생략 가능
* 주의 : @RequestParam을 생략하면 required=false 적용
* 참고 : 애노테이션의 생략은 코드는 깔끔해지지만 명확성은 줄어든다

requestParamOption
- @RequestParam(required = true)
   - 파라미터 필수 여부 / 기본 값이 true 이다
- true인 경우 요청 파라미터에 없으면 400 예외가 발생
- false인 경우 요청 파라미터에 없으면 null값 입력
  (따라서 null값을 받을 수 없는 int는 사용 불가능(500 예외 발생), Integer 사용하거나 defaultValue 사용)
* 주의 : /url?username= 와 같이 파라미터 이름만 있고 값이 없는 경우 빈 문자로 통과하게 된다
- defaultValue : 파라미터에 값이 없는 경우 기본 값을 적용 가능
- defaultValue를 사용하면 기본 값이 있기 때문에 required는 의미가 없다
- defaultValue는 빈 문자의 경우에도 설정한 기본 값이 적용된다

requestParamMap
- 파라미터를 Map, MultiValueMap으로 조회할 수 있다

HTTP 요청 파라미터 - @ModelAttribute

실제 개발을 하게 되면 요청 파라미터를 받아서 필요한 객체를 만드는 경우가 많다
그때 @ModelAttribute 애노테이션을 사용할 수 있다

@ResponseBody
@RequestMapping("/model-attribute-before")
public String modelAttributeBefore(
    @RequestParam String username,
    @RequestParam int age
) {
    User user = new User(username, age);
    return "ok";
}

@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute User user) {
    log.info("username={}, age={}", user.getUsername, user.getAge());
    return "ok";
}

@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(User user) {
    log.info("username={}, age={}", user.getUsername, user.getAge());
    return "ok";
}

modelAttributeBefore
- 기존에는 @RequestParam을 통해 값을 가져오고 객체를 생성해주었다

modelAttributeV1
- 스프링 MVC는 @ModelAttribute가 있으면 아래 과정을 수행한다
   - User 객체를 생성한다
   - 요청 파라미터의 이름으로 User 객체의 프로퍼티를 찾는다
   - 프로퍼티가 존재하면 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩)한다

modelAttributeV2
- @ModelAttribute는 생략할 수 있다
- 스프링은 @RequestParam 생략과의 혼란을 방지하기 위해 생략시 아래 규칙들을 적용한다
   - String, int, Integer 같은 단순 타입 = @RequestParam
   - 나머지 = @ModelAttribute(argument resolver 로 지정해둔 타입도 제외)

HTTP 요청 메시지 - 단순 텍스트

HTTP message body에 데이터를 직접 담아서 요청
   - HTTP API 에서 주로 사용, JSON, XML, TEXT
   - 데이터 형식은 주로 JSON 사용
   - POST, PUT, PATCH

@Controller
public class RequestBodyStringController {
    @PostMapping("/request-body-string-v1")
    public void requestBodyStringV1(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) {
        String messageBody = httpEntity.getBody();
        log.info("messageBody={}", messageBody);
        return new HttpEntity<>("ok");
    }
    
    @ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messageBody) {
        log.info("messageBody={}", messageBody);
        return "ok";
    }
}

requestBodyStringV1
- HTTP 메시지 바디의 데이터를 InputString 을 사용해서 직접 읽을 수 있다

requestBodyStringV2
스프링 MVC는 아래 파라미터를 지원한다
   - InputString(Reader) : HTTP 요청 메시지 바디의 내용을 직접 조회
   - OutputString(Writer) : HTTP 응답 메시지의 바디에 직접 결과 출력

requestBodyStringV3
- HttpEntity : HTTP header, body 정보를 편리하게 조회
   - 메시지 바디 정보를 직접 조회
   - 요청 파라미터 조회 기능(@RequestParam, @ModelAttribute)과는 전혀 관계 없다
- HttpEntity는 응답에도 사용 가능하다
   - 메시지 바디 정보 직접 반환
   - 헤더 정보 포함 가능
   - view 조회 X
* 참고 : HttpEntity를 상속받은 다음 객체들도 같은 기능을 제공한다
   - RequestEntity : HttpMethod, url 정보가 추가, 요청에서 사용
   - ResponseEntity : HTTP 상태 코드 설정 가능, 응답에서 사용
     (return new ResponseEntity<String>("Test", responseHeaders, HttpStatus.CREATED)
* 스프링 MVC 내부에서 HTTP 메시지 바디를 읽어서 문자나 객체로 변환해서 전달해주는데,
  이때 HTTP 메시지 컨버터(HttpMessageConverter)라는 기능을 사용한다

requestBodyStringV4
- @RequestBody : HTTP 메시지 바디 정보를 직접 조회할 수 있다
   - 요청 파라미터 조회 기능(@RequestParam, @ModelAttribute)과는 전혀 관계 없다
- @ResponseBody : 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있다(view는 사용하지 않는다)

HTTP 요청 메시지 - JSON

@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);
        User user = objectMapper.readValue(messageBody, User.class);
        log.info("username={}, age={}", user.getUsername(), user.getAge());
        response.getWriter().write("ok");
    }
    
    @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
        User user = objectMapper.readValue(messageBody, User.class);
        log.info("username={}, age={}", user.getUsername(), user.getAge());
        return "ok";
    }
    
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody User user) {
        log.info("username={}, age={}", user.getUsername(), user.getAge());
        return "ok";
    }
    
    @ResponseBody
    @PostMapping("/request-body-json-v4")
    public String requestBodyJsonV4(HttpEntity<User> httpEntity) {
        User user = httpEntity.getBody();
        log.info("username={}, age={}", user.getUsername(), user.getAge());
        return "ok";
    }
    
    @ResponseBody
    @PostMapping("/request-body-json-v5")
    public User requestBodyJsonV5(@RequestBody User user) {
        log.info("username={}, age={}", user.getUsername(), user.getAge());
        return user;
    }
}

requestBodyJsonV1
- HttpServletRequest 를 사용해서 직접 HTTP 메시지 바디에서 데이터를 읽어오고 Jackson 라이브러리인 objectMapper를 사용해서 자바 객체로 변환한다

requestBodyJsonV2
- HttpServletRequest를 사용하는 대신에 @RequestBody를 사용해서 HTTP 메시지에서 데이터를 꺼내서 messageBody에 저장한다
- V1과 동일하게 objectMapper를 통해서 자바 객체로 변환한다

requestBodyJsonV3
- @RequestBody 객체 파라미터
   - @RequestBody User user : @RequestBody에 직접 만든 객체를 지정할 수 있다
* 참고 : HttpEntity, @RequestBody를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다(HTTP 메시지 컨버터는 문자 뿐 아니라 JSON도 객체로 변환해주는데, V2의 작업을 대신 처리해준다)
- @RequestBody는 생략이 불가능하다(생략하면 HTTP 메시지 바디가 아니라 요청 파라미터를 처리하게 됨)
* 주의 : HTTP 요청시에 content-type이 application/json인지 꼭 확인해야 한다. 그래야 JSON을 처리할 수 있는 HTTP 메시지 컨버터가 실행된다

requestBodyJsonV4
- 앞서 말한 것처럼 HttpEntity를 사용해서 처리할 수 있다

requestBodyJsonV5
- @ResponseBody를 사용해서 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있다
  (물론 이 경우에도 HttpEntity를 사용할 수 있다)

* 참고 : 메시지 컨버터 개입 위치
           JSON 요청 -> HTTP 메시지 컨버터 -> 객체
           객체 -> HTTP 메시지 컨버터 -> JSON 요청

HTTP 응답 - 정적 리소스, 뷰 템플릿

스프링(서버)에서 응답 데이터를 만드는 방법 3가지

1. 정적 리소스
웹 브라우저에 정적인 HTML, css, js를 제공할 때는 정적 리소스를 사용한다

2. 뷰 템플릿 사용
웹 브라우저에 동적인 HTML을 제공할 때는 뷰 템플릿을 사용한다

3. HTTP 메시지 사용
HTTP API를 제공하는 경우에는 HTML이 아닌 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다

정적 리소스

스프링 부트는 Class Path의 아래 디렉토리에 있는 정적 리소스들을 제공한다
(/static, /public, /resources, /META-INF/resources)
src/main/resources는 리소스를 보관하는 곳이고, 클래스 패스의 시작 경로이다

정적 리소스 경로
/src/main/resources/static

예를 들어, 아래와 같이 파일이 들어있으면
src/main/resources/static/basic/hello-form.html
웹 브라우저에서 다음과 같이 실행하면 된다.
http://localhost:8080/basic/hello-form.html

정적 리소스는 해당 파일의 변경 없이 그대로 서비스한다

뷰 템플릿

뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달한다.
일반적으로 HTML을 동적으로 생성하는 용도로 사용하지만, 다른 것들도 가능하다.
뷰 템플릿이 만들 수 있는 것이라면 뭐든지 가능하다. 스프링 부트는 기본 뷰 템플릿 경로를 제공한다.

뷰 템플릿 경로
src/main/resources/templates

예를 들어, 아래와 같은 뷰 템플릿이 있으면
src/main/resources/templates/response/hello.html
컨트롤러에서 아래와 같이 호출할 수 있다

@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) {
        /* ... */
        return "response/hello";
    }
    
    @RequestMapping("/response/hello")
    public void responseViewV3(Model model) {
        model.addAttribute("data", "hello!!");
    }
}

- String을 반환하는 경우 - View or HTTP 메시지
@ResponseBody가 없으면 response/hello로 뷰 리졸버가 실행되어 뷰를 찾고, 렌더링 한다

- void를 반환하는 경우
@Controller를 사용하고 HttpServletResponse, OutputStream 같은 HTTP 메시지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용
* 참고로 위 방식은 명시성이 떨어져서 권장하지는 않는다

@ResponseBody, HttpEntity를 사용하면 뷰 템플릿이 아닌 HTTP 메시지 바디에 직접 응답 데이터를 출력한다

Thymeleaf

타임리프 라이브러리를 추가하면 스프링 부트가 자동으로 ThymeleafViewResolver와 필요한 스프링 빈들을 등록한다
* 아래 설정을 통해 템플릿 뷰의 prefix, suffix를 변경할 수 있다

spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

HTTP 응답 - HTTP API, 메시지 바디에 직접 입력

HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다

* 참고 : HTML이나 뷰 템플릿을 사용해도 HTTP 응답 메시지 바디에 HTML 데이터가 담겨서 전달된다
  여기서 설명하는 내용은 정적 리소스나 뷰 템플릿을 거치지 않고, 직접 HTTP 응답 메시지를 전달하는 경우를 말한다

@Controller
//@RestController
public class ResponseBodyController {
    @GetMapping("/response-body-string-v1")
    public void responseBodyV1(HttpServletResponse response) throws IOException {
        response.getWriter().write("ok");
    }
    
    /**
    * HttpEntity, ResponseEntity(Http Status 추가)
    * @return
    */
    @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;
    }
}

responseBodyV1
- HttpServletResponse 객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지를 전달한다

responseBodyV2
- ResponseEntity 객체를 통해 HTTP 메시지 바디, HTTP 응답 코드를 설정해서 전달한다

responseBodyV3
- @ResponseBody 를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력할 수 있다

responseBodyJsonV1
- ResponseEntity 를 반환한다. HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환된다

responseBodyJsonV2
- ResponseEntity 는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody 를 사용하면 이런 것을 설정하기 까다롭다
- @ResponseStatus(HttpStatus.OK) 애노테이션을 사용하면 응답 코드도 설정할 수 있다
* 물론 애노테이션이기 때문에 응답 코드를 동적으로 변경할 수는 없다
  프로그램 조건에 따라서 동적으로 변경하려면 ResponseEntity 를 사용하면 된다

@RestController

@Controller 대신에 @RestController 애노테이션을 사용하면 해당 컨트롤러에 모두 @ResponseBody가 적용되는 효과가 있다. 따라서 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력하게 된다
* 이름 그대로 REST API를 만들 때 사용하는 컨트롤러이다

* 참고 : @ResponseBody는 클래스 레벨에 선언하면 전체 메서드에 적용되는데, @RestController 애노테이션 안에 @ResponseBody가 적용되어 있다

HTTP 메시지 컨버터

뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라, HTTP API 처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다

@ResponseBody 사용 원리

@ResponseBody를 사용

- HTTP의 BODY에 문자 내용을 직접 반환
- viewResolver 대신에 HttpMessageConverter가 동작
- 기본 문자 처리 : StringHttpMessageConverter
- 기본 객체 처리 : MappingJackson2HttpMessageConverter
- byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음
* 참고 : 응답의 경우 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해서 HttpMessageConverter가 선택된다

스프링 MVC는 아래 경우에 HTTP 메시지 컨버터를 적용한다
   - HTTP 요청 : @RequestBody, HttpEntity(RequestEntity)
   - HTTP 응답 : @ResponseBody, HttpEntity(ResponseEntity)

HTTP 메시지 컨퍼터 인터페이스에서 제공하는 주요 기능은 아래와 같다

public interface HttpMessageConverter<T> {

    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
    
    List<MediaType> getSupportedMediaTypes();
    
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;

}

- canRead(), canWrite() : 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 체크
- read(), write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능

스프링 부트 기본 메시지 컨버터

0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter

* 스프링 부트는 위 컨버터 외에도 다양한 컨버터를 제공하는데, 대상 클래스 타입과 미디어 타입 둘을 체크해서 사용 여부를 결정한다

ByteArrayHttpMessageConverter
- byte[] 데이터를 처리한다
- 클래스 타입: byte[] , 미디어타입: */*
- 요청 예) @RequestBody byte[] data
- 응답 예) @ResponseBody return byte[] 쓰기 미디어타입 application/octet-stream

StringHttpMessageConverter
- String 문자로 데이터를 처리한다
- 클래스 타입: String , 미디어타입: */*
- 요청 예) @RequestBody String data
- 응답 예) @ResponseBody return "ok" 쓰기 미디어타입 text/plain

MappingJackson2HttpMessageConverter
- application/json
- 클래스 타입: 객체 또는 HashMap , 미디어타입 application/json 관련
- 요청 예) @RequestBody HelloData data
- 응답 예) @ResponseBody return helloData 쓰기 미디어타입 application/json 관련

HTTP 요청 데이터 읽기

- HTTP 요청이 오고, 컨트롤러에서 @RequestBody , HttpEntity 파라미터를 사용한다
- 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead() 를 호출한다
   - 대상 클래스 타입을 지원하는가. 예) @RequestBody 의 대상 클래스 ( byte[] , String , HelloData )
   - HTTP 요청의 Content-Type 미디어 타입을 지원하는가. 예) text/plain , application/json , */*
- canRead() 조건을 만족하면 read() 를 호출해서 객체 생성하고, 반환한다

HTTP 응답 데이터 생성

- 컨트롤러에서 @ResponseBody , HttpEntity 로 값이 반환된다
- 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 를 호출한다
   - 대상 클래스 타입을 지원하는가. 예) return의 대상 클래스 ( byte[] , String , HelloData )
   - HTTP 요청의 Accept 미디어 타입을 지원하는가.(더 정확히는 @RequestMapping 의 produces )
      예) text/plain , application/json , */* canWrite()
- 조건을 만족하면 write() 를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성한다.

요청 매핑 핸들러 어댑터 구조

HTTP 메시지 컨버터는 스프링 MVC의 핸들러를 호출하는 과정에서 사용된다

SpringMVC 구조

RequestMappingHandlerAdapter

@RequestMapping을 처리하는 애노테이션 기반의 컨트롤러를 처리하는 핸들러 어댑터

RequestMappingHandlerAdapter 동작 방식

ArgumentResolver

RequestMappingHandlerAdaptor는 ArgumentResolver를 호출해서 핸들러가 필요로 하는 다양한 파라미터의 값(객체)을 생성하고 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다
* 정확히는 HandlerMethodArgumentResolver이다

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);
    
    @Nullable
    Object resolveArgument(MethodParameter parameter,
                           @Nullable ModelAndViewContainer mavContainer,
                           NativeWebRequest webRequest, @Nullable WebDataBinderFactory
                           binderFactory) throws Exception;
}

동작 방식

1. ArgumentResolver의 supportsParameter를 호출해서 해당 파라미터를 지원하는지 체크
2. 지원하면 resolveArgument를 호출해서 실제 객체를 생성
3. 생성된 객체를 컨트롤러 호출시 전달

ReturnValueHandler

ArgumentResolver와 비슷한 역할을 하는데 응답 값을 변환하고 처리한다
* 정확히는 HandlerMethodReturnValueHandler이다

HTTP 메시지 컨버터

HTTP 메시지 컨버터의 위치

요청의 경우

@RequestBody를 처리하는 ArgumentResolver / HttpEntity를 처리하는 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성

응답의 경우

@ResponseBody, HttpEntity를 처리하는 ReturnValueHandler가 HTTP 메시지 컨버터를 호출해서 응답 결과를 생성
* @RequestBody @ResponseBody 가 있으면 RequestResponseBodyMethodProcessor (ArgumentResolver)
  HttpEntity 가 있으면 HttpEntityMethodProcessor (ArgumentResolver)를 사용
* 참고 : 스프링은 HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler, HttpMessageConverter를 인터페이스로 제공하기 때문에 언제든지 기능을 확장할 수 있다(기능 확장은 WebMvcConfigurer 를 상속 받아서 스프링 빈으로 등록하면 된다)