Spring

[Spring MVC] 스프링 MVC(4)

챛채 2023. 6. 1. 18:35
  • 스프링 로깅에 대해 알아볼 수 있다.
  • HTTP 요청과 응답을 처리하는 다양한 방식 알아본다.
  • HTTP 메시지 컨버터에 대해 알아본다.

6. 스프링 MVC -기본기능

1) 로깅 알아보기

운영 시스템에서 System.out.println() 같은 시스템 콘솔을 사용하여 필요한 정보를 출력하지 않고, 별도의 로깅 라이브러리를 사용해서 로그를 출력한다.

스프링부트 라이브러리 사용시 로깅 라이브러리(spring-boot-starter-logging)가 함께 포함되는데 스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.

  • SLF4J
  • Logback

SLF4J는 Logback, Log4J, Log4J2 등 많은 라이브러리를 통합해서 인터페이스로 제공한다.

SLF4J는 인터페이스이고 그 구현체로 Logback 같은 로그 라이브러리를 사용한다.

 

로그 선언 방법

  • private Logger log = LoggerFactory.getLogger(getClass());
  • private static final Logger log = LoggerFactory.getLogger(Xxx.class)
  • @Slf4j : 롬복 사용 가능

[LogTestController.java]

@Slf4j //lombok이 제공하는 에노테이션
@RestController
public class LogTestController {
	//Logger는 org.lsf4j.Logger 인터페이스를 사용하고 getClass()로 자신을 넣으면 됨.
    // private final Logger log = LoggerFactory.getLogger(getClass());
    //Slf4j 에노테이션이 자동으로 추가해줌

    @RequestMapping("/log-test")
    public String logTest() {
        String name = "spring";
        System.out.println("name = " + name);
        log.info(" info log=" + name); //이렇게 사용하면 안 됨(연산이 일어남)
        //" info log=Spring"

        log.trace("trace log={}", name); //치환
        log.debug("debug log={}", name); //개발서버
        log.warn("warn log={}", name);
        log.error("error log={}", name); //경고
        log.info("info log ={}", name); //에러
       
       return "ok";
        //@RestController -> 문자열("ok")을 반환하면 String이 그대로 바로 반환됨 http 메시지 바디에 바로 들어감

  }

}

@Controller가 아닌 @RestController를 사용했다. @Controller는 요청 매핑 메소드 반환 타입이 String이면 뷰 이름으로 인식이 된다. 그래서 뷰를 찾고 뷰가 랜더링 된다. 하지만 @RestController는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력을 해서 실행 결과로 ok 메시지를 받을 수 있다.

 

@Controller를 쓰고 메소드에 @ResponseBdoy를 붙여도 동일한 기능을 한다.

 

로그 레벨 순서는 TRACE > DEBUG > INFO > WARN > ERROR 순인데 개발 서버는 보통 debug를 출력하고 운영은 info를 출력한다. 예를들어 info의 경우 info, warn, error까지 상위 레벨의 로그까지 나온다.

 

로그 레벨 설정은 application.properties에서 할 수 있다.

[application.properties]

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


#hello.springmvc 패키지와 그 하위 로그 레벨 설정(TRACE까 가장 낮은 레벨)
#운영에서는 기본적으로 info로 설정하여 info, warn, error을 보여줌
logging.level.hello.springmvc=debug
  • log.debug("data=" + data);
    • 로그 출력 레벨 info 설정시에도 해당 코드에 있는 + 연산이 실행되어 리소스 낭비
  • log.debug("data={}", data);
    • 로그 출력 레벨을 info로 설정하면 아무일도 발생 x, 의미없는 연산 발생 x

로그 사용 장점

  • 쓰레드 정보, 클래스 이름 같은 부가 정보 함께 볼 수 있고, 출력 모양 조절 가능
  • 로그 상황에 맞게 조절 가능
  • 파일, 네트워크 등 로그 별도의 위치에 남길 수 있다. (파일로 남길 때에는 일별, 특정 용량에 따라 로그 분할도 가능)
  • System.out보다 성능도 좋음

2) 요청 매핑

요청을 컨트롤러에서 매핑하는 여러가지 방법들에 대해 알아보자.

요청 매핑이란 URL 요청이 왔을 때 어떤 컨트롤러가 호출이 되어야할지 mapping해 놓는 것을 말한다.

 

[MappingController.java]

@RestController
public class MappingController {
    private Logger log = LoggerFactory.getLogger(getClass());
    
