Web/Spring

Spring MVC (6) - 스프링 MVC 기본기능 (2) : Http Body

now0204 2023. 10. 14. 23:49

 

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

 

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

 

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

> 뷰 템플릿 : 동적 페이지 제공

> HTTP 메시지 : HTTP API를 제공하는 경우, HTTP 메시지 바디에 JSON 같은 형식으로 데이터 실어 보냄

 

1.1 정적 리소스 

 

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

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

 

- src/main/resources는 리소스를 보관하는 곳이자 클래스 패스 시작 경로이다.

  따라서 다음 디렉토리에 리소스 넣어두면 스프링 부트가 정적 리소스로 제공

 

* 내장 톰캣의 경우 webapp같은 경로를 따로 제공하지 않음 

* 빌드될때 최종적으로 java와 resources같은 곳에 빌드 

 

1.2 뷰 템플릿 

 

뷰 템플릿을 통해 HTML이 생성되고, 뷰가 응답으로 만들어짐 

HTML을 동적으로 생성하는 용도로 사용 

 

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

//뷰 템플릿을 호출하는 3가지 방법 (컨트롤러에서)

public ModelAndView responseViewV1() {
 ModelAndView mav = new ModelAndView("response/hello")
 .addObject("data", "hello!");
 return mav;
 }
 // ModelAndView 이용


 public String responseViewV2(Model model) {
 model.addAttribute("data", "hello!!");
 return "response/hello";
 }
 // String을 return해서 핸들러 어답터가 처리하도록 함 
 
 @RequestMapping("/response/hello")
 public void responseViewV3(Model model) {
 model.addAttribute("data", "hello!!");
 }
 // return값이 void이면, requestMapping된 주소를 뷰 경로로 추정 (권장x)

 

- String을 반환하는 경우 

  @ResponseBody가 없으면, 리턴 값으로 뷰 리졸버가 실행되어 뷰를 찾고 랜더링

  @ResponseBody가 있으면, 뷰 리졸버 실행하지 않고, HTTP 메시지 바디에 직접 문자 입력

 

- void 반환하는 경우 

  @Controller를 사용하고, Response, OutputStream같은 HTTP 메시지 바디를 처리하는 파라미터가 없다면,

뷰이름으로 추정해서 사용

 

- HTTP 메시지 

 @ResponseBody 혹은 HttpEntity를 사용하면, 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 응답 데이터 출력 가능하다. 

 

*Thymeleaf 사용할 때 prefix와 subfix고치기

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

 

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

 

- HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 한다. 

- HTTP 메시지 바디에 JSON같은 형식으로 데이터 실어 보낸다. 

 

@Slf4j
@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 

  > 서블릿과 유사 -> response객체사용

 

- responseBodyV2

  > ResponseEntity는 HttpEntity 상속받음 (HttpEntity 메시지 헤더,바디 정보를 담고있음) 

     ResponseEntity는 HTTP응답코드 설정할 수 있음 

   

- responseBodyV3

 > @ResponseBody를 사용하면, HTTP 메시지 컨버터를 통해 HTTP 메시지를 직접 입력할 수 있다.

    (ResponseEntity도 동일한 방식으로 작동한다고 함)

 

- responseBodyJsonV1

  ResponseEntity를 반환 -> HTTP 메시지 컨버터 -> JSON 변환 

 

- responseBodyJsonV2

  @ResponseStatus 애노테이션을 사용해서 상태설정 

  단, HTTP 응답코드를 동적으로 수정하고 싶으면 ResponseEntity 사용하면됨

 

@RestController

    컨트롤러에 모두 @ResponseBody가 적용됨 -> HTTP 메시지 바디에 직접 데이터 입력 

    RestAPI만들때 사용하는 컨트롤러임  

 

*HttpEntity는 response,request처럼 http메시지 자체에 접근할 수 있음

(wrapprt임 @RequestBody에 사용한 경우 get()메서드 해서 뽑아야함)

 

 

3.메시지 컨버터 

 

 

출처: 스프링 MVC(1) - 김영한

 

- @ResponseBody를 사용

      HTTP BODY에 문자 내용을 직접 반환 

      viewResolver 대신에 HttpMessageConverter가 동작함 

      이때 기본문자StringHttpMessageConverter가 작동 

              기본 객체MappingJackson2HttpMessageConverter사 동작 

       byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음 

 

* 응답의 경우 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해서 

   HttpMessageConverter가 선택됨 

 

 3.1 스프링 MVC는 다음 경우에 HTTP 메시지 컨버터를 적용한다.

 

 HTTP 요청: @RequestBody, HttpEntity(RequestEntity)  > 컨트롤러 호출 이전에 컨버터 작동함 

 HTTP 응답: @ResponseBody, HttpEntity(ResponseEntity)

 

- 메시지 컨버터 인터페이스 

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

 

HTTP 메시지 컨버터는 요청과 응답 둘 다 사용됨

canRead(), canWrite(): 메시지 컨버터가 해당 클래스, 미디어타입 지원하는지 체크 (Accept, Content-Type 헤더)

read(), write(): 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능 

 

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

0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter 
2 = MappingJackson2HttpMessageConverter

 

- 스프링 부트는 다양한 메시지 컨버터를 제공함

- 클래스 타입과 미디어 타입을 체크해서 사용여부를 결정한다. 

   조건을 만족하지 않으면, 다음 메시지 컨버터로 우선순위 넘어감 

 

* 클래스 타입은 return 혹은 매개변수로 등록된 클래스 타입을 의미 

* 미디어 타입은 요청에 타입, 쓰기 미디어 타입은 응답 타입 

