BackEnd/Spring MVC

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

샤아이인 2022. 2. 27.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

5. HTTP 요청 파라미터 - @ModelAttribute

이전까지는 요청 파라미터를 받아서 필요한 인자를 추출하고, 그 값들을 객체를 만들어 넣어주는 방식을 취하였다. 다음과 같이 말이다.

@RequestParam String username;
@RequestParam int age;

HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
 

스프링은 이 과정을 자동적으로 처리해주는 @ModelAttribute 기능을 지원한다.

 

● HelloData

import lombok.Data;

@Data
public class HelloData {
    private String username;
    private int age;
}
 

롬복으로 @Data를 사용하면 @Getter , @Setter , @ToString , @EqualsAndHashCode , @RequiredArgsConstructor 를 자동으로 적용해준다.

 

● @ModelAttribute 적용 - modelAttributeV1

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

파라미터로 선언된 helloData 객체에 요청 파라미터의 값들이 모두 들어가있다.

 

스프링MVC는 @ModelAttribute 가 있으면 다음을 실행한다.

1) HelloData 객체를 생성한다.

2) 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다.

그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.

 

예) 파라미터 이름이 username 이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다.

 

만약 int값을 담는 age 프로퍼티에 "abc"와 같은 문자열이 전달된다면 BindException 이 발생한다.

이런 바인딩 오류를 처리하는 방법은 검증 부분에서 다룬다.

 

또한 @ModelAttrbute는 생략이 가능하다.

한가지 의문이 든다. 이전에 @RequestParam도 생략이 가능했는데... 어떻게 구별할까?

 

스프링은 해당 생략시 다음과 같은 규칙을 적용한다.

- String , int , Integer 같은 단순 타입 => @RequestParam 생략으로 인식

- 나머지 => @ModelAttribute 생략으로 인식 (argument resolver 로 지정해둔 타입 외)

 

● @ModelAttribute의 name 속성!

@ModelAttribute는 name 속성을 지정하여 model에 지정한 이름으로 자동 추가해 준다.

즉 2가지 역할을 하게되는 것 이다.

 

1) 요청 파라미터 처리

@ModelAttribute 는 Item 객체를 생성하고, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력해준다.

 

2) Model에 추가 (view에서 사용하기 위함)

@ModelAttribute 는 중요한 한가지 기능이 더 있는데, 바로 모델(Model)에 @ModelAttribute 로 지정한 객체를 자동으로 넣어준다.

아래 코드를 보면 model.addAttribute("item", item) 가 없어도 잘 동작하는 것을 확인할 수 있다.

 

다음 코드를 살펴보자. 우선 불편하게 @RequestParam으로 직접 값을 받은후, 객체를 생성하여 model에 집접 추가하는 방식을 보자.

@PostMapping("/add")
public String addItemV1(@RequestParam String itemName, @RequestParam int price, @RequestParam Integer quantity,
                          Model model) {
    Item item = new Item();
    item.setItemName(itemName);
    item.setPrice(price);
    item.setQuantity(quantity);
    itemRepository.save(item);
    model.addAttribute("item", item);
    return "basic/item";
}
 

하나하나 직접 추가하게되니 매우 번거롭다.

 

이를 대신하기 위해 @ModelAttribute("item")을 사용하면, "item"값을 key값으로 model에 담게 된다.

또한 컨트롤러의 인자에 Model model을 추가해줄 필요도 없다.

@ModelAttribute("hello") Item => item 이름을 hello 로 지정

/**
* @ModelAttribute("item") Item item
* model.addAttribute("item", item); 자동 추가 
*/
@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item) {
    itemRepository.save(item); //model.addAttribute("item", item); //자동 추가, 생략 가능
    return "basic/item";
}
 

(추가, 만약 @ModelAttribute Item item 과 같이 이름을 지정해주지 않으면, class명의 첫 글자를 소문자로 바꾼

Item => item 즉 "item"을 이름으로 Model에 등록해준다.)

 

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

HTTP message body에 데이터를 직접 담아서 요청하는경우도 많다.

- HTTP API에서 주로 사용, JSON, XML, TEXT 사용

- 데이터 형식을 주로 JSON 사용하여 전달한다.

- POST, PUT, PATCH 와 같은 메서드 사용이 가능하다.

 

기존의 요청 파라미터들은 @RequestParam, @ModelAttribute 를 사용했지만, HTTP 메시지 바디를 통해서 데이터가 넘어올때는 기존의 방식을 사용할수가 없다.

 

우선 단순한 TEXT부터 메시지 바디에 담아서 전송하고, 읽어보자.

InputStream을 사용하여 직접 읽을 수 있다.

 

● RequestBodyStringController

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

Postman 을 통하여 http://localhost:8080/request-body-string-v1 에 요청해보자. 정상적으로 받을 수 있다.

하지만 아직 불편한 부분이 많다.

서블릿의 기능을 전부 사용하는것도 아닌데 HttpServletRequest를 인자로 받고 있으며, 받은 후에도 후처리로 Stream()을 변환하는 등 복잡한 과정이 선행되어야만 message body를 살펴볼수 있다.

 

이를 수정해 보자!!

 

● Input, Output 스트림, Reader - requestBodyStringV2

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

스프링 MVC는 다음 파라미터를 지원한다.

- InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회

- OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력

 

V2에서는 inputStream을 직접 인자로 받아서 String으로 변환하여 출력할수 있게되었다.

 

● HttpEntity - requestBodyStringV3

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

HttpEntity 를 사용하여 header, body의 정보를 편리하게 조회할수 있다.

이는 HttpMessageConverter가 HTTP 메시지 body의 내용을 변환해주었기 때문이다.