    /* 1. 기본 요청
    * @RequestMapping({"/hello-basic", "hello-go"}) 이렇게도 가능
    * HTTP 메서드 모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE
    * */
    @RequestMapping(value = "/hello-basic")
    public String helloBasic() {
        log.info("helloBasic");
        return "ok";
    }

    //2.method 특정 HTTP 메서드 요청만 허용, 여기에 POST 요청하면 405상태코드 반환
    @RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
    public String mappingGetV1() {
        log.info("mappingGetV1");
        return "ok";
    }

    /*
     * 3. 편리한 축약 애노테이션 (코드보기)
     * @GetMapping
     * @PostMapping
     * @PutMapping
     * @DeleteMapping
     * @PatchMapping
     */
    @GetMapping(value = "/mapping-get-v2") //코드 내부에서 @ReuqestMapping과 method를 지정해서 사용하는 것 확인 가능
    public String mappingGetV2() {
        log.info("mapping-get-v2");
        return "ok";
    }

    /*
    * pathVariable(경로 변수) 사용 ->제일 자주 쓰임
    * 리소스 경로에 식별자 넣는 스타일
    *
    * 변수명이 같으면 생략 가능
    * @PathVariable("userId") String userId -> @PathVariable user Id
    *
    * http://localhost:8080/mapping/userA
    *
    * */
    @GetMapping("/mapping/{userId}")
    public String mappingPath(@PathVariable("userId")String data) {
        log.info("mappingPath userId={}", data);
        return "ok";
    }

    /**
     * PathVariable 사용 다중
     * http://localhost:8080/mapping/users/userA/orders/100
     */
    @GetMapping("/mapping/users/{userId}/orders/{orderId}")
    public String mappingPath(@PathVariable String userId, @PathVariable Long
            orderId) {
        log.info("mappingPath userId={}, orderId={}", userId, orderId);
        return "ok";
    }


    /** 특정 파라미터 조건 매핑 params = "key=value"
     * value에 해당하는 url 뒤의 query string 형식을 통해 특정 파라미터가 있거나 없는 조건을 통해 추가
     	매핑할 수 있다. 
     * 파라미터로 추가 매핑
     * ?mode=debug가 아닌 다른 걸 전송하면 400 Bad Request
     * params="mode",
     * params="!mode"
     * params="mode=debug"
     * params="mode!=debug" (! = )
     * params = {"mode=debug","data=good"}
     *
     * http://localhost:8080/mapping-param?mode=debug
     */
    @GetMapping(value = "/mapping-param", params = "mode=debug")
    public String mappingParam() {
        log.info("mappingParam");
        return "ok";
    }

    /**
     * 특정 헤더로 추가 매핑
     * HTTP 요청 메세지의 header 내용을 통해 추가 매핑할 수 있다.
     * 헤더에 mode=debug를 안 넣어주면 404
     * headers="mode",
     * headers="!mode"
     * headers="mode=debug"
     * headers="mode!=debug" (! = )
     */
    @GetMapping(value = "/mapping-header", headers = "mode=debug")
    public String mappingHeader() {
        log.info("mappingHeader");
        return "ok";
    }

    /**
     * Content-Type 헤더 기반 추가 매핑 Media Type
     * 내가 보낼 요청 Header에 Content-Type=application/json이 아니라면 415 Unsupported Media Type
     * consumes="application/json"
     * consumes="!application/json"
     * consumes="application/*"
     * consumes="*\/*"
     * MediaType.APPLICATION_JSON_VALUE
     *
     * HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑
     *     만약 맞지 않으면 HTTP 415 상태코드 반환
     */
    @PostMapping(value = "/mapping-consume", consumes = "application/json")
    public String mappingConsumes() {
        log.info("mappingConsumes");
        return "ok";
    }


    /**미디어 타입 조건 매핑-HTTP 요청 Accept, produce
     * return할 데이터 타입 명시 
     * Accept 헤더 기반 Media Type
     * produces = "text/html"
     * produces = "!text/html"
     * produces = "text/*"
     * produces = "*\/*"
     *
     * HTTP 요청의 Accept헤더를 기반으로 미디어 타입으로 매핑 ->만약 맞지 않으면 HTTP 406 상태코드 반환
     * 
     */
    @PostMapping(value = "/mapping-produce", produces = "text/html")
    public String mappingProduces() {
        log.info("mappingProduces");
        return "ok";
    }
}