* 요청이던 응답이던 HTTP MessageBody에 데이터를 넣을꺼면 미디어 타입 무조건 지정해야함 

* content-type <=> 미디어 타입 

 

> ByteArrayHttpMessageConverter: byte[] 데이터 처리함 

   클래스타입: byte[], 미디어 타입 */*  (Content-type)

   요청 @RequestBody byte[] data

   응답 @ResponseBody return byte[] + 쓰기 미디어타입 application/octet-stream (Accpet, produces)

 

> StringHttpMessageConverter : String 문자로 데이터 처리함 

   클래스 타입 : String, 미디어 타입: */* (Content-type)

   요청 @RequestBody String data

   응답 @ResponseBody return "string" + 쓰기 미디어타입 text/plain (Accpet, produces)

 

> MappingJackson2HttpMessageConverter : json관련

   클래스 타입 : 객체 혹은 HashMap 미디어 타입 : application/json 관련  (Content-type)

   요청 @RequestBody HelloData data

   응답 @ResponseBody return helloData + 쓰기 미디어 타입 application/json (Accpet, produces)

 

 

 

3.2 HTTP 요청 데이터 읽기 

 

- HTTP 요청이 오고, 컨트롤러에서 @RequestBody or HttpEntity 사용

 

- 메시지 컨버터가 메시지 읽을 수 있는지 확인하기 위해 canRead()호출 

    > 대상 클래스 타입 지원하는가 (해당 클래스로 컨버팅 가능 한가?

    > HTTP 요청의 Content-Type 미디어 타입을 지원하는가 (해당 미디어 타입의  메시지 자체 읽을 수 있는가?)

 

- canRead() 조건을 만족하면 read()를 호출해서 객체를 생성하고 반환함  

 

3.3 HTTP 응답 데이터 읽기 

 

- 컨트롤러에서 @ResponseBody, HttpEntity로 값이 반환된다

- 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 호출한다.

   > 대상 클래스 타입을 지원하는가? (해당 클래스를 컨버팅 가능 한가?

   > HTTP 요청의 Accept 미디어 타입을 지원하는가 (해당 미디어 타입으로 메시지를 작성 할 수  있는가?)

       (정확히는 @RequestMapping의 produces -> produces지정지 확인, 아니면 Accept확인

- canWriter() 조건을 만족하면, write()를 호출해서 HTTP 응답 메시지 데이터를 생성한다.

 

4. 요청 매핑 핸들러 어뎁터 구조 

     

출처: 스프링 MVC (인프런) 김영한

 - 메시지 컨버터는 요청 매핑 핸들러 어뎁터 안에서 작동한다. 

 - 핸들러 어뎁터는 기본적으로 컨트롤러의 argument를 확인해서 필요한 매개변수 ArgumentResolver 호출해서 만들고

    모든 매개변수가 준비되면, 그때 컨트롤러를 호출한다.

 

4.1 ArgumentResolver 

 

다양한 파라미터를 매개변수로 받을 수 있는 이유는 위 덕분임 

ArgumentResolver 호출해서 컨트롤러가 필요로하는 다양한 파라미터 값(객체)생성

기본적으로 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를 만들 수도 있다.

 

4.2 ReturnValueHandler 

 

- 컨트롤러에서 String으로 뷰이름 반환해도 동작하는 이유는 ReturnValueHandler덕분임

- 스프링은 10개가 넘는 ReturnValueHandler를 지원함 

- 컨트롤러의 반환 값을 변환함 

 

4.3 HTTP 메시지 컨버터 

 

출처: 스프링 MVC (인프런) 김영한

 - HTTP 메시지 컨버터를 사용하는 @RequestBody도

   컨트롤러가 필요로 하는 파라미터의 값에 사용된다.

 - @ResponseBody의 경우도 컨트롤러 반환 값을 이용한다. 

 

요청의 경우 : @RequestBody를 처리하는 ArgumentResolver사 있고,

                      HttpEntity를 처리하는 resolvert가 있다. 

                      이 Resolver들이 HTTP 메시지 컨버터를 사용해서 필요한 순간에 객체를 생성하는 것임 

 

응답의 경우 : @ResponseBody와 HttpEntity를 처리하는 ReturnValueHandler가 있고 여기서 HTTP 메시지 컨버터를 호출함 

 

 

확장 : 스프링은 다음 인터페이스를 모두 제공함 > 따라서 필요하면 확장가능 

HandlerMethodArgumentResolver
HandlerMethodReturnValueHandler
HttpMessageConverter

//실제로 확장할 일은 많지 않음 WebMvcConfigurer 검색해보자 (확장하고 싶으면)

 

정리하자면, 요청 파라미터는 ArgumentResolver선에서 해결 

                    메시지 바디를 읽어서 객체를 생성해야할 때, ArgumentResolver가 컨버터를 사용해서 바디의 내용을 읽고

                    객체 변환해서 반환함 -> 핸들러 어답터는 이 값을 받아서 핸들러에 전달 하며 호출 

                    응답도 비슷한 과정 HTTP 메시지를 Body에 직접 입력할 필요가 있을 때 (정적 or 뷰템플릿 없이)

                     메시지 컨버터에 도움을 받음

 

메시지 컨버터의 역할은 HTTP 요청 메시지 본문과 HTTP 응답 메시지 본문을 통째로 메시지로 다루어준다.

 

*스프링 MVC는 @RequestBody @ResponseBody 가 있으면

RequestResponseBodyMethodProcessor (ArgumentResolver)

HttpEntity 가 있으면 HttpEntityMethodProcessor (ArgumentResolver)를 사용한다