Programming/스프링

[김영한 스프링 MVC 1] 스프링 MVC - 기본 기능

지고르 2023. 10. 29. 16:25
로깅

 

운영 시스템 => 로깅 라이브러리 사용해 로그 출력

(SLF4J 라이브러리 => Logback, Log4J 등 라이브러리 통합해 인터페이스로 제공)

 

 

  • 로그 선언

private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class)
@Slf4j

 

 

  • LogTestController
package hello.springmvc.basic;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController //반환값으로 뷰 찾지 않고 HTTP 메시지 바디에 바로 입력
public class LogTestController {

    //private final Logger log = LoggerFactory.getLogger(getClass());

    @RequestMapping("/log-test")
    public String logTest(){
        String name = "logTest";

        //모든 정보 출력
        System.out.println("name = " + name);

        //logging level (application.properties 설정)에 따라 출력
        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);

		////사용금지
        log.trace("trace log =" + name); //자바에서 연산 실행해 CPU, 메모리 등 자원 낭비

        return "ok";
    }

}

 

 

 

  • 로그 레벨 설정
#전체 로그 레벨 설정 (기본 info)
logging.level.root=info

#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug

 

레벨 : : TRACE > DEBUG > INFO > WARN > ERROR

 

 

  • 로그 라이브러리 사용 시 장점

쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정 가능
로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절 가능
시스템 아웃 콘솔에만 출력하지 않고, 로그를 파일, 네트워크 등 별도 위치에 남길 수 있음  
=> 파일로 남길 때 일별/특정 용량별로 로그 분할 가능
일반 System.out보다 성능 좋음 (내부 버퍼링, 멀티 쓰레드 등) 

 

 

 

요청 매핑

 

  • HTTP 메소드 매핑 축약

@RequestMapping

=>

@GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping

 

 

  • MappingController
package hello.springmvc.basic.requestmapping;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

@RestController
public class MappingController {

    private Logger log = LoggerFactory.getLogger(getClass());

    @RequestMapping({"/hello-basic", "/hello-go"}) ////다중 설정 가능
    public String helloBasic(){
        log.info("helloBasic");

        return "ok";
    }

    /**
    * PathVariable
    * 변수명이 같으면 생략 가능
    * /mapping/userA
    */
    @GetMapping("/mapping/{userId}") 
    public String mappingPath(@PathVariable("userId") String data){ //@Pathvariable String userId
        log.info("mappingPath userId = {}", data);

        return "ok";
    }

    /**
     * PathVariable 다중 사용
     */
    @GetMapping("/mappping/users/{userId}/orders/{orderId}")
    public String mappingPath(@PathVariable String userId, @PathVariable Long orderId){
        log.info("mappingPath userId = {}, orderId = {}", userId, orderId);

        return "ok";
    }

    /**
     * 특정 파라미터 조건 매핑
     * params = "mode",
     * params = "!mode",
     * params = "mode=debug",
     * params = "mode!=debug",
     * params = {"mode=debug", "data=good"}
     */
    @GetMapping(value = "/mapping-param", params = "mode=debug") // localhost:8080/mapping-param?mode=debug 일 떄만 불러옴
    public String mappingParam(){
        log.info("mapping param");

        return "ok";
    }

    /**
     * 특정 헤더 조건 매핑
     * headers = "mode",
     * headers = "!mode",
     * headers = "mode=debug",
     * headers = "mode!=debug"
     */
    @GetMapping(value = "/mapping-header", headers= "mode=debug")
    public String mappingHeader(){
        log.info("mapping header");

        return "ok";
    }

    /**
     * 미디어 타입 조건 매핑 - HTTP 요청 - Content-Type
     * consumes="application/json"
     * consumes="!application/json"
     * consumes="application/*"
     * consumes="*\/*"
     * MediaType.APPLICATION_JSON_VALUE
     * 틀리면 HTTP 415 코드 반환
     */
    @PostMapping(value = "/mapping-consume", consumes = "application/json")
    public String mappingConsumes() {
        log.info("mappingConsumes");
        return "ok";
    }

    /**
     * 미디어 타입 조건 매핑 - HTTP 요청 - Accept
     * produces = "text/html"
     * produces = "!text/html"
     * produces = "text/*"
     * produces = "*\/*"
     * 틀리면 HTTP 406 코드 반환
     */
    @PostMapping(value = "/mapping-produce", produces = "text/html")
    public String mappingProduces() {
        log.info("mappingProduces");
        return "ok";
    }

}

 

 

 

