개발일기
Spring Boot MVC 기본기능 모음집 회고- 2편 본문
HTTP 요청 메시지 - 단순 텍스트
Http message body에 데이터를 직접 담아서 요청
해당 방식으로는 JSON, XML, TEXT
주로 JSON형식을 사용한다.
POST, PUT , PATCH, DELETE
package hello.springmvc.basic.request;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
@Slf4j
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v1")
public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
response.getWriter().write("ok");
}
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
responseWriter.write("ok");
}
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(RequestEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new ResponseEntity<String>("ok", HttpStatus.OK);
}
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody){
log.info("messageBody={}", messageBody);
return "ok";
}
}
v1에서는 HTTP 메시지 바디의 데이터를 InputStream을 통하여 데이터를 읽어 올 수 있다.
v2에서는 writer로 응답 결과 값을 보내주는 부분을 Writer를 매개변수로 받아서 responseWriter.write("ok")를 통해서 응답 결과 값을 반환 해주는 모습을 확인할 수 있다.
스프링 MVC는 다음 파라미터를 지원한다.
InputStream(Reader) : HTTP 요청 메시지 바디에 내용을 직접 조회
OutputStream(Writer) : HTTP 응답 메시지의 바디에 직접 결과 출력
HttpEntity - requestBodyStringV3
HttpEntity를 사용해서 header, body정보를 편리하게 조회할 수 있다.
HttpMessageConverter사용한다. StringHttpMessageConverter HttpEntity는 또한 매개변수를 추가적으로 받을 필요 없이 Return도 HttpEntity를 사용할 수 있다.
ResponseEntity<String> ("hhhelloo", responseHeaders, HttpStatus.CREATED)
V4는 RequestBody를 통해서 적용한 부분이다.
@RequestBody
@RequestBody를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다. 추가적으로 헤더 정보가 필요하다면 HttpEntity를 이용하거나, @RequestHeader를 사용하면 된다.
package hello.springmvc.basic.request;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import hello.springmvc.basic.HelloData;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* {"username" : "hello" , "age": 20}
* content-type: application/json
*/
@Slf4j
@Controller
public class RequestBodyJsonController {
private ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("request-body-json-v1")
public void reqeustBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody : {}" , messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
response.getWriter().write("ok");
}
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) throws IOException {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV3(HttpEntity<HelloData> httpEntity) throws IOException {
HelloData data = httpEntity.getBody();
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) throws IOException {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
}
v1은 HttpServletRequest를 사용해서 직접 Http메시지 바디에서 데이터를 읽어와서 문자로 바꾼다. 이때 코드는 아까와 똑같이 InputStream으로 받아온다. 그 후, InputStream으로 받을 값을 String 변수에 넣어 준 후, 문자로 되어 있는 JSON데이터를 JSON라이브러리인 ObjectMapper를 사용하여 자바 객체로 변환한다.
v2는 @RequestBody를 사용하여 String데이터로 messageBody를 가져와 준다. 이때, messageBody는 json이 String으로 되어 있는 형태이다.
objectMapper.readValue(messageBody, HelloData.class)
해당 방식으로 문자로 되어 있던 JSON데이터를 ObjectMapper를 통해서 자바 객체로 변경해준다.
@RequestBody 객체 변환 - V3
@RequestBody HelloData data
HttpEntity, @RequestBody를 사용하면 HTTP메시지 컨버터가 메시지 바디의 내용을 우리가 원하는 문자나 객체등으로 변환해준다.
이러한 과정을 V2에서 객체로 변환하는 것을 MessageConverter가 해준 것이다.
@RequestBody는 생략이 불가능하다.
@RequestBody를 생략 할 시 Integer , String , int 같은 단순 타입 = @RequestParam
나머지 = @ModelAttribute(argument resolver로 지정해둔 타입외)
따라서 이 경우 HelloData에 @RequestBody를 생략하면 @ModelAttribute가 적용되어버린다.
HelloData data -> @ModelAttribute HelloData data
따라서 생략해버리면 @RequestBody는 요청 파라미터가 되어버린다.
주의 할 점 Http 요청 시에 Content-type이 application/json인지 꼭 확인해야함 그렇지 않으면 HttpMessageConverter가 이를 판단하여 변경해주는데, 이를 지키지 않을 시 JSON 컨버터가 실행안됨.
V4에서 보면
HttpEntity<HelloData> httpEntity
위에와 같은 형식으로 매개변수를 받아 올 수 있으며
HelloData data = httpEntity.getBody(); 해당 방식으로 getBody를 통해서 꺼내줌.
@ResponseBody
- 메시지 바디 정보 직접 반환(view 조회X)
- HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용 (Accept: application/json)
@RequestBody요청
- JSON요청 -> Http 메시지 컨버터 -> 객체
@ResponseBody
- 객체 -> HTTP 메시지 컨버터 -> JSON응답
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
ViewResolver가 탐색할 수 있는 부분을 yml , properties를 통하여 설정해줄 수 있음.
package hello.springmvc.basic.response;
import hello.springmvc.basic.HelloData;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@Slf4j
@RestController
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 ResponseEntity<String> responseBodyV2() throws IOException {
return new ResponseEntity<>("ok", HttpStatus.OK);
}
// @ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() throws IOException {
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
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
v1에서 서블릿에서 다뤘던 것처럼 response.getWriter.write("ok") 해당 방식으로 응답 메시지를 전달 해주었다.
v2 ResponseEntity는 HttpEntity를 상속받았다. HttpEntity는 메시지의 헤더, 바디 정보를 가지고 있다. ResponseEntity는 여기에 추가해서 HttpStatus 응답 상태코드를 설정할 수 있다.
return new ResponseEntity<>("ok", HttpStatus.OK);
v3 @ResponseBody를 사용하면 view를 사용하지 않고, Http메시지 컨버터를 통해서 HTTP 메시지를 입력할 수 이다. ResponseEntity도 이와 동일한 방식으로 진행함.
JsonV1 ReponseEntity를 반환한다. 여기서 HTTP 메시지 컨버터를 통해서 JSON형식으로 변환되어서 반환된다.
JsonV2 ResponseEntity는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody를 사용하면 이런 것을 설정하기 까다롭다.
이때는, @ResponseStatus(HttpStatus.OK) 애노테이션을 사용하면 응답코드도 설정할 수 있다.
프로그램을 따라서 동적으로 변경하고 싶으면 ResponseEntity사용하자..!
@RestController
@Controller 어노테이션 대신에 @RestController를 사용하면 해당 컨트롤러에 있는 모두에 @ResponseBody 어노테이션이 붙는 효과가 있다. 따라서 뷰템플릿을 사용하는 것이 아닌 HTTP Message Body에 직접 데이터를 입력한다.
@ResponseBody는 클래스 레벨에 두면 전체 메서드에 적용되는데, @RestController 애노테이션 안에 @ResponseBody가 적용되어 있다.
HTTP 메시지 컨버터
@ResponseBody를 사용하면
HTTP의 BODY에 문자 내용을 직접 반환한다.
@ResponseBody사용
- HTTP의 BODY에 문자 내용을 직접 반환
- viewResolver 대신에 HttpMessageConverter가 동작
- 기본 문자처리: StringHttpMessageConverter
- 기본 객체처리: MappingJackson2HttpMessageConveter
- byte등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있다.
HTTP 메시지 컨버터는 HTTP 요청, HTTP 응답 둘 다 사용된다.
canRead() , canWrite() : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크
read() , write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
스프링부트는 엄청 많은 메시지 컨버터를 제공한다. 대상 클래스 타입과 미디어 타입 둘을 체크해서 사용 여부를 결정한다. 만약 만족하지 않으면 다음 메시지 컨버터로 우선 순위가 넘어간다.
중요한 컨버터를 살펴보자..!
ByteArrayHttpMessageConverter: byte[] 데이터를 처리한다.
클래스 타입 : byte[] , 미디어 타입 : */*
예 ) @RequestBody byte[] data
응답 ) @ResponseBody return byte[] 쓰기 미디어 타입 application/octet-stream
StringHttpMessageConverter: String 문자로 데이터를 처리한다.
클래스 타입: String, 미디어 타입: */*
요청 예) @RequestBody HelloData Data
응답 ) @ResponseBody return helloData
Http 요청 데이터 읽기
- Http 요청이 온 후, RequestBody , HttpEntity 파라미터를 사용한다.
- 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead()를 호출 한다.
- 대상 클래스 타입을 지원하는가
- @RequestBody의 대상 클래스 bye[], String, 객체
- 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 응답 메시지 바디에 데이터를 생성한다.
요청 매핑 헨들러 어뎁터 구조
모든 비밀은 애노테이션 기반의 컨트롤러, @RequestMapping 요청 핸들러에있음 핸들러 어댑터에서 핸들러 컨트롤러 사이에 @RequestMappingHandlerAdapter에 있다.
RequestMappingHandlerAdapter 동작 방식
ArgumentResolver
애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있음. 어떻게 이런일이 가능한가?
이렇게 다양한 파라미터를 처리할 수 있는 이유는 ArgumentResolver덕분이다.
RequestMappingHandlerAdapter가 ArgumentResolver를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터 값을 생성해준다.
ArgumentResolver는 supportsParameter()호출해서 해당 파리미터가 지원되는지 체크한다.
지원하면 resolveArgument()를 호출해서 실제 객체를 생성해준다.
ReturnValueHandler
이것은 ArgumentResolver와 매우 유사한 형태이며 응답 값을 변환하고 처리하는 역할을 한다.
요청의 경우 @RequestBody를 처리하는 ArgumentResolver가 있고, HttpEntity를 처리하는 ArgumentResolver가 있다.
ArgumentResolver가 넘어온 json데이터나 여러 데이터 형태들을 객체로 변환 시켜준다.
응답의 경우 @ResponseBody와 HttpEntity 를 처리하는 ReturnValueHandler가 있다.
스프링은 이러한 것들을 인터페이스을 사용하는 기능들을 인터페이스로 제공한다. 덕분에 필요하면 언제든지 확장할 수 있다.
- HandlerMethodArgumentResovler
- HandlerMethodReturnValueHandler
- HttpMessageConverter
실제 기능을 확장할 때 WebMvcController를 사용하자
실제로 기능적인 부분이아니라 이것저것 뭔가 설정을 추가하고 필요할 때, WebMvcConfigure에서 사용했던 기억이 있다.
'Spring > (김영한님)스프링 MVC 1편 - 백엔드 웹 개발 핵심기술' 카테고리의 다른 글
Spring Boot MVC 기본기능 모음집 회고- 1편 (0) | 2024.03.27 |
---|---|
MVC 프레임워크 만들기 (V3, V4, V5) (1) | 2024.03.06 |
MVC 프레임워크 만들기 정리 및 회고 (V1, V2) (0) | 2024.03.06 |
서블릿, JSP , MVC 패턴 회고 및 정리 (0) | 2024.03.02 |
Session2. 서블릿 정리및 회고 ( request, response) (2) | 2024.02.27 |