또한 응답에서도 HttpEntity 를 사용할수 있다.

 

스프링 MVC는 다음 파라미터를 지원한다.

HttpEntity: HTTP header, body 정보를 편리하게 조회하는 기능

- 메시지 바디 정보를 직접 조회

요청 파라미터를 조회하는 기능과 전혀 관계 없음 @RequestParam X, @ModelAttribute X

 

HttpEntity는 응답에도 사용 가능

- 메시지 바디 정보 직접 반환

- 헤더 정보 포함 가능

- view는 조회하지 않는다.

 

추가로 인자를 RequestEntity로 받을수도 있고, 응답의 반환값으로 ResponseEntity를 사용할수도 있다.

return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED)
 

 

● @RequestBody - requestBodyStringV4

/**
* @RequestBody
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - 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";
}
 

@RequestBody

@RequestBody 를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다.

참고로 헤더 정보가 필요하다면 HttpEntity 를 사용하거나 @RequestHeader 를 추가로 사용하면 된다.

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

 

@ResponseBody

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

 

7. HTTP 요청 메시지 - JSON

이번에는 HTTP API에서 주로 사용하는 JSON 데이터를 받아보자.

 

● RequestBodyJsonController

@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 메시지 바디에서 데이터를 stream으로 받아온 후, StreamUtils를 사용하여 문자로 변환한다.

문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper 를 사용해서 자바 객체로 변환한다.

결과는 다음과 같다.

 

● requestBodyJsonV2 - @RequestBody 문자 변환

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

@RequestBody를 통해 메시지 바디에 있던 string을 받았다.

중간에 HttpMessageConverter 사용 -> StringHttpMessageConverter 적용하여 HTTP 메시지의 바디 내용을 문자열로 변환한것 이다.

 

문자로 된 messageBody 데이터를 objectMapper를 활용하여 helloData 객체로 변환하였다.

 

문자로 변환하고 다시 json으로 변환하는 과정이 불편하다.

@ModelAttribute처럼 한번에 객체로 변환할 수는 없을까?

 

● requestBodyJsonV3 - @RequestBody 객체 변환

// @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)

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

@RequestBody 객체 파라미터 => (@RequestBody HelloData data)

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

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

HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해주는데, 우리가 방금 V2에서 했던 작업을 대신 처리해준다.

 

중요한점이 있는데 @RequestBody는 생략이 불가능하다.

스프링은 String, int, Integer 와 같은 단순 타입들은 @RequestParam이 생략된것이라 인식하고,

나머지는 @ModelAttribute 가 생략된것으로 인식한다.

 

따라서 @RequestBody를 생략하면 HelloData class 이기 때문에 @ModelAttribute로 인식하여버린다.

이는 HTTP의 바디가 아닌, 요청 파라미터를 처리하게 되는 것 이다.

 

물론 앞서 배운 것과 같이 HttpEntity를 사용해도 된다.

 

● requestBodyJsonV4 - HttpEntity

@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> helloData) {
    HelloData body = helloData.getBody();
    log.info("username={}, age={}", body.getUsername(), body.getAge());
    return "OK";
}
 

 

마지막으로 반환 또한 객체로 반환해 보자.

 

 requestBodyJsonV5

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

@ResponseBody

응답의 경우에도 @ResponseBody 를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있다.

물론 이 경우에도 HttpEntity 를 사용해도 된다.

 

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

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

 

● 정적 리소스

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

 

● 뷰 템플릿 사용

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

 

● HTTP 메시지 사용

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

 

● 정적 리소스

스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.

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

 

src/main/resources 는 리소스를 보관하는 곳 이고, class path 의 시작 경로이다. 따라서 다음 디렉토리에 리소스를 넣어두면 스프링부트가 정적 리소스로 서비스 해준다.

 

정적 리소스 경로

src/main/resources/static 의 경로에 src/main/resources/static/basic/hello-form.html 이 들어있으면, 웹 브라우저에서 다음과 같이 실행하면 된다. => http://localhost:8080/basic/hello-form.html

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

 

● 뷰 템플릿

뷰 템플릿을 거쳐서 HTML을 동적으로 생성하여 응답하게 된다.

 

스프링부트는 기본 뷰 템플릿 경로를 제공한다.

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

 

● ResponseViewController - 뷰 템플릿을 호출하는 컨트롤러

@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!");
        return "response/hello";
    }

    @RequestMapping("/response/hello")
    public void responseViewV3(Model model) {
        model.addAttribute("data", "hello!!");
    }
}
 

V1에서는 ModelAndView 객체를 만들어 데이터를 저장하고, 반환하였다. modelview 객체안에 논리적 이름까지 포함되있기 때문에 반환할경우 viewResolver를 통하여 경로를 찾을수 있다.

 

V2 에서는 String을 반환하고 있다.

논리적인 이름인 "response/hello" 를 반환하면 다음 경로의 뷰 템플릿이 렌더링 되는 것을 확인할 수 있다.

실행: templates/response/hello.html

 

@ResponseBody 가 없으면 response/hello 로 뷰 리졸버가 실행되어서 뷰를 찾고, 렌더링 한다.

@ResponseBody 가 있으면 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 response/hello 라는 문자가 입력된다.

 

V3 에서는 void를 반환하고 있다.

@Controller 를 사용하고, HttpServletResponse , OutputStream(Writer) 같은 HTTP 메시지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용하게 된다.

- 요청 URL: /response/hello

- 실행: templates/response/hello.html

참고로 이 방식은 명시성이 너무 떨어지고 이렇게 딱 맞는 경우도 많이 없어서, 권장하지 않는다.

댓글