1. HTTP 응답
스프링(서버)에서 응답 데이터를 만드는 방법은 크게 3가지로 나눌 수 있다.
- 정적 리소스
- ex. 웹 브라우저에 정적인 HTML, CSS, JS를 제공할 때 사용한다.
- 뷰 템플릿 사용
- ex. 웹 브라우저에 동적인 HTML을 제공할 때 사용한다.
- HTTP 메시지 사용
- HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.
아래에서 하나씩 알아보자.
정적 리소스
스프링 부트는 classpath의 다음 디렉토리에 있는 정적 리소스를 제공한다.
- /static, /public, /resources, /META-INF/resources
src/main/resources는 리소스를 보관하는 곳이면서 classpath의 시작 경로다. 따라서 다음 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공한다.
- 정적 리소스 경로: src/main/resources/static
- 다음 경로에 파일이 들어있으면, 웹 브라우저에서 다음과 같이 실행하면 된다.
# 파일 경로
src/main/resources/static/basic/hello-form.html
# 웹 브라우저 실행
http://localhost:8080/basic/hello-form.html
정적 리소스는 해당 파일을 변경 없이 그대로 서비스한다.
뷰 템플릿
뷰 템플릿을 거쳐 HTML이 생성되고, View가 응답을 만들어서 전달한다. 일반적으로 HTML을 동적으로 생성하는 용도로 사용하지만, 뷰 템플릿이 만들 수 있는 것이라면 뭐든지 가능하다.
스프링 부트는 기본 뷰 템플릿 경로를 제공한다.
- 뷰 템플릿 경로: src/main/resources/templates
- 다음 경로에 파일을 넣어놓고, 해당 뷰 템플릿을 호출할 컨트롤러를 만들어야 한다.
a. 뷰 템플릿 호출용 Controller
ModelAndView 객체에 viewName을 넣어 생성하고, View에 출력할 값을 바인딩한다. 아래 URL이 실행되면 /resources/static/ 경로 아래에 있는 response/hello.html을 렌더링 한다.
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1() {
ModelAndView mav = new ModelAndView("response/hello")
.addObject("data", "hello!");
return mav;
}
@Controller를 사용하면서 String을 반환하는 경우
- @Responsebody가 없으면, response/hello로 뷰 리졸버가 실행돼 View를 찾고 렌더링 한다.
- @ResponseBody가 있으면, 뷰 리졸버를 실행하지 않고, HTTP message body에 직접 response/hello라는 문자가 입력된다.
- 아래 코드에선 View의 논리 이름인 response/hello를 반환하면 다음 경로의 뷰 템플릿이 렌더링 되는 것을 확인할 수 있다.
- 실행: templates/response/hello.html
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model) {
model.addAttribute("data", "hello!!");
return "response/hello";
}
@Controller를 사용하면서 Void를 반환하는 경우
- HttpServletResponse, OutputStream(Writer) 같은 HTTP message body를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 View 이름으로 사용한다.
- 요청 URL: /response/hello
- 실행: templates/response/hello.html
- 이 방식은 명시성이 너무 떨어지고 이렇게 딱 맞는 경우가 많이 없어서 권장하지 않는다.
@RequestMapping("/response/hello")
public void responseViewV3(Model model) {
model.addAttribute("data", "hello!!");
}
참고
@Responsebody, HttpEntity를 사용하면, 뷰 템플릿을 사용하는 게 아니라 HTTP message body에 직접 응답 데이터를 출력할 수 있다.
b. Thymeleaf 스프링 부트 설정
다음 라이브러리를 build.gradle에 추가하면 스프링 부트가 자동으로 ThymeleafViewResolver와 필요한 스프링 빈들을 등록한다.
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
그리고 다음 설정도 사용한다. 이 설정은 기본값이기 때문에 필요할 때만 다르게 설정하면 된다.
application.properties
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
참고
스프링 부트의 Thymeleaf 관련 추가 설정은 다음 공식 사이트를 참고하자.
HTTP API, 메시지 바디에 직접 입력
HTTP 요청에서 응답까지 대부분 다뤘으므로 여기선 간단하게 정리해 본다.
- HTML이나 뷰 템플릿을 사용해도 HTTP 응답 message body에 HTML 데이터가 담겨서 전달된다. 여기서 설명하는 내용은 정적 리소스나 뷰 템플릿을 거치지 않고, 직접 HTTP 응답 message를 전달하는 경우를 말한다.
a. 단순 텍스트
HttpServletResponse 객체를 통해 HTTP message body에 직접 "ok" 응답 message를 전달한다.
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok");
}
HttpEntity를 상속받은 ResponseEntity로 HTTP 응답 코드도 함께 전달할 수 있다.
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
return new ResponseEntity<>("ok", HttpStatus.OK);
}
@ResponseBody를 사용하면 View를 사용하지 않고, HTTP message Converter를 통해 HTTP message를 직접 입력할 수 있다. ResponseEntity도 동일하게 동작한다.
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
b. JSON
ResponseEntity를 반환한다. HTTP message Converter를 통해 JSON 형식으로 변환되어 반환된다.
@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);
}
ResponseEntity는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody를 사용하면 응답 코드를 설정하기가 까다롭다. 이때, @ResponseStatus(HttpStatus.OK) 애노테이션을 적용하면 응답 코드도 설정할 수 있다.
- 물론 애노테이션이기 때문에 응답 코드를 동적으로 변경할 수 없다. 프로그램 조건에 따라서 동적으로 변경하려면 ResponseEntity를 사용하면 된다.
- @RestController를 적용(@ResponseBody 생략)한 이 방식을 사용하는 경우가 많다.
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
c. @RestController
@Controller 대신에 @RestController 애노테이션을 사용하면, 해당 Controller에 모두 @ResponseBody가 적용된다. 따라서 뷰 템플릿을 사용하는 게 아니라, HTTP message body에 직접 데이터를 입력한다. 이름 그대로 Rest API(HTTP API)를 만들 때 사용하는 Controller다.
- class 레벨에 두면 전체 메서드에 적용되는데, @RestController 애노테이션 안에 @ResponseBody가 적용돼 있다.
2. HTTP 메시지 컨버터
뷰 템플릿으로 HTML을 생성해서 응답하지 않고, HTTP API처럼 JSON 데이터를 HTTP message body에서 직접 읽거나 쓰는 경우, HTTP message Converter를 사용하면 편리하다.
@ResponseBody 사용 원리
HTTP message Converter를 사용하기 전, 스프링 입문 강의에서 설명한 내용을 살펴보자.
@ResponseBody 사용 시 동작 원리를 알아본다.
- HTTP message body에 문자 내용을 직접 반환한다.
- viewResolver 대신 HttpMessageConverter가 동작한다.
- 기본 문자 처리: StringHttpMessageConverter
- 기본 객체 처리: MappingJackson2HttpMessageConverter
- 이외에도 byte 처리 등 기타 여러 HttpMessageConverter가 기본으로 등록돼 있다.
- 응답의 경우 클라이언트의 HTTP Accept 헤더와 서버의 Controller 반환 타입 정보를 조합해서 적절한 HttpMessageConverter가 선택된다.
HTTP message Converter Interface
스프링 MVC는 다음의 경우에 HTTP message Converter를 적용한다.
- HTTP 요청: @RequestBody, HttpEntity(RequestEntity)
- HTTP 응답: @ResponseBody, HttpEntity(ResponseEntitiy)
HTTP message Converter는 HTTP 요청, HTTP 응답이 둘 다 사용된다.
- canRead(), canWrite(): message Converter가 해당 class, 미디어 타입을 지원하는지 확인하는 메서드
- read(), write(): message Converter를 통해 message를 읽고 쓰는 기능
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;
}
a. HTTP 요청 데이터 읽기
- HTTP 요청이 오고, Controller에서 @RequestBody, HttpEntity 파라미터를 사용한다.
- message Converter가 message를 읽을 수 있는지 확인하기 위해 canRead()를 호출한다.
- 대상 class 타입을 지원하는지 확인한다.
- ex. @RequestBody의 대상 class (byte[], String, HelloData)
- HTTP 요청의 Content-Type 미디어 타입을 지원하는지 확인한다.
- ex. text/plain, application/json, */*
- 대상 class 타입을 지원하는지 확인한다.
- 위의 canRead() 조건을 만족하면, read()를 호출해 객체를 생성한 뒤 반환한다.
b. HTTP 응답 데이터 생성
- Controller에서 @ResponseBody, HttpEntity로 값이 반환된다.
- message Converter가 message를 쓸 수 있는지 확인하기 위해 canWrite()를 호출한다.
- 대상 class 타입을 지원하는지 확인한다.
- ex. return의 대상 class (byte[]. String, HelloData)
- HTTP 요청의 Accept 미디어 타입을 지원하는지 확인한다. 더 정확히는 @RequestMapping의 produces를 확인한다.
- ex. text/plain, application/json, */*
- 대상 class 타입을 지원하는지 확인한다.
- 위의 canWrite() 조건을 만족하면, write()를 호출해 HTTP 응답 message body에 데이터를 생성한다.
스프링 부트 기본 메시지 컨버터
스프링 부트는 다양한 message Converter를 제공하는데, 대상 class 타입과 미디어 타입을 확인해서 사용 여부를 결정한다. 만약 만족하지 않으면 다음 message Converter로 우선순위가 넘어간다.
...
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
...
a. ByteArrayHttpMessageConverter
byte[] 데이터를 처리한다.
- class 타입: byte[], 미디어 타입: */*
- 요청 예시: @RequestBody byte[] data
- 응답 예시: @ResponseBody return byte[] (쓰기 미디어 타입: application/octet-stream
b. StringHttpMessageConverter
String 문자로 데이터를 처리한다.
- class 타입: String, 미디어 타입: */*
- 요청 예시: @RequestBody String data
- 응답 예시: @ResponseBody return "ok" (쓰기 미디어 타입: text/plain)
// content-type: application/json
@RequestMapping
void hello(@RequestBody String data) {}
c. MappingJackson2HttpMessageConverter
application/json 타입으로 데이터를 처리한다.
- class 타입: 객체 또는 HashMap, 미디어 타입: application/json 관련
- 요청 예시: @RequestBody HelloData data
- 응답 예시: @ResponseBody return data (쓰기 미디어 타입: application/json 관련)
// content-type: application/json
@RequestMapping
void hello(@RequestBody HelloData data) {}
3. 요청 매핑 핸들러 어댑터 구조
HTTP message Converter는 스프링 MVC 어디쯤에서 사용되는 걸까? 저번 섹션에서 본 아래 그림에서는 보이지 않는다.
RequestMappingHandlerAdapter
@RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter를 살펴보면 찾을 수 있다.
- RequestMappingHandlerAdapter가 호출된 상태에서 핸들러를 호출하기 전 해당 핸들러의 파라미터에 맞는 객체를 던져줘야 한다.
a. ArgumentResolver
생각해 보면 애노테이션 기반의 Controller는 매우 다양한 파라미터를 사용할 수 있었다. HttpServletRequest와 Model은 물론이고, @RequestParam, @ModelAttribute 같은 애노테이션, 그리고 @RequestBody, HttpEntity 같은 HTTP message를 처리하는 부분까지 매우 큰 유연함을 보여줬다.
참고
가능한 파라미터 목록은 다음 공식 메뉴얼에서 확인할 수 있다.
이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다. 애노테이션 기반의 Controller를 처리하는 RequestMappingHandlerAdapter는 바로 이 ArgumentResolver를 호출해서 Controller(Handler)가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다. 그리고 이렇게 파라미터의 값이 모두 준비되면 Controller(Handler)를 호출하면서 값을 넘겨준다.
스프링은 무려 30개가 넘는 ArgumentResolver를 기본으로 제공한다.
- 정확히는 HandlerMethodArgumentResolver인데, 줄여서 ArgumentResolver라고 부른다.
- 원한다면 이 Interface를 직접 확장해 원하는 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()를 호출해서 실제 객체를 생성한 뒤, Controller 호출 시 해당 객체를 넘긴다.
b. ReturnValueHandler
HandlerMethodReturnValueHandler를 줄여서 ReturnValueHandler라고 부른다. ArgumentResolver와 비슷한데, 이것은 응답 값을 변환하고 처리한다.
참고
가능한 응답 값 목록은 다음 공식 메뉴얼에서 확인할 수 있다.
Controller에서 String으로 View 이름을 반환해도 동작하는 이유가 바로 ReturnValueHandler 덕분이다. 스프링은 10여 개가 넘는 ReturnValueHandler를 지원한다.
- ex. ModelAndView, @ResponseBody, HttpEntity, String
HTTP message Converter
HTTP message Converter를 사용하는 @RequestBody도 Controller가 필요로 하는 파라미터의 값에 사용된다. @ResponseBody의 경우도 Controller의 반환 값을 이용한다.
a. 요청의 경우
@RequestBody를 처리하는 ArgumentResolver가 있고, HttpEntity를 처리하는 ArgumentResolver가 있다. 이 ArgumentResolver들이 HTTP message Converter를 사용해서 필요한 객체를 생성하는 것이다.
b. 응답의 경우
@ResponseBody와 HttpEntity를 처리하는 ReturnValueHandler가 있다. 그리고 여기에서 HTTP message Converter를 호출해서 응답 결과를 만든다.
스프링 MVC는 @RequestBody, @ResponseBody가 있으면 RequestResponseBodyMethodProcessor (ArgumentResolver, ReturnValueHandler 둘 다 구현)를, HttpEntity가 있으면 HttpEntitiyMethodProcessor (ArgumentResolver, ReturnValueHandler 둘 다 구현)를 사용한다.
확장
스프링은 다음을 모두 Interface로 제공한다. 따라서 필요하다면 언제든지 기능을 확장할 수 있다.
- 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) {
//...
}
};
}