@RequestMapping("/hello-basic") : /hello-basic URL 호출이 오면 이 메서드가 실행되도록 매핑한다. {"/hello-basic", "/hello-go"} 이런 식으로 다중 매핑도 가능하다. method 속성으로 HTTP 메서드 지정하지 않으면 무관하게 호출된다.(GET, HEAD, POST, PATCH, DELETE 전부 허용된다.)

 

3) 요청 매핑- API 예시

회원 관리를 HTTP API로 만든다고 가정하면 다음과 같이 매핑

 

[MappingClassController.java]

package hello.springmvc.basic.requestmapping;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
    /*
     회원 관리 API
     회원 목록 조회: GET /users
     회원 등록: POST /users
     회원 조회: GET /users/{userId}
     회원 수정: PATCH /users/{userId}
     회원 삭제: DELETE /users/{userId}
    */
    
    /**
     * GET /mapping/users
     */
    @GetMapping
    public String user() {
        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;
    }
}

@RequestMapping("/mapping/users") 클래스 레벨에 매핑 정보를 두면 메서드 레벨에서 해당 정보를 조합해서 사용한다.

 

4)HTTP 요청 - 기본, 헤더 조회

[RequestHeaderController.java]

@Slf4j
@RestController
public class RequestHeaderController {

    @RequestMapping("/headers")
    public String headers(HttpServletRequest request,
                          HttpServletResponse response,
                          HttpMethod httpMethod, //HTTP 메서드 조회
                          Locale locale, //Locale 정보 조회
                          @RequestHeader MultiValueMap<String, String> headerMap, //모든 HTTP 헤더를 MultiValueMap 형식으로 조회
                          @RequestHeader("host") String host, //특정 HTTP 헤더 조회
                          @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";
    }
}

MultiValueMap는 MAP과 유사하지만 하나의 키에 여러 값을 받을 수 있다.

keyA=value1&keyA=value2와 같은 형태로 요청을 보내면 keyA=[vlaue1,vlaue2]로 담긴다.

 

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

클라이언트에서 서버로 요청 데이터를 전달할 때 사용하는 방법에는 3가지가 있다.

  1. GET-쿼리 파라미터 : request.getParameter("파라미터명"), 메시지 바디 없음
  2. POST-HTML Form : 메시지 바디에 쿼리 파라미터 형식으로 전달, request.getParameter()사용 가능, 메시지 바디 있음
  3. HTTP message body : HTTP API에서 주로 사용 주로 JSON형태로 보냄, HTTP message Body에 직접 데이터 담아서 요청함

GET 쿼리파라미터와 POST HTML Form 전송 방식은 형식이 같아서 구분 없이 request.getParameter()를 사용해서 조회가 가능하다. @RequestParam 어노테이션을 사용하면 단순 타입 (String, int 등)을 바로 가져올 수 있다. 생략도 가능하다.

 

요청 파라미터가 참조형 객체일 때 @ModelAttribute를 사용할 수 있고 객체면 생략도 가능하다.

 

[RequestParamController.java]

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


    /*
    * @RequestParam 사용 - 파라미터 이름으로 바인딩
    * @ResponseBody 추가 - View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
    *
    * */
    @ResponseBody //View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") String memberName, //request.getParameter("username")
            @RequestParam("age") int memberAge) {   //@RequestParam : 파라미터 이름으로 바인딩
        log.info("username={}, age={}", memberName, memberAge);
        return "ok";
    }

    //@RequestParam 사용 -> HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
    /*
    만약 url이 ?username=userA&age=20이면 @RequestParam을 통해 변수 username에는 userA가, 변수 age에는
    20이 들어감
    */
    @ResponseBody
    @RequestMapping("/request-param-v3")
    public String requestParamV3(
            @RequestParam String username,
            @RequestParam int age){
        log.info("username={}, age={}", username, age);
        return "ok";
    }


    //@RequestParam 사용 -> String, int 등 단순 타입이면 @RequestParam도 생략 가능
    @ResponseBody
    @RequestMapping("/request-param-v4")
    public String requestParamV4(String username, int age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }
