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

윤경·2021년 9월 11일
0

Spring MVC

목록 보기
10/26
post-thumbnail

[8] HTTP 요청 파라미터 - @ModelAttribute

요청 파라미터를 받아 필요한 객체를 만들고 그 객체에 값을 넣어주어야 하는데 스프링에서는 이 과정을 완전히 자동화해준다. ➡️ @ModelAttribute 기능 제공

✔️ HelloData

요청 파라미터를 바인딩 받을 객체 생성

package hello.springmvc.basic;

import lombok.Data;

@Data   // @Getter , @Setter , @ToString , @EqualsAndHashCode , @RequiredArgsConstructor 를 자동으로 적용
public class HelloData {
    private String username;
    private int age;
}

✔️ RequestParamController 코드 추가

이 코드를
이렇게 쉽게 쓸 수 있음

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

        return "ok";
    }

📌 @ModelAttribute

  • HelloData 객체 생성
  • 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾음 → 해당 프로퍼티의 setter를 호출해 파라미터 값을 입력(바인딩)

객체에 getUsername(), setUsername()이 있다고 치면 이 객체는 username이라는 프로퍼티를 가지고 있다.
즉, username 프로퍼티의 값을 변경하려면 setUsername()이 호출, 조회하면 getUsername()이 호출된다.

✔️ 추가

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

        return "ok";
    }

➡️ 이 코드도 위 코드 밑에 추가해주면 되는데 이렇게 @ModelAttribute 또한 생략이 가능하다.

📌

String, int, Integer 같은 단순 타입 = @RequestParam
나머지 = @ModelAttribute (argument resolver로 지정해둔 타입 외)


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

요청 파라미터와 다르게 HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우@RequestParam, @ModelAttribute를 사용할 수 없다. (물론 HTML Form 형식으로 전달되면 요청 파라미터로 ㅇㅈ)

✔️ RequestBodyStringController

package hello.springmvc.basic.request;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

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

➡️ HTTP 바디 메시지를 InputStream을 사용해 직접 읽자.

✔️ v2 추가

➡️ 위 코드 아래에 해당 코드를 추가해주면 됨

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

InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력

✔️ v3 추가

// http 메시지 자체를 그대로 주고 받는 형식
    @PostMapping("/request-body-string-v3")
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException { // http 메시지 컨버터라는 게 작동하게 됨
        String messageBody = httpEntity.getBody(); // 변환된 바디를 꺼낼 수 있음

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

        return new HttpEntity<>("ok");
    }

Spring MVC가 지원하는 파라미터

  • HttpEntity: HTTP header, body 정보 편리하게 저회 가능
    요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와 관계 없음
    응답에도 사용 가능

HttpEntity를 상속받은 RequestEntity, ResponseEntity도 있음.
ResponseEntity는 상태 코드도 설정할 수 있다.


잘 된다!! 잘 돼!! 난 이제 스프링 없인 개발 할 수 없다.

그런데 이것마저 귀찮은 개발자들은 애노테이션을 사용한다.

✔️ v4 추가

@ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messageBody) {    // v4를 실무에서 가장 많이 쓰는
        log.info("messageBody={}", messageBody);

        return "ok";
    }

(결과 동작은 어차피 지금껏 했던 것과 같고 잘 돌아가니까 첨부하지 않겠다.)

@RequestBody
: http 메시지 바디 정보를 편리하게 조회.
헤더 정보가 필요하다면 HttpEntity 또는 @RequestHeader 사용하기.

⭐️ 이렇게 메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와 관계 없음

즉,

  • 요청 파라미터를 조회하는 기능: @RequestParam, @ModelAttribute
  • HTTP 메시지 바디를 직접 조회하는 기능: @RequestBody

@ResponseBody를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아 전달 할 수 있다.
물론 이 경우에도 뷰 사용 X


[10] HTTP 요청 메시지 - JSON

✔️ RequestBodyJsonController

package hello.springmvc.basic.request;

import com.fasterxml.jackson.databind.ObjectMapper;
import hello.springmvc.basic.HelloData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@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 data = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", data.getUsername(), data.getAge());

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

HttpServletRequest를 사용해 직접 http 메시지 바디에서 데이터를 읽어와 문자로 변환.
문자로 된 Json 데이터를 Jackson 라이브러리 (objectMapper)를 사용해 자바 객체로 변환.

✔️ v2

위 코드 밑에 추가하면 된다.

@ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
        HelloData data = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", data.getUsername(), data.getAge());

        return "ok";
    }

HttpServletRequest 객체 자체가 필요한 것은 아니므로 코드를 변경했다.

@RequestBody를 사용해 http 메시지에서 데이터를 꺼내어 messageBody에 저장.
문자로 된 json 데이터 messageBody를 objectMapper를 통해 자바 객체로 변환.

