HTTP Response
- 응답 데이터
- 정적 리소스
- 예) 브라우저에 정적인 HTML, CSS, JS 를 제공할 때는, 정적 리소스를 사용한다.
- 뷰 템플릿
- 예) 웹 브라우저에 동적인 HTML을 제공할 때는 뷰 템플릿을 사용한다.
- HTTP 메시지 사용
- HTTP API를 제공하는 경우에는 HTML이 아닌 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.
- 정적 리소스
HTTP Response - 정적 리소스
- 정적 리소스
- 스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.
/static, /public, /resources, /META-INF/resources
- src/main/resources는 리소스를 보관하는 곳이고, 또 클래스패스의 시작 경로이다.
- 다음 디렉토리에 리소스를 넣으면 스프링 부트가 정적 리소스로 서비스를 제공한다.
- 정적 리소스 경로
src/main/resources/static
- 만약 src/main/resources/static/basic/hello-form.html에 파일이 있으면 http://localhost:8080/biasic/hello-form.html 로 접근할 수 있다.
- 정적 리소스는 해당 파일 변경 없이 그대로 서비스한다.
- 스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공한다.
HTTP Response - 뷰 템플릿
- 뷰 템플릿
- 뷰 템플릿을 거쳐서 HTML 이 생성되고, 뷰가 응답을 만들어서 전달한다.
- 일반적으로 HTML을 동적으로 생성하는 용도로 사용하지만, 다른 것도 가능하다.
- 뷰 템플릿 경로
src/main/resources/template
- 예제
- 뷰 템플릿 생성
- src/main/resources/template/response/hello.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <p th:text="${data}">empty</p> </body> </html>
- src/main/resources/template/response/hello.html
- ResponseViewController - 뷰를 호출하는 컨트롤러
@Controller public class ResponseViewController { @RequestMapping("/response-view-v1") public ModelAndView responseViewV1() { ModelAndView mav = new ModelAndView("response/hello.html") .addObject("data", "hello!"); return mav; } @RequestMapping("/response-view-v2") public String responseViewV2(Model model) { model.addAttribute("data", "hello!"); return "response/hello"; } @RequestMapping("/response-view-v3") public void responseViewV3(Model model) { model.addAttribute("data", "hello!"); } }
- V1: ModelAndView에 Viewname을 세팅하여 return한다.
- V2: String을 View로 반환
- @ResponseBody가 없으면 HTTP 메시지가 아닌 View Resolver가 실행되어 View를 찾고 렌더링 한다.
- @ResponseBody가 있으면 View Resolver를 실행하지 않고, HTTP 메시지 바디에 직접 문자열이 입력된다.
- V3: void를 반환하는 경우
- @Controller 를 사용하고, HttpServletResponse , OutputStream(Writer) 같은 HTTP 메시지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용
- 요청 URL: /response/hello
- 실행: templates/response/hello.html
- 참고로 이 방식은 명시성이 너무 떨어지고 이렇게 딱 맞는 경우도 많이 없어서, 권장하지 않는다.
- @Controller 를 사용하고, HttpServletResponse , OutputStream(Writer) 같은 HTTP 메시지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용
- 뷰 템플릿 생성
- 참고: Tymeleaf 설정
- 의존성
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
- 스프링 부트가 자동으로 ThymeleafViewResolver와 필요한 스프링 빈들을 등록한다.
- 설정: application.properties
spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html
- prefix, suffix 설정
- 의존성
HTTP Response - HTTP API, 메시지 바디에 직접 입력
- HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로 HTTP 메시지 바디에 JSON같은 형식으로 데이터를 실어 보낸다.
- 참고
- HTML이나 뷰 템플릿을 사용해도 HTTP 응답 메시지 바디에 HTML 데이터가 담겨서 전달된다.
- 정적 리소스나 뷰 템플릿을 거치지 않고, 직접 HTTP 응답 메시지를 전달하는 경우를 말한다.
@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 HttpEntity<String> responseBodyV2() {
return new HttpEntity<String>("ok");
}
@GetMapping("/response-body-string-v3")
@ResponseBody
public String responseBodyV3() {
return "ok";
}
@GetMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1() {
HelloData helloData = new HelloData();
helloData.setUsername("test");
helloData.setAge(10);
return new ResponseEntity<HelloData>(helloData, HttpStatus.OK);
}
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("test");
helloData.setAge(10);
return helloData;
}
}
- String
- V1: 서블릿을 다룰 때 처럼 HttpServletResponse를 직접 사용할 수 있다.
- V2: HttpEntity와 상속받은 ResponseEntity를 활용하여 사용할 수 있다.
- ResponseEntity를 하면, HttpStatus를 사용하여 응답코드를 지정할 수 있다.
- V3: @ResponseBody를 사용하면 view를 생성하지 않고 string을 리턴한다.
- JSON
- V1: HttpEntity와 상속받은 ResponseEntity를 반환할 수 있다.
- Message Converter를 사용하여 JSON 형식으로 변환되어사 반환된다.
- V2: 객체 자체를 반환할 수 있다. 필요하다면 @ResponseStatus 애노테이션을 활용하여 응답 코드를 지정할 수 있다.
- 동적으로 응답 코드를 생성하기 위해서는 ResponseEntity를 사용하는 것이 좋다.
- V1: HttpEntity와 상속받은 ResponseEntity를 반환할 수 있다.
- @ RestController
- @Controller 대신에 @RestController 애노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody 가 적용되는 효과가 있다.
- 따라서 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력한다.
- 이름 그대로 Rest API(HTTP API)를 만들 때 사용하는 컨트롤러이다.
- 참고로 @ResponseBody 는 클래스 레벨에 두면 전체에 메서드에 적용되는데, @RestController 에노테이션 안에 @ResponseBody 가 적용되어 있다.
HTTP Message Converter
- Spring MVC는 다음의 경우 HTTP Message Converter를 적용한다.
- HTTP 요청: @RequestBody, HttpEntity(RequestEntity)
- HTTP 응답: @ResponseBody, HttpEntity(ResponseEntity)
- HTTP Message Converter Interface
- org.springframework.http.converter.HttpMessageConverter
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 MessageConverter는 HTTP 요청, 응답 둘 다 사용된다.
- canRead(), canWrite(): 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는 지 체크
- read(), write(): 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능
- Spring Boot 기본 메시지 컨버터
- (일부 생략)
0 = ByteArrayHttpMessageConverter 1 = StringHttpMessageConverter 2 = MappingJackson2HttpMessageConverter
- 스프링 부트는 다양한 메시지 컨버터를 제공하는 데, 대상 클래스 타입과 미디어 타입 둘을 체크해서 사용여부를 결정한다.
- MediaType -> 요청(HTTP request header: Content-Type, @RequestMapping parameter: consume)
- MediaType -> 응답(HTTP request header: Accept, @RequestMapping parameter: produce)
- 주요 메시지 컨버터
- ByteArrayHttpMessageConverter: byte[] 데이터를 처리한다.
- 클래스 타입: byte[], 미디어 타입: /
- 요청 예: @RequestBody byte[] data
- 응답 예: @ResponseBody return byte[], 쓰기 미디어 타입: application/octet-stream
- StringHttpMessageConverter: String 문자로 데이터를 처리한다.
- 클래스 타입: String , 미디어타입: /
- 요청 예) @RequestBody String data
- 응답 예) @ResponseBody return “ok” 쓰기 미디어타입 text/plain
- MappingJackson2HttpMessageConverter: application/json
- 클래스 타입: 객체 또는 HashMap , 미디어타입 application/json 관련
- 요청 예) @RequestBody HelloData data
- 응답 예) @ResponseBody return helloData 쓰기 미디어타입 application/json 관련
- ByteArrayHttpMessageConverter: byte[] 데이터를 처리한다.
- 스프링 부트는 다양한 메시지 컨버터를 제공하는 데, 대상 클래스 타입과 미디어 타입 둘을 체크해서 사용여부를 결정한다.
- (일부 생략)
- HTTP 요청 데이터 읽기
- HTTP 요청이 오고, 컨트롤러에서 @RequestBody , HttpEntity 파라미터를 사용한다.
- 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead() 를 호출한다.
- 대상 클래스 타입을 지원하는가.
- 예) @RequestBody 의 대상 클래스 ( byte[] , String , HelloData )
- HTTP 요청의 Content-Type 미디어 타입을 지원하는가.
- 예) text/plain , application/json , /
- 대상 클래스 타입을 지원하는가.
- canRead() 조건을 만족하면 read() 를 호출해서 객체 생성하고, 반환한다.
- HTTP 응답 데이터 생성
- 컨트롤러에서 @ResponseBody , HttpEntity 로 값이 반환된다.
- 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 를 호출한다.
- 대상 클래스 타입을 지원하는가.
- 예) return의 대상 클래스 ( byte[] , String , HelloData )
- HTTP 요청의 Accept 미디어 타입을 지원하는가.(더 정확히는 @RequestMapping 의 produces)
- 예) text/plain , application/json , /
- 대상 클래스 타입을 지원하는가.
- canWrite() 조건을 만족하면 write() 를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성한다.
HttpConverter에 위치, feat ArgumentResolver, ReturnValueHandler
- RequestMappingHandlerAdapter 동작 방식
- DispatcherServlet이 RequestMappingHandlerAdapter 호출
- RequestMappingHandlerAdapter가 Handerl 호출
- Handler 파라미터에 맞는 Argument를 생성해야 한다.
- ArgumentResolver가 지원
- HttpServletRequest, Model, @RequestParam, @ModelAttribute, @RequestBody, HttpEntity…
- Handler가 RequestMappingHandlerAdapter에게 결과 return
- Handler가 Return할 때 ReturnValueHandler가 반환 값을 변환한다.
- ModelAndView, @ResponseBody, HttpEntity …
- ArgumentResolver
- 애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다.
- HttpServletRequest , Model, @RequestParam , @ModelAttribute, @RequestBody , HttpEntity 등
- 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.
- 애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdaptor 는 바로 이 ArgumentResolver 를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다.
- 파리미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다.
- 스프링은 30개가 넘는 ArgumentResolver 를 기본으로 제공한다.
public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }
- 동작 방식
- ArgumentResolver 의 supportsParameter() 를 호출해서 해당 파라미터를 지원하는지 체크
- 지원하면 resolveArgument() 를 호출해서 실제 객체를 생성한다.
- 이렇게 생성된 객체가 컨트롤러 호출시 넘어가는 것이다.
- 여러분이 직접 이 인터페이스를 확장해서 원하는 ArgumentResolver 를 만들 수도 있다.
- 애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다.
- ReturnValueHandler
- HandlerMethodReturnValueHandler 를 줄여서 ReturnValueHandle 라 부른다.
- ArgumentResolver 와 비슷한데, 이것은 응답 값을 변환하고 처리한다.
- 컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 바로 ReturnValueHandler 덕분이다.
- 스프링은 10여개가 넘는 ReturnValueHandler 를 지원한다.
- 예) ModelAndView , @ResponseBody , HttpEntity , String
- HTTP Message Converter 위치
- ArgumentResolver에서 @RequestBody, HttpEntity등을 만나면 HTTP Message Converter를 호출한다.
- ReturnValueHandler에서 @ResponseBody, HttpEntity등을 만나면 HTTP Message Converter를 호출한다.
- 요청
- @RequestBody 를 처리하는 ArgumentResolver 가 있고, HttpEntity 를 처리하는 ArgumentResolver 가 있다.
- 이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.
- 응답
- @ResponseBody 와 HttpEntity 를 처리하는 ReturnValueHandler 가 있다.
- 여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.
- 확장
- 스프링은 다음을 모두 인터페이스로 제공한다. 따라서 필요하면 언제든지 기능을 확장할 수 있다.
- HandlerMethodArgumentResolver
- HandlerMethodReturnValueHandler
- HttpMessageConverter
- 스프링이 필요한 대부분의 기능을 제공하기 때문에 실제 기능을 확장할 일이 많지는 않다.
- 기능 확장은 WebMvcConfigurer 를 상속 받아서 스프링 빈으로 등록하면 된다.
@Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { //... } @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { //... } }; }
- 스프링은 다음을 모두 인터페이스로 제공한다. 따라서 필요하면 언제든지 기능을 확장할 수 있다.
** 출처: 스프링 MVC - 백엔드 웹 개발 핵심 기술