//
//    @RequestParam.required
//    /request-param-required -> username이 없으므로 예외
//
//    주의!
//    /request-param-required?username= -> 빈문자로 통과 (null 아님)
//
//    주의!
//    /request-param-required
//    int age-> null을 int에 입력하는 것 불가능, 따라서 Integer 변경해야 함(또는 defaultValue 사용)
    @ResponseBody
    @RequestMapping("/request-param-required")
    public String requestParamRequired(
            @RequestParam(required = true) String username, //username이 필수로 들어가야함
            @RequestParam(required = false) Integer age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }

    /*
         @RequestParam-defaultValue 사용
         참고: defaultValue는 빈 문자의 경우에도 적용 -> guest로 들어옴
        /request-param-default?username=은 guest
     */
    @ResponseBody
    @RequestMapping("/request-param-default")
    public String requestParamDefault(
            @RequestParam(required = true, defaultValue = "guest") String username, //username이 필수로 들어가야함
            @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])
     */
    @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";
    }

    /*
    * public String modelAttributeV1(@RequestParam String username, @RequestParanm int age)
    *   HelloData helloData = new HelloData();
    *   helloData.setUsername(username);
    *   helloData.setAge(age);
    *
    * @ModelAttribute 사용 -> HelloData 객체가 생성되고, 요청 파라미터 값도 전부 들어가 있음
    * 객체 한 번에 받아서 사용 가능
    * age=abc처럼 타입이 안맞는 경우에는 BindException발생
    * */
    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(),
                helloData.getAge());
        return "ok";
    }

	/*
    @ModelAttribute는 생략 가능한데 @RequestParam도 생략할 수 있으니 혼란 발생
    */

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

@RequestParam("파라미터 이름") 자료형 변수명 이렇게 선언하고 만약 파라미터 이름과 변수명이 같은 경우에는 ("파라미터 이름")을 생략할 수 있다.

 

@RequestParam은 Servlet의 response.getParam()과 동일한 역할을 한다. response.getPram()의 return형은 String이어서 Integer.ParseInt()함수를 통하여 형변환을 해주어야 하지만 @RequestParam은 형변환을 알아서한 후에 변수에 넣어주어서 따로 신경 쓰지 않아도된다.

 

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

 

요청 파라미터와는 다르게 HTTP message body를 통해서 데이터가 직접 넘어오는 경우는 @RequestParam과 @ModelAttribute를 사용할 수 없다.

HTTP message body의 데이터를 InputStream을 사용해서 직접 읽을 수 있다.

 

[RequestBodyStringController.java]

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

    /*
    * 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 적용
    *
    * 응답에서도 HttpEntity 사용 가능
    * -메시지 바디 정보 직접 반환(view 조회x)
    * -HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
    * */
    @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");
    }
	
    /*
    요청 메시지를 @RequestBdoy로 처리가 가능
    헤더 정보 필요시 @RequestHeader, HttpEntity 추가 사용 가능
    메시지 바디 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와는 전혀 관계 없음
   
   	@RequestBody 
    -메시지 바디 정보 직접 조회
    -HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
    @ResponseBody
    -메시지 바디 정보 직접 반환(view 조회x)
    -HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
   
   */
    @ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messageBody) {

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

스프링 MVC 내부에서 HTTP 메시지 바디를 읽어 문자 또는 객체로 변환해서 전달해주는데, 이때 HttpMessageConverter라는 기능을 사용한다.

 

요청 파라미터를 조회하는 기능 : @RequestParam, @ModelAttribute

HTTP 메시지 바디를 직접 조회하는 기능 : @RequestBody

 

 

7)HTTP 요청 메시지 - JSON

단순 텍스트의 경우는 InputStream이나 @RequestBody등으로 처리가 가능하지만 JSON 요청을 보낼 경우에는 Jackson라이브러리의 ObjectMapper를 통하여 자바 객체로 변환하는 작업이 필요하다.

 

[RequestBodyJsonController.java]

/**
 * {"username":"hello", "age":20}
 * content-type: application/json
 */
@Slf4j
@Controller
public class RequestBodyJsonController {
    private ObjectMapper objectMapper = new ObjectMapper();