아직 만족하지 못한 개발자들은 문자 변환 → Json 변환 과정이 번거롭다. @ModelAttribute 처럼 한 번에 객체로 변환하고 싶다 ➡️ v3

✔️ v3

@ResponseBody
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());

        return "ok";
    }

@RequestBody에 직접 만든 객체를 지정할 수 있음

HttpEntity, @RequestBody를 사용하면 HTTP 메시지 컨버터가 바디의 내용을 원하는 문자나 객체로 변환해준다.

‼️ @RequestBody는 생략할 수 없다.

✔️ v4, v5

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

    @ResponseBody
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());

        return data;
    }

@ResponseBody: 응답의 경우에도 이 애노테이션을 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있다. 물론 HttpEntity 사용 가능

@RequestBody: JSON 요청 → HTTP 메시지 컨버터 → 객체
@ResponseBody: 객체 HTTP → 메시지 컨버터 → JSON 응답


[11] 응답 - 정적 리소스, 뷰 템플릿

📌 응답 데이터 만드는 방법 3가지

  • 정적 리소스
  • 뷰 템플릿 사용
  • HTTP 메시지 사용

정적 리소스

스프링 부트는 클래스 패스의 /static, /public, /resources, /META-INF/resources에 있는 정적 리소스를 제공

src/main/resources: 리소스를 보관하는 곳. 클래스패스의 시작 경로.
여기에 리소스를 넣어놓으면 스프링 부트가 정적 리소스로 서비스 제공

뷰 템플릿

: 뷰 템플릿을 거쳐 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
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!");

        return mav;
    }

    @RequestMapping("/response-view-v2")
    public String responseViewV2(Model model) {
        model.addAttribute("data", "hello!");
        // "/response/hello" -> 뷰의 논리 이름이 됨.
        return "/response/hello";
    }

    // 절대 권장하지 않는 방법 ^^
    // 컨트롤러의 경로 이름과 뷰의 논리 이름이 일치하면 아무것도 반환을 안해도 (void)
    // response/hello -> 이게 논리적 뷰 이름으로 요청이 되어버림
    @RequestMapping("/response/hello")
    public void responseViewV3(Model model) {
        model.addAttribute("data", "hello!");
    }
}

📌 v2처럼 String을 반환하는 경우 - View or HTTP 메시지

  • @ResponseBody없으면 response/hello 로 뷰 리졸버가 실행되어서 뷰를 찾고, 렌더링
  • @ResponseBody있으면 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 response/hello 라는 문자가 입력

📌 v3처럼 Void를 반환하는 경우 - 비추 비추 비추!!!!!!!!
@Controller를 사용하고, HttpServletResponse, OutputStream(Writer) 같은 HTTP 메시지
바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용

➡️ 명시성이 너무 떨어지고 이렇게 딱 맞는 경우도 없기 떄문에 비추!!!!!!!!!!


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

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

📌
HTML이나 뷰 템플릿을 사용해도 http 응답 메시지 바디에 html 데이터가 담겨 전달된다.

여기서 얘기하는 건 정적 리소스나 뷰 템플릿을 거치지 않고, 직접 http 응답 메시지를 전달하는 경우를 말한다.

✔️ ResponseBodyController

package hello.springmvc.basic.response;

import hello.springmvc.basic.HelloData;
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 javax.servlet.http.HttpServletResponse;
import java.io.IOException;

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

📌 responseBodyV1
: 서블릿을 직접 다룰 떄 처럼 HttpServletResponse 객체를 통해 바디에 직접 ok 응답 메시지를 전달

📌 responseBodyV2
: ResponseEntity는 HttpEntity를 상속 받았는데 이는 http 메시지의 헤더, 바디 정보를 가진다.
ResponseEntity는 여기에 HTTP 응답 코드를 설정할 수 있는 기능 추가

📌 responseBodyV3
: @ResponseBody를 사용하면 view를 거치지 않고 http 메시지 컨버터를 통해 메시지를 직접 입력할 수 있따. (ResponseEntity도 동일한 방식)

📌 responseBodyJsonV1
: ResponseEntity를 반환.
http 메시지 컨버터를 통해 JSON 형식으로 변환되어 반환

📌 responseBodyJsonV2
: ResponseEntity는 Http 응답 코드를 설정 할 수 있는데 @ResponseBody를 사용하면 못하므로 @ResponseStatus()로 응답 코드를 설정할 수 있다.

물론! 애노테이션이기 때문에 동적으로 응답 코드를 변경할 순 없다. ➡️ 동적으로 하려면 ResponseEntity 사용

📌 @RestController = @Controller + @ResponseBody


[13] HTTP 메시지 컨버터

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

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