요청 매핑 - API 예시

 

 

  • 회원 관리 API

회원 목록 조회: GET /users
회원 등록: POST /users
회원 조회: GET /users/{userId}
회원 수정: PATCH /users/{userId}
회원 삭제: DELETE /users/{userId}

 

 

  • MappingClassController
package hello.springmvc.basic.requestmapping;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {

    /**
     * GET /mapping/users
     */
    @GetMapping
    public String users() {
        return "get users";
    }
    
    /**
     * POST /mapping/users
     */
    @PostMapping
    public String addUser() {
        return "post user";
    }
    
    /**
     * GET /mapping/users/{userId}
     */
    @GetMapping("/{userId}")
    public String findUser(@PathVariable String userId) {
        return "get userId=" + userId;
    }
    
    /**
     * PATCH /mapping/users/{userId}
     */
    @PatchMapping("/{userId}")
    public String updateUser(@PathVariable String userId) {
        return "update userId=" + userId;
    }
    
    /**
     * DELETE /mapping/users/{userId}
     */
    @DeleteMapping("/{userId}")
    public String deleteUser(@PathVariable String userId) {
        return "delete userId=" + userId;
    }

}

 

 

 

HTTP 요청 - 기본, 헤더 조회

 

 

  • RequestHeaderController
package hello.springmvc.basic.request;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Locale;

@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";
    }

}

HttpMethod : HTTP 메서드 조회
Locale : Locale 정보를 조회한다.
@RequestHeader MultiValueMap<String, String> headerMap : 모든 HTTP 헤더를 MultiValueMap 형식으로 조회
@RequestHeader("host") String host : 특정 HTTP 헤더를 조회
@CookieValue(value = "myCookie", required = false) String cookie : 특정 쿠키를 조회 (필수 X)

 

 

  • MultiValueMap

Map과 유사

하나의 키에 여러 값 받을 수 있음

MultiValueMap<String, String> map = new LinkedMultiValueMap();
map.add("keyA", "value1");
map.add("keyA", "value2");

//[value1,value2]
List<String> values = map.get("keyA");

 

 

 

 

HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form / @RequestParam / @ModelAttribute

 

  • 클라이언트 → 서버로 요청 데이터 전달하는 방법

1. GET - 쿼리 파라미터

http://localhost:8080/url?username=hello&age=20


메시지 바디 없이, URL 쿼리 파라미터에 데이터 전달
ex) 검색, 필터, 페이징 등

=> 요청 파라미터(request parameter) 조회

 

2. POST - HTML Form

POST url
content-type: application/x-www-form-urlencoded

..

username=hello&age=20 // 메시지 바디


메시지 바디에 쿼리 파리미터 형식으로 전달
ex) 회원 가입, 상품 주문 등

=> 요청 파라미터(request parameter) 조회

 

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

 

 

 

  • RequestParamController
package hello.springmvc.basic.request;

import hello.springmvc.basic.HelloData;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.Map;
import java.util.Objects;

@Slf4j
@Controller
//@RestController // => @Responsebody + @Controller
public class RequestParamController {
	