	//HttpServletRequest로 HTTP 메시지 바디에서 데이터 읽어온 후 문자 변환 ->Jackson라이브러리로 자바 객체 변환
    @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);

		//json형식 요청 데이터를 그대로 출력
        //messageBody={"username":"hello", "age"="20"}
        log.info("messageBody={}", messageBody);
        
        //json 형식으로 요청 온 데이터를 HelloData로 역직렬화
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        
        //역직렬화된 HelloData 데이터 출력
        //username=hello, age=20
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

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

    }

    /*
    * @RequestBody
    * HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
    *
    * @ResponseBody
    * -모든 메서드에 @ResponseBody 적용
    * -메시지 바디 정보 직접 반환(view 조회 x)
    * -HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
    * */
    @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody)throws IOException{

        log.info("messageBdoy={}", messageBody);
       
        //json 형식으로 요청 온 데이터를 HelloData로 역직렬화
        HelloData data = objectMapper.readValue(messageBody, HelloData.class);
        
        //역직렬화된 HelloData 데이터 출력
        //username=hello, age=20
        log.info("username={}, age={}", data.getUsername(), data.getAge());

        return "ok";
    }

    /*
    * @RequestBody 생략 불가능(@ModelAttribute가 적용되어 버림)
    * 요청 데이터를 objectMapper 없이 바로 객체로 받을 수 있음
    * json 데이터 형식(content-type:application/json)을 받으면 HTTP메시지컨버터가 ObjectMapper역할을 해줌
    * */
    @ResponseBody
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

	/**
     * HttpEntity로도 json형식을 받을 수 있다.
     * json 데이터 형식(content-type:application/json)을 받으면 HTTP메시지컨버터가 ObjectMapper역할을 해줌
     */
    @ResponseBody
    @PostMapping("/request-body-json-v4")
    public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
        HelloData data = httpEntity.getBody();
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

    /*
    * @RequestBody 생략 불가능(@ModelAttribute가 적용되어 버림)
    * HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter
    * (content-type : application/json)
    *
    * @ResponseBody 적용
    * -메시지 바디 정보 직접 반환(view 조회x)
    * -HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용
    * (Accept : application/json)
    * */
    /*
     * 반환타입도 HelloData타입으로 가능
     * HttpMessageConverter에 의해 객체가 json문자형태로 변환되어 반환
     * 요청시 json to object / 응답시 object to json
     * 응답 나갈 때 json으로 나갈지 판단은 요청시 accept 확인해야 함
     */
    @ResponseBody
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) { //json이 그대로 hellodata로 들어옴
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return data;
    }
}

requestBodyJsonV3과 같이 문자로 변환하는 작업 없이 바로 객체로 받을 수 있다.

HttpEntity, @RequestBody등을 사용하면 HTTP 메세지 컨버터가 HTTP 메시지 바디 내용(문자나 json)을 문자, 객체 등으로 변환해준다.

JSON처리할 수 있는 메시지 컨버터 실행하기 위해 요청시에는 content-type : application/json여부를 확인해야한다.

 

 

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

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

  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/baisc/hello-form.html

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

 

뷰 템플릿은 HTML을 동적으로 생성하여 전달한다.

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

//뷰 템플릿 생성
src/main/resources/templates/response/hello.html

 

[src/main/resources/templates/response/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.java] - 뷰 템플릿을 호출하는 컨트롤러

@Controller
public class ResponseViewController {

    @RequestMapping("/response-view-v1")
    public ModelAndView responseViewV1() {
        ModelAndView mav = new ModelAndView("response/hello")
                .addObject("data", "hello!");

        return mav;
    }

    //만약 여기서 @ResponseBody를 쓰면 view 안 찾고 response/hello가 응답 메시지 코드로 나감 (화면에 보여짐)
    @RequestMapping("/response-view-v2")
    public String responseViewV2(Model model) {
        model.addAttribute("data", "hello!");
        return "response/hello";

    }

    //절대 권장 X
    @RequestMapping("/response/hello")
    public void responseViewV3(Model model) {
        model.addAttribute("data", "hello!");
    }
    //컨트롤러의 경로 이름과 뷰의 논리적 이름이 같고 아무것도 반환을 안 하면 앞의 슬래쉬 떼고 response/hello가 논리적 뷰의 이름으로 진행됨

}

String을 반환하는 경우에는 @ResponseBody가 없으면 response/hello로 뷰리졸버가 실행되어 뷰를 찾고 랜더링한다.

@ResponseBody가 있으면(또는 HttpEntity 반환시) response/hello라는 문자가 HTTP 메시지 바디에 직접 입력된다.

 

response/hello는 논리명이기 때문에 실제 실행은 "template/resources/hello.html"로 된다.

 

그리고 스프링 프로젝트 생성시 타임리프(thymeleaf)를 의존성주입으로 추가해뒀기 때문에 자동으로 ThymeleaftViewResolver와 스프링 빈들을 등록한다.

 

 

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

HTTP API를 제공하는 경우 HTML이 아닌 데이터를 전달해야 하므로, HTTP Message Body에 JSON 형식으로 데이터를 실어 보낸다.

 

[ResponseBodyController.java]

@Slf4j
//@Controller
//@ResponseBody
@RestController //@Controller + @ResponseBody
public class ResponseBodyController {
//------------------------------문자 처리------------------------------
    //HttpServletResponse 객체를 통해서 Http 메시지 바디에 직접 응답 메시지 전달
    @GetMapping("/response-body-string-v1")
    public void responseBodyV1(HttpServletResponse response) throws IOException{
        response.getWriter().write("ok");
    }