HTTP 메시지 컨버터는 요청, 응답 둘 다 사용된다.

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

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

0순위 ByteArrayHttpMessageConverter
1순위 StringHttpMessageConverter
2순위 MappingJackson2HttpMessageConverter (JSON으로 바꿔주든, 객체로 바꿔주든 양방향)

스프링부트는 대상 클래스 타입, 미디어 타입을 체크해 사용 여부를 결정하고 만족하지 않는다면 다음 우선순위로 넘어간다.

📌 ByteArrayHttpMessageConverter
: byte[] 데이터 처리

  • 클래스 타입: byte[]
  • 미디어 타입: /

📌 StringHttpMessageConverter
: String 문자로 데이터 처리

  • 클래스 타입: String
  • 미디어 타입: /
  content-type: application/json
  @RequestMapping
  void hello(@RequetsBody String data) {}

📌 MappingJackson2HttpMessageConverter
: application/json (byte도 아니고 string도 아니고)

  • 클래스 타입: HashMap
  • 미디어 타입: application/json 관련
 content-type: application/json
 @RequestMapping
 void hello(@RequetsBody HelloData data) {}

HTTP 요청 데이터 읽기

  1. HTTP 요청이 오고 컨트롤러에서 @RequestBody, HttpEntity 파라미터 사용
  2. 메시지 컨버터가 메시지를 읽을 수 있는지 canRead()를 호출해 우선순위대로 체크
  • 대상 클래스 타입 지원?
  • 미디어 타입 지원?
  1. canRead() 조건을 만족하면 read()를 호출해 객체 생성, 반환

HTTP 응답 데이터 생성

  1. 컨트롤러에서 @ResponseBody, HttpEntity로 값 반환
  2. 메시지 컨버터가 메시지를 쓸 수 있는지 canWrite()를 호출해 확인
  • 대상 클래스 타입 지원?
  • 미디어 타입 지원?
  1. canWrite() 조건을 만족하면 write()를 호출해 응답 메시지 바디에 데이터 생성

[14] 요청 매핑 헨들러 어뎁터 구조

메시지 컨버터는 MVC 어디쯤 사용되지 ?? 🤷🏻

RequestMappingHandlerAdapter 동작 방식

애노테이션 기반 컨트롤러는 매우 다양한 파라미터 사용 가능 (HttpServletRequest, Model, @RequestParam, @ModelAttribute, @RequestBody, HttpEntity.. 등등)
➡️ 이는 ArgumentResolver 덕분

애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdaptor는 바로 ArgumentResolver를 호출해 컨트롤러가 필요로 하는 다양한 파라미터 객체를 생성한다. 이렇게 준비된 파라미터 값으로 컨트롤러를 호출하며 값을 넘겨주면 된다.

📌 HandlerMethodArgumentResolver를 줄여 ArgumentResolver라고 함

ArgumentResolversupportsParameter()를 호출해 해당 파라미터를 지원하는지 체크.
지원한다면? resolveArgument()를 호출해 실제 객체를 생성.
이렇게 생성된 객체가 컨트롤러 호출 시 넘어감.

📌 HandlerMethodReturnValueHandler를 줄여 ReturnValueHandle라고 함

이는 ArgumentResolver와 비슷한데 응답 값을 변환하고 처리함.

컨트롤러에서 String으로 뷰 이름을 반환해도 동작하는 이유는 ReturnValueHandler덕분이다. (우리가 앞에서 이것저것으로 반환했던 것들이 다 동작하는 이유!)

HTTP 메시지 컨버터 위치

HTTP 메시지 컨버터를 사용하는 @RequestBody도 컨트롤러가 필요로 하는 파라미터 값에 사용되고 @ResponseBody도 컨트롤러 반환 값을 이용한다.

📌 요청
@RequestBody를 처리하는 ArgumentResolver가 있고,
HttpEntity를 처리하는 ArgumentResolver가 있다.
이 ArgumentResolver들이 HTTP 메시지 컨버터를 사용해 필요한 객체를 생성

📌 응답
@ResponseBodyHttpEntity를 처리하는 ReturnValueHandler가 있는데 여기서 HTTP 메시지 컨버터를 호출해 응답 결과를 생성

✔️ @RequestBody, @ResponseBody ➡️ RequestResponseBodyMethodProcessor (ArgumentResolver)

✔️ HttpEntity ➡️ HttpEntityMethodProcessor (ArgumentResolver)를 사용

확장을 원한다면 HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler, HttpMessageConverter를 인터페이스로 제공하기 때문에 언제든지 구현체를 넣어 확장할 수 있다.
But, 확장할 일이 없음



내 Dock bar에 갇힌 하하님

profile
개발 바보 이사 중

0개의 댓글