    // 반환 타입 없으면서 응답값 있으면 view 조회 X // ok만 화면에 표시됨
    @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");
    }

    @RequestMapping("/request-param-v2")
    @ResponseBody // HTTP 메시지 바디에 내용 입력 (view 조회 X)
    public String requestParamV2(
            @RequestParam("username") String memberName, // @Requestparam : 파라미터 이름으로 바인딩
            @RequestParam("age") int memberAge // = request.getParameter("age")
    ){
        log.info("username={}, age={}", memberName, memberAge);

        return "ok";
    }

    @RequestMapping("/request-param-v3")
    @ResponseBody
    public String requestParamV3(
            @RequestParam String username, // HTTP 파라미터명 = 변수명 이면 괄호값 생략 가능
            @RequestParam int age
    ){
        log.info("username={}, age={}", username, age);

        return "ok";
    }

    @RequestMapping("/request-param-v4")
    @ResponseBody
    public String requestParamV4(String username, int age){ // String, int, Integer 등 단순 타입 => @ 생략 가능 
        log.info("username={}, age={}", username, age);

        return "ok";
    }

    @RequestMapping("/request-param-required")
    @ResponseBody
    public String requestParamV5(
            @RequestParam String username, // required 기본값 true (어노테이션 생략 시 기본값 false)
            @RequestParam(required = false) Integer age){ // int는 null 허용 X -> 500 에러 (defaultValue 나 Integer 사용하기)
        log.info("username={}, age={}", username, age);

        return "ok";
    }
    // required=true 인데 해당 파라미터 이름, 값 전달 X => 400 예외
    // 파라미터 이름만 있으면 => 빈 문자열로 통과

    @RequestMapping("/request-param-default")
    @ResponseBody
    public String requestParamDefault(
            @RequestParam(defaultValue = "guest") String username, // null, 빈 문자에 적용
            @RequestParam(required = false,defaultValue = "-1") int age){
        log.info("username={}, age={}", username, age);

        return "ok";
    }
	/**
 	* @RequestParam Map, MultiValueMap
	* Map(key=value)
	* MultiValueMap(key=[value1, value2, ...]) ex) (key=userIds, value=[id1, id2])
 	*/
    @RequestMapping("/request-param-map")
    @ResponseBody
    public String requestParamMap(@RequestParam Map<String, Objects> paramMap){
        log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));

        return "ok";
    }

    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData){ // => HelloData 객체 생성, 요청 파라미터 명의 프로퍼티 찾아 바인딩
        //HelloData data = new HelloData();
		//data.setUsername(username);
		//data.setAge(age);
        
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        log.info("helloData={}", helloData);

        return "ok";
    }

    @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2(HelloData helloData){ // 단순 타입 외에는 @ModelAttribute으로 인식 => 생략 가능 (argument resolver 지정한 타입 외)
        log.info("helloData={}", helloData);

        return "ok";
    }

}

 

 

 

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

 

요청 파라미터와 다르게 HTTP 메시지 바디를 통해 직접 넘어오면 @RequestParam, @ModelAttribute 사용 불가 (HTML Form은 요청 파라미터로 인정)

 

 

  • RequestBodyStringController
package hello.springmvc.basic.request;

import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;

@Slf4j
@Controller
public class RequestBodyStringController {

    @PostMapping("/request-body-string-v1")
    public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // InputStream을 사용해 HTTP 메시지 바디 데이터 읽음
        ServletInputStream inputStream = request.getInputStream(); 
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        log.info("messageBody={}", messageBody);

        response.getWriter().write("ok");
    }
	
    /**
 	* InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
 	* OutputStream(Writer): HTTP 응답 메시지 바디에 결과 직접 출력
 	*/
    @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");
    }

	/**
 	* HttpEntity: HTTP header, body 정보를 편리하게 조회
 	* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
 	* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용 (HTTP 메시지 바디 읽어 문자/객체로 변환, 전달)
 	*
 	* 응답에서도 HttpEntity 사용 가능
 	* - 메시지 바디 정보 직접 반환(view 조회X)
 	* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
 	*/
    @PostMapping("/request-body-string-v3")
    public ResponseEntity<String> requestBodyStringV3(RequestEntity<String> httpEntity) throws IOException {
        //String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        String messageBody = httpEntity.getBody();

        log.info("messageBody={}", messageBody);

        return new ResponseEntity<String>("ok", HttpStatus.CREATED);
    }
    // HttpEntity 상속받은 객체들 =>
    // @RequestEntity : HttpMethod, url 정보 추가됨
    // @ResponseENtity : HTTP 상태 코드 설정 가능

	/**
 	* @RequestBody
 	* - 메시지 바디 정보를 직접 조회 (헤더 정보 필요하면 @HttpEntity or @RequestHeader)
 	*
 	* @ResponseBody
 	* - 메시지 바디 정보 직접 반환 (view 조회X)
 	*/
    @ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {
        log.info("messageBody={}", messageBody);

        return "ok";
    }

}

 

요청 파라미터 조회 : @RequestParam , @ModelAttribute
HTTP 메시지 바디 조회 : @RequestBody

 

 

 

HTTP 요청 메시지 - JSON

 

  • RequestBodyJsonController
package hello.springmvc.basic.request;

import com.fasterxml.jackson.databind.ObjectMapper;
import hello.springmvc.basic.HelloData;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.IOException;
import java.nio.charset.StandardCharsets;


/**
 * {"username":"hello", "age":20}
 * content-type: application/json
 */
@Slf4j
@Controller
public class RequestBodyJsonController {

