-
Spring MVC (6) - 스프링 MVC 기본기능 (2) : Http BodyWeb/Spring 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.메시지 컨버터
- @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. 요청 매핑 핸들러 어뎁터 구조
- 메시지 컨버터는 요청 매핑 핸들러 어뎁터 안에서 작동한다.
- 핸들러 어뎁터는 기본적으로 컨트롤러의 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 메시지 컨버터
- 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)를 사용한다
'Web > Spring' 카테고리의 다른 글
Spring MVC 2 - 국제화/메시지 (0) 2023.10.24 스프링 MVC - 웹 페이지 만들기 (0) 2023.10.15 Spring MVC (6) - 스프링 MVC 기본 기능 (0) 2023.10.09 SpringMVC (5) - 스프링 MVC 구조 이해 (0) 2023.10.02 SpringMVC (4.5) - FrontController V5 [어댑터 패턴 - 중요] (0) 2023.10.02