목차
1. 응답 - 정적 리소스, 뷰 템플릿
2. 응답 - HTTP API, 바디에 직접 입력
3. HTTP 메시지 컨버터
4. 요청 매핑 핸들러 어댑터 구조
응답 - 정적 리소스, 뷰 템플릿
스프링에서 응답 데이터를 만드는 방법은 총 3가지이다.
1) 정적 리소스
스프링 부트에서는 /static, /publc, /resources, /META-INF/resources 정보를 제공한다.
src/main/resources는 리소스를 보관하는 곳이면서, 클래스 패스의 시작경로인 것이다.
따라서 위의 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공하게 된다.
정적 리소스 경로
src/main/resources/static
만약 아래와 같은 경로에 파일이 있다고 하자
src/main/resources/static/basic/hello-form.html
이런 경우 웹 브라우저에서 아래와 같이 실행하면 된다.
http://localhost:8080/basic/hello-form.html
정적 리소스는 해당 파일을 변경 없이 그대로 서비스 하는 것이다.
2) 뷰 템플릿 사용
뷰 템플릿을 거쳐 HTML이 생성되고, 뷰가 응답을 만들어 전달하게 된다. 스프링 부트는 기본 뷰 템플릿 경로를 제공해준다.
뷰 템플릿 경로
src/main/resources/templates
뷰 템플릿 생성
src/main/resources/templates/response/hello.html
정적 리소스와 달리 뷰 템플릿은 동적으로 작동하므로, 데이터를 담고 있거나 한다. 따라서 url로 먼저 입력을 받아 매핑된 컨트롤러에서 model에 데이터를 담는다. 이후 데이터와 함께 뷰 템플릿으로 dispatch하게 된다.
url에 response-view가 입력되면, Model에 데이터를 ("data", "helloWorld!")로 매핑하고 return "response/hello"를 통해 뷰 템플릿으로 이동하게 된다. 뷰에서는 매핑되어있는 hello!를 키인 data로 찾아 empty에 hello!를 덮어쓰고 응답을 하게 되는 것이다.
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
가 기본으로 설정되어 있기때문에, 논리 이름으로도 뷰 템플릿으로 dispatch할 수 있는 것이다.
동작 방식
1. 뷰템플릿 담당 컨트롤러
@Slf4j
@Controller
public class ResponseController {
@GetMapping("/response-view")
public String responseViewV1(Model model) {
model.addAttribute("data", "helloWorld!");
return "response/hello";
}
}
2. 뷰 템플릿 (Thymeleaf 사용)
<!DOCTYPE html>
<html xmlns:th="http://www.thymefleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="${data}">we put data here!</p>
</body>
</html>
응답 - HTTP API, 바디에 직접 입력
HTTP API로 바디에 직접 입력하는 것이다.
@ResponseBody, HttpEntity를 사용하여 메시지 바디에 직접 응답 데이터를 출력한다. 이는 뷰 템플릿을 사용하지 않는다.
String 응답
@GetMapping("/response-body-string")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok");
}
@GetMapping("/response-body-string2")
public ResponseEntity<String> responseBodyV2() {
return new ResponseEntity<>("ok", HttpStatus.OK);
}
@ResponseBody // 이 방식을 사용하면 된다.
@GetMapping("/response-body-string3")
public String responseBodyV3() {
return "ok";
}
string으로 응답하기 위해 위의 3가지 방식을 사용할 수 있다.
Json 응답
@GetMapping("/response-body-json")
public ResponseEntity<Member> responseJsonV1() {
Member member = new Member();
member.setId(1L);
member.setName("memberA");
member.setAge(20);
return new ResponseEntity<>(member, HttpStatus.OK);
}
@ResponseBody
@GetMapping("/response-body-json2")
public Member responseJsonV2() {
Member member = new Member();
member.setId(1L);
member.setName("memberA");
member.setAge(20);
return member;
}
Json으로 응답하기 위해 위의 2가지 방식을 사용할 수 있다.
HTTP 메시지 컨버터
위의 예제들에서 보듯, 뷰 템플릿으로 HTML을 생성해 응답하는 것이 아니라, HTTP API처럼 데이터를 HTTP 메시지 바디에 직접 읽고 쓰는 경우에는 HTTP 메시지 컨버터를 사용하는 것이 편리하다.
1. 언제
HTTP 요청과 응답이 @RequestBody, HttpEntity인 경우에 사용된다.
2. 동작 방식
HTTP 메시지 컨버터는 canRead(), canWrite()로 요청과 응답으로 오는 정보를 지원하는지 체크하게 된다.
3. 스프링 부트 기본 메시지 컨버터
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
1) ByteArrayHttpMessageConverter
byte[] 데이터를 처리한다.
클래스 타입: byte[], 미디어 타입: */* 를 지원한다.
요청 예시: @RequestBody byte[] data
응답 예시: @ResponseBody return byte[] 쓰기 미디어 타입: application/octet-stream
2) StringHttpMessageConverter
String 문자로 데이터를 처리한다.
클래스 타입: String, 미디어 타입: */*
요청 예시: @RequestBody String data
응답 예시: @ResponseBody return "ok" 쓰기 미디어 타입:text/plain
3) MappingJackson2HttpMessageConverter
Json으로 데이터를 처리한다.
클래스 타입: 객체 또는 HashMap, 미디어 타입: application/json 관련
요청 예시: @RequestBody HelloData data
응답 예시: @ResponseBody return helloData 쓰기 미디어 타입: application/json 관련
예시
StringHttpMessageConverter
동작과정
1) HTTP 요청 데이터 읽기
1.ByteConverter부터 순차적으로 돈다. -> canRead()? X
2. StringConverter를 돈다. canRead()? -> 클래스 타입이 String 이고, content-type이 application/json은 미디어 타입 */* 에 포함되므로 O
3. read() 호출 후 객체를 생성해서 반환한다.
2) HTTP 응답 데이터 생성
1. 컨트롤러에서 @ResponseBody 어노테이션을 체크하거나 return 타입이 HttpEntity인지 체크한다.
2. 메시지 컨버터가 응답 데이터 String 형식으로 메시지를 쓸 수 있는지 canWrite()를 호출해서 체크한다.
3. canWrite()를 만족하면 write()를 호출해 응답메시지 바디에 데이터를 생성한다.
요청 매핑 핸들러 어댑터 구조
어노테이션 기반 컨트롤러는 어떤 원리에 의해 동작하는 것인지 알아보자.
@ResponseBody
@GetMapping("/response-get-json-body2")
public String responseGetJsonV1(@RequestBody Member member) {
log.info("id={}, name={}, age={}", member.getId(),member.getName(), member.getAge());
return "ok";
}
기존에 공부했던 mvc 패턴을 다시 보며 과정을 이해해보자.
1. RequestMapping 어노테이션 기반의 핸들러(컨트롤러)이므로, RequestMappingHandler를 조회하게 된다.
2. 이후 핸들러 어댑터 목록에서 RequestMappingHandlerAdapter를 받아오게 된다.
이 두 과정은 기존의 이해와 같다. 하지만 어노테이션 기반의 핸들러 동작을 이해하기 위해 RequestMappingHandlerAdapter을 구체적으로 살펴볼 필요가 있다.
2.1 어댑터는 ArgumentResolver를 호출하게 된다. 스프링에서는 30개가 넘는 ArgumentResolver를 지원하는데 이중 @RequestBody Member를 보고 객체 Argument를 지원하는 ArgumentResolver를 호출하게 된다. 호출 과정은 아래 사진을 참고하자.
2.2 만약 @RequestBody 어노테이션이 있거나 인자가 HttpEntity인 경우에는 ArgumentResolver가 HTTP 메시지 컨버터를 호출 후 객체를 생성한다. (여기선 MappingJackson2HttpMessageConverter)
이를 반환하면 어댑터가 받고 가지고 있다가 핸들러 호출시 인자로 넘기게 된다. (@RequestBody, HttpEntity가 아닌 경우는 HTTP 메시지 컨버터를 호출하지 않고, ArgumentResolver에서만 로직이 작동하고 어댑터에 객체를 반환한다.)
2.3 받은 인자를 통해 핸들러에서 로직을 수행하고 return 을 통해 String이든, 객체든 반환하게 된다. 이럴 때 returnValueHandler가 사용된다. 앞서 ArgumentResolver와 마찬가지로, 지원할 수 있는 ValueHandler가 조회되고, ValueHandler가 HTTP 메시지 컨버터를 호출해 객체를 반환받아 Dispatcher Servlet에 반환하는 것이다. ((단, @RequestBody, HttpEntity가 아닌 경우는 HTTP 메시지 컨버터를 호출하지 않는다.)
본 포스팅은 김영한님 인프런 강의내용을 바탕으로 복습을 위해 작성하였습니다. 강의를 통해 배운 개념을 바탕으로 추가적으로 공부한 부분과 간단한 코드 예제를 작성하였습니다. 코드 전체를 복사한 것이 아니라 임의로 수정하거나 생략하였습니다.
'백엔드 > Spring' 카테고리의 다른 글
[Spring] 상품 상세, 등록폼, 등록 처리, 상품 수정 (0) | 2022.07.28 |
---|---|
[Spring] 타임리프 실습 (0) | 2022.07.26 |
[Spring] 스프링 HTTP 요청 받기 (0) | 2022.07.22 |
[Spring] 스프링 MVC - 구조 이해 (0) | 2022.07.11 |
[Spring] MVC 패턴 핸들러, 어댑터 (0) | 2022.06.14 |