	// Jackson 라이브러리 ObjectMapper 사용해 JSON → 자바 객체 변환
    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); //json 데이터를 HelloData로 매핑
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        response.getWriter().write("ok");
    }

    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
        log.info("messageBody={}", messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); //json 데이터를 HelloData로 매핑
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return "ok";
    }

    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData messageBody) { //@RequestBody 생략 불가 // 생략 시 @ModelAttrivute로 인식, 기본값 대입
        log.info("messageBody={}", messageBody);

        return "ok";
    }

    @PostMapping("/request-body-json-v4")
    public String requestBodyJsonV4(@HttpEntity<HelloData> httpEntity) {
    	HelloData data = httpEntity.getBody();
        log.info("messageBody={}", data);

        return "ok";
    }

    @ResponseBody // JSON으로 응답
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) { // JSON -> 객체
        log.info("messageBody={}", data);

        return data;
    }

}

 

 

 

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

 

 

  • 서버에서 응답 데이터 만드는 방법

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

 

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

 

3. HTTP 메시지 사용
HTTP API => HTML이 아니라 데이터를 전달

→ HTTP 메시지 바디에 JSON 등 형식으로 데이터 전달

 

 

 

  • 정적 리소스 (파일 변경 없이 그대로 서비스)

스프링부트는 클래스패스의 하단 디렉토리에 있는 정적 리소스 제공함

/static , /public , /resources , /META-INF/resources

 

src/main/resources/static/basic/hello-form.html

=>

http://localhost:8080/basic/hello-form.html

 

 

 

  • 뷰 템플릿 (동적 HTML)

기본 뷰 템플릿 경로 : src/main/resources/templates

 

 

 

  • hello.html (타임리프)
<!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>

 

 

 

  • ResponseViewController
package hello.springmvc.basic.response;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class ResponseViewController {

    @RequestMapping("/response-view-v1")
    public ModelAndView responseViewV1(){
        ModelAndView mav = new ModelAndView("response/hello")
                .addObject("data", "hello!"); //선택한 뷰 내 object 값에 hello! 대입

        return mav; 
    }

    // @ResponseBody X => 사용 시 페이지 반환 안 하고 String 자체를 화면에 띄움
    @RequestMapping("/response-view-v2")
    public String responseViewV2(Model model){
        model.addAttribute("data", "hello!");

        return "response/hello"; // response/hello 로 뷰 리졸버 실행, 뷰 찾아 렌더링
    }

    // 불명확하므로 권장 X
    @RequestMapping("/response/hello") //경로값=반환값 같으면 해당 값 페이지 자동 반환
    public void responseViewV3(Model model){
        model.addAttribute("data", "hello!");
    }

}

 

 

※ application.properties

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

 

 

 

  • HTTP 메시지 바디에 직접 입력 (HTTP API)

※ HTML, 뷰 템플릿도 HTTP 응답 메시지 바디에 HTML 데이터 담아 전달함

이 방식은 정적 리소스/뷰 템플릿 거치지 않고 직접 HTTP 응답 메시지 전달하는 경우

 

  • ResponseBodyController
package hello.springmvc.basic.response;

import hello.springmvc.basic.HelloData;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.io.IOException;

@Slf4j
@Controller
// @RestController => @Controller + @ResponseBody
public class ResponseBodyController {

    @GetMapping("/response-body-string-v1")
    public void responseBodyV1(HttpServletResponse response) throws IOException {
        response.getWriter().write("ok"); // response 객체 통해 직접 메시지 바디 입력
    }
    
 	/**
 	* HttpEntity, ResponseEntity(Http Status 추가)
 	* @return
 	*/
    @GetMapping("/response-body-string-v2")
    public ResponseEntity<String> responseBodyV2() {
        return new ResponseEntity<>("ok", HttpStatus.OK); // HTTP 응답 코드, 메시지 바디 내용 전달
    }

    @ResponseBody // view 사용하지 않고 메시지 내용 입력
    @GetMapping("/response-body-string-v3")
    public String responseBodyV3() {
        return "ok";
    }

    @GetMapping("/response-body-json-v1")
    public ResponseEntity<HelloData> responseBodyJsonV1() { //ResponseEntity 반환 (객체 -> Json)
        HelloData helloData = new HelloData();
        helloData.setUsername("abc");
        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("abc");
        helloData.setAge(20);

        return helloData;
    }

}

 

 

 

HTTP 메시지 컨버터

 

  • @ResponseBody 원리

@ResponseBody

→ HTTP BODY에 문자 내용을 직접 반환
  viewResolver 대신 HttpMessageConverter 동작

(기본 문자 처리: StringHttpMessageConverter, 기본 객체 처리: MappingJackson2HttpMessageConverter)

 