    //HttpEntity는 Http 메시지의 헤더, 바디 정보를 갖고 있다.
    //ResponseEntity는 HTTP 응답 코드를 설정할 수 있다.
    @GetMapping("/response-body-string-v2")
    public ResponseEntity<String> responseBodyV2() throws IOException{
        return new ResponseEntity<>("ok", HttpStatus.OK);
    }

    //@ResponseBody를 사용하면 view 사용하지 않고 HTTP 메시지 컨버터를 통해서 HTTP 메시지 직접 입력 가능
    //@ResponseBody
    @GetMapping("/response-body-string-v3")
    public String responseBodyV3() {
        return "ok";
    }


    //------------------------------JSON 처리------------------------------
    //HTTP 메시지 컨버터를 통하여 JSON 형식으로 변환되어 반환
    @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);
    }

    /*
    * ResponseEntity는 응답 코드 설정할 수 있는데, @ResponseBody를 사용하면 이런 것을 설정하기
    * 까다롭기 때문에 @ResponseStatus(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;
    }


}

 

@ResponseBody를 클래스 레벨에 설정해주면 전체 메서드에 적용이된다.

@RestController는 @Controller과 @RestController가 같이 적용이 됨

 

 

10) HTTP 메시지 컨버터

HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용한다.

 

HttpMessageConverter가 작동하는 예시는 

  • localhost:8080/hello-api호출
  • helloController의 hello-api URL을 처리할 메소드 실행
  • @ResponseBody가 붙어있으면 HttpMessageConverter가 실행돼서 return값 반환

 

@ResponseBody를 사용하면 HTTP의 BODY에 문자 내용을 직접 반환하며, viewResolver 대신 HttpMessageConverter가 동작한다. 요청과 다르게 응답의 경우 클라이언트의 Http Header의 Accept와 서버 컨트롤러 반환타입 정보를 조합하여 컨버터의 종류 (StringHttpMessageConverter-기본 문자 처리, MappingJackson2HttpMessageConverter-기본 객체처리 등) 선택한다. 

 

스프링 MVC가 HTTP 메시지 컨버터를 적용하는 경우는 HTTP 요청, 응답 둘 다 사용된다

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

HttpMessageConverter 기능에는 크게 2가지가 있는데

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

 

메세지 컨버터 작동 순위도 있는데 다음과 같은 메시지 컨버터들은 HttpMessageConverter 인터페이스를 상속한 컨버터들이다.

  • 0 순위 : ByteArrayHttpMessageConverter
  • 1 순위 : StringHttpMessageConverter
  • 2 순위 : MappingJackson2HttpMessageConverter

몇가지 주요한 메시지 컨버터가 있는데

ByteArrayHttpMessageConverter의 경우 byte[]를 처리하며 클래스타입이 byte[]이고 미디어타입이 */*인 경우 동작한다.

  • Request 예시 : @RequestBody byte[] data
  • Reponse 예시 : @ResponseBody return byte[] / 쓰기 미디어 타입은 application/octet-stream(자동 결정)

 

StringHttpMessageConverter의 경우 String문자를 처리하며 클래스타입은 String, 미디어타입은 */*인 경우 동작한다.

  • Request 예시 : @RequestBody String data
  • Response 예시 : @ResponseBody return "ok" / 쓰기 미디어 타입 text/plain

 

MappingJackson2HttpMessageConverter는 클래스타입은 객체나 JSON타입을 주로 처리하며 미디어타입은 application/json이다.

  • Request 예시 : @RequestBody HelloData data
  • Response 예시 : @ReponseBody return helloData / 쓰기 미디어 타입 application/json

 

요청데이터를 읽는 방법에 대한 정리를 다음과 같이 할 수 있다.

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

 

응답데이터를 읽는 방법에 대한 정리를 다음과 같이 할 수 있다.

  • 컨트롤러 반환시 @ResponseBody, HttpEntity 반환타입 체크한다.
  • 메시지 컨버터의 canWrite()가 호출되어 메시지를 쓸 수 있는지 확인한다.
    • 1)대상 클래스 타입을 확인 : byte[], String, HelloData
    • 2)HTTP 요청의 Accept 미디어타입 지원여부 확인(@RequestMapping의 produces)
  • canWrite() 조건 만족시 write()를 호출하여 HTTP 응답메시지 바디에 데이터를 생성한다.

*/* : 아무거나 다 됨