※ 응답 => 클라이언트의 HTTP Accept 헤더 & 서버의 컨트롤러 반환 타입 정보 조합해 HttpMessageConverter 선택

 

 

 

  • 스프링MVC가 사용하는 HTTP 메시지 컨버터

HTTP 요청: @RequestBody , HttpEntity(RequestEntity)
HTTP 응답: @ResponseBody , HttpEntity(ResponseEntity)

=> 이 때 HTTP 메시지 컨버터 적용

 

 

  • HTTP 메시지 컨버터 인터페이스
package org.springframework.http.converter;

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[] , 미디어타입: */* 
요청 ex) @RequestBody byte[] data
응답 ex) @ResponseBody return byte[]  + 미디어타입 application/octet-stream

 

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

 

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

 

 

  • 적용 예시
content-type: application/json

@RequestMapping
void hello(@RequestBody String data) {}

=> StringHttpMessageConverter

 

content-type: application/json

@RequestMapping
void hello(@RequestBody HelloData data) {}

=> MappingJackson2HttpMessageConverter

 

content-type: text/html

@RequestMapping
void hello(@RequestBody HelloData data) {}

=> 오류

 

 

 

  • 동작 과정

HTTP 요청 데이터 읽기
HTTP 요청이 오고, 컨트롤러에서 @RequestBody , HttpEntity 파라미터 사용
→ 메시지 컨버터가 메시지 읽을 수 있는지 확인하기 위해 canRead() 호출 (대상 클래스 타입, HTTP 요청의 Content-Type 미디어 타입)
  canRead() 조건 만족 시 read() 호출

  객체 생성, 반환

 

HTTP 응답 데이터 생성
컨트롤러에서 @ResponseBody , HttpEntity 로 값이 반환
  메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 호출 (대상 클래스 타입, HTTP 요청의 Accept 미디어 타입 - 정확히는 @RequestMapping 의 produces )
  canWrite() 조건 만족 시 write() 호출

  HTTP 응답 메시지 바디에 데이터 생성

 

 

 

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

 

  • 구조

HTTP 메시지 컨버터

=> 애노테이션 기반의 컨트롤러

= @RequestMapping 을 처리하는 핸들러 어댑터

= RequestMappingHandlerAdapter 가 담당

 

 

  • RequestMappingHandlerAdapter 동작 방식

 

 

  • ArgumentResolver (=HandlerMethodArgumentResolver)

: 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체) 생성

 

스프링 => 30개 이상의 ArgumentResolver 기본 제공

 

public interface HandlerMethodArgumentResolver {

	boolean supportsParameter(MethodParameter parameter);

	@Nullable
	Object resolveArgument(
    		MethodParameter parameter, 
    		@NullableModelAndViewContainer mavContainer,
		NativeWebRequest webRequest, 
        	@Nullable WebDataBinderFactory binderFactory) 
	throws Exception;
}

 

supportsParameter() 호출, 해당 파라미터를 지원하는지 체크  
→ 지원 시 resolveArgument() 를 호출해서 실제 객체 생성

→  컨트롤러 호출 시 해당 객체 전달

 

※ 인터페이스 확장해 원하는 ArgumentResolver 만들 수 있음

 

 

  • ReturnValueHandler (=HandlerMethodReturnValueHandler)

: 응답 값 변환, 처리

 

ex) 컨트롤러에서 String으로 뷰 이름을 반환해도 동작하도록 해줌

ex) ModelAndView , @ResponseBody , HttpEntity , String

 

 

  • HttpMessageConverter

 

요청

@RequestBody 담당 ArgumentResolver, HttpEntity 담당 ArgumentResolver가 있음

해당 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해 필요한 객체 생성함

 

응답

@ResponseBody와 HttpEntity를 처리하는 ReturnValueHandler가 있음

=> HTTP 메시지 컨버터를 호출해 응답 결과 생성

 

@RequestBody, @ResponseBody => RequestResponseBodyMethodProcessor (ArgumentResolver) 사용

HttpEntity => HttpEntityMethodProcessor (ArgumentResolver) 사용

 

 

  • 확장

HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler, HttpMessageConverter 

=> 모두 인터페이스 => 확장 가능

 

확장 원하면 WebMvcConfigurer 상속받아 스프링 빈으로 등록하면 됨

 

@Bean
public WebMvcConfigurer webMvcConfigurer() {
 	return new WebMvcConfigurer() {
 		@Override
 		public void addArgumentResolvers(List<HandlerMethodArgumentResolver>resolvers) {
 			//...
 		}
	 
 		@Override
 		public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
 			//...
 		}
 	};
}