[김영한 스프링 MVC 1] 스프링 MVC - 기본 기능
로깅
운영 시스템 => 로깅 라이브러리 사용해 로그 출력
(SLF4J 라이브러리 => Logback, Log4J 등 라이브러리 통합해 인터페이스로 제공)
- 로그 선언
private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class)
@Slf4j
- LogTestController
package hello.springmvc.basic;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController //반환값으로 뷰 찾지 않고 HTTP 메시지 바디에 바로 입력
public class LogTestController {
//private final Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/log-test")
public String logTest(){
String name = "logTest";
//모든 정보 출력
System.out.println("name = " + name);
//logging level (application.properties 설정)에 따라 출력
log.trace("trace log = {}", name);
log.debug("debug log = {}", name); //개발 서버
log.info("info log = {}", name); //운영 서버
log.warn("warn log = {}", name);
log.error("error log = {}", name);
////사용금지
log.trace("trace log =" + name); //자바에서 연산 실행해 CPU, 메모리 등 자원 낭비
return "ok";
}
}
- 로그 레벨 설정
#전체 로그 레벨 설정 (기본 info)
logging.level.root=info
#hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug
레벨 : : TRACE > DEBUG > INFO > WARN > ERROR
- 로그 라이브러리 사용 시 장점
쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정 가능
로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절 가능
시스템 아웃 콘솔에만 출력하지 않고, 로그를 파일, 네트워크 등 별도 위치에 남길 수 있음
=> 파일로 남길 때 일별/특정 용량별로 로그 분할 가능
일반 System.out보다 성능 좋음 (내부 버퍼링, 멀티 쓰레드 등)
요청 매핑
- HTTP 메소드 매핑 축약
@RequestMapping
=>
@GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping
- MappingController
package hello.springmvc.basic.requestmapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping({"/hello-basic", "/hello-go"}) ////다중 설정 가능
public String helloBasic(){
log.info("helloBasic");
return "ok";
}
/**
* PathVariable
* 변수명이 같으면 생략 가능
* /mapping/userA
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data){ //@Pathvariable String userId
log.info("mappingPath userId = {}", data);
return "ok";
}
/**
* PathVariable 다중 사용
*/
@GetMapping("/mappping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId){
log.info("mappingPath userId = {}, orderId = {}", userId, orderId);
return "ok";
}
/**
* 특정 파라미터 조건 매핑
* params = "mode",
* params = "!mode",
* params = "mode=debug",
* params = "mode!=debug",
* params = {"mode=debug", "data=good"}
*/
@GetMapping(value = "/mapping-param", params = "mode=debug") // localhost:8080/mapping-param?mode=debug 일 떄만 불러옴
public String mappingParam(){
log.info("mapping param");
return "ok";
}
/**
* 특정 헤더 조건 매핑
* headers = "mode",
* headers = "!mode",
* headers = "mode=debug",
* headers = "mode!=debug"
*/
@GetMapping(value = "/mapping-header", headers= "mode=debug")
public String mappingHeader(){
log.info("mapping header");
return "ok";
}
/**
* 미디어 타입 조건 매핑 - HTTP 요청 - Content-Type
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
* 틀리면 HTTP 415 코드 반환
*/
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
/**
* 미디어 타입 조건 매핑 - HTTP 요청 - Accept
* produces = "text/html"
* produces = "!text/html"
* produces = "text/*"
* produces = "*\/*"
* 틀리면 HTTP 406 코드 반환
*/
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
}
요청 매핑 - API 예시
- 회원 관리 API
회원 목록 조회: GET /users
회원 등록: POST /users
회원 조회: GET /users/{userId}
회원 수정: PATCH /users/{userId}
회원 삭제: DELETE /users/{userId}
- MappingClassController
package hello.springmvc.basic.requestmapping;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
/**
* GET /mapping/users
*/
@GetMapping
public String users() {
return "get users";
}
/**
* POST /mapping/users
*/
@PostMapping
public String addUser() {
return "post user";
}
/**
* GET /mapping/users/{userId}
*/
@GetMapping("/{userId}")
public String findUser(@PathVariable String userId) {
return "get userId=" + userId;
}
/**
* PATCH /mapping/users/{userId}
*/
@PatchMapping("/{userId}")
public String updateUser(@PathVariable String userId) {
return "update userId=" + userId;
}
/**
* DELETE /mapping/users/{userId}
*/
@DeleteMapping("/{userId}")
public String deleteUser(@PathVariable String userId) {
return "delete userId=" + userId;
}
}
HTTP 요청 - 기본, 헤더 조회
- RequestHeaderController
package hello.springmvc.basic.request;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Locale;
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String headers(HttpServletRequest request,
HttpServletResponse response,
HttpMethod httpMethod,
Locale locale,
@RequestHeader MultiValueMap<String, String> headerMap,
@RequestHeader("host") String host,
@CookieValue(value = "myCookie", required = false) String cookie) {
log.info("request={}", request);
log.info("response={}", response);
log.info("httpMethod={}", httpMethod);
log.info("locale={}", locale);
log.info("headerMap={}", headerMap);
log.info("header host={}", host);
log.info("myCookie={}", cookie);
return "ok";
}
}
HttpMethod : HTTP 메서드 조회
Locale : Locale 정보를 조회한다.
@RequestHeader MultiValueMap<String, String> headerMap : 모든 HTTP 헤더를 MultiValueMap 형식으로 조회
@RequestHeader("host") String host : 특정 HTTP 헤더를 조회
@CookieValue(value = "myCookie", required = false) String cookie : 특정 쿠키를 조회 (필수 X)
- MultiValueMap
Map과 유사
하나의 키에 여러 값 받을 수 있음
MultiValueMap<String, String> map = new LinkedMultiValueMap();
map.add("keyA", "value1");
map.add("keyA", "value2");
//[value1,value2]
List<String> values = map.get("keyA");
HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form / @RequestParam / @ModelAttribute
- 클라이언트 → 서버로 요청 데이터 전달하는 방법
1. GET - 쿼리 파라미터
http://localhost:8080/url?username=hello&age=20
메시지 바디 없이, URL 쿼리 파라미터에 데이터 전달
ex) 검색, 필터, 페이징 등
=> 요청 파라미터(request parameter) 조회
2. POST - HTML Form
POST url
content-type: application/x-www-form-urlencoded
..
username=hello&age=20 // 메시지 바디
메시지 바디에 쿼리 파리미터 형식으로 전달
ex) 회원 가입, 상품 주문 등
=> 요청 파라미터(request parameter) 조회
3. HTTP message body에 데이터를 직접 담아서 요청
HTTP API에서 주로 사용 (JSON, XML, TEXT 등)
데이터 형식은 주로 JSON 사용
ex) POST, PUT, PATCH
- RequestParamController
package hello.springmvc.basic.request;
import hello.springmvc.basic.HelloData;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
@Slf4j
@Controller
//@RestController // => @Responsebody + @Controller
public class RequestParamController {
// 반환 타입 없으면서 응답값 있으면 view 조회 X // ok만 화면에 표시됨
@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
log.info("username={}, age={}", username, age);
response.getWriter().write("ok");
}
@RequestMapping("/request-param-v2")
@ResponseBody // HTTP 메시지 바디에 내용 입력 (view 조회 X)
public String requestParamV2(
@RequestParam("username") String memberName, // @Requestparam : 파라미터 이름으로 바인딩
@RequestParam("age") int memberAge // = request.getParameter("age")
){
log.info("username={}, age={}", memberName, memberAge);
return "ok";
}
@RequestMapping("/request-param-v3")
@ResponseBody
public String requestParamV3(
@RequestParam String username, // HTTP 파라미터명 = 변수명 이면 괄호값 생략 가능
@RequestParam int age
){
log.info("username={}, age={}", username, age);
return "ok";
}
@RequestMapping("/request-param-v4")
@ResponseBody
public String requestParamV4(String username, int age){ // String, int, Integer 등 단순 타입 => @ 생략 가능
log.info("username={}, age={}", username, age);
return "ok";
}
@RequestMapping("/request-param-required")
@ResponseBody
public String requestParamV5(
@RequestParam String username, // required 기본값 true (어노테이션 생략 시 기본값 false)
@RequestParam(required = false) Integer age){ // int는 null 허용 X -> 500 에러 (defaultValue 나 Integer 사용하기)
log.info("username={}, age={}", username, age);
return "ok";
}
// required=true 인데 해당 파라미터 이름, 값 전달 X => 400 예외
// 파라미터 이름만 있으면 => 빈 문자열로 통과
@RequestMapping("/request-param-default")
@ResponseBody
public String requestParamDefault(
@RequestParam(defaultValue = "guest") String username, // null, 빈 문자에 적용
@RequestParam(required = false,defaultValue = "-1") int age){
log.info("username={}, age={}", username, age);
return "ok";
}
/**
* @RequestParam Map, MultiValueMap
* Map(key=value)
* MultiValueMap(key=[value1, value2, ...]) ex) (key=userIds, value=[id1, id2])
*/
@RequestMapping("/request-param-map")
@ResponseBody
public String requestParamMap(@RequestParam Map<String, Objects> paramMap){
log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
return "ok";
}
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData){ // => HelloData 객체 생성, 요청 파라미터 명의 프로퍼티 찾아 바인딩
//HelloData data = new HelloData();
//data.setUsername(username);
//data.setAge(age);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
log.info("helloData={}", helloData);
return "ok";
}
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData){ // 단순 타입 외에는 @ModelAttribute으로 인식 => 생략 가능 (argument resolver 지정한 타입 외)
log.info("helloData={}", helloData);
return "ok";
}
}
HTTP 요청 메시지 - 단순 텍스트
요청 파라미터와 다르게 HTTP 메시지 바디를 통해 직접 넘어오면 @RequestParam, @ModelAttribute 사용 불가 (HTML Form은 요청 파라미터로 인정)
- RequestBodyStringController
package hello.springmvc.basic.request;
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.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
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 {
// InputStream을 사용해 HTTP 메시지 바디 데이터 읽음
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
response.getWriter().write("ok");
}
/**
* InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
* OutputStream(Writer): HTTP 응답 메시지 바디에 결과 직접 출력
*/
@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");
}
/**
* HttpEntity: HTTP header, body 정보를 편리하게 조회
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용 (HTTP 메시지 바디 읽어 문자/객체로 변환, 전달)
*
* 응답에서도 HttpEntity 사용 가능
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@PostMapping("/request-body-string-v3")
public ResponseEntity<String> requestBodyStringV3(RequestEntity<String> httpEntity) throws IOException {
//String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new ResponseEntity<String>("ok", HttpStatus.CREATED);
}
// HttpEntity 상속받은 객체들 =>
// @RequestEntity : HttpMethod, url 정보 추가됨
// @ResponseENtity : HTTP 상태 코드 설정 가능
/**
* @RequestBody
* - 메시지 바디 정보를 직접 조회 (헤더 정보 필요하면 @HttpEntity or @RequestHeader)
*
* @ResponseBody
* - 메시지 바디 정보 직접 반환 (view 조회X)
*/
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
return "ok";
}
}
※
요청 파라미터 조회 : @RequestParam , @ModelAttribute
HTTP 메시지 바디 조회 : @RequestBody
HTTP 요청 메시지 - JSON
- RequestBodyJsonController
package hello.springmvc.basic.request;
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.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 {
// Jackson 라이브러리 ObjectMapper 사용해 JSON → 자바 객체 변환
private ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(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); //json 데이터를 HelloData로 매핑
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
response.getWriter().write("ok");
}
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class); //json 데이터를 HelloData로 매핑
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData messageBody) { //@RequestBody 생략 불가 // 생략 시 @ModelAttrivute로 인식, 기본값 대입
log.info("messageBody={}", messageBody);
return "ok";
}
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(@HttpEntity<HelloData> httpEntity) {
HelloData data = httpEntity.getBody();
log.info("messageBody={}", data);
return "ok";
}
@ResponseBody // JSON으로 응답
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) { // JSON -> 객체
log.info("messageBody={}", data);
return data;
}
}
HTTP 응답 - 정적 리소스, 뷰 템플릿
- 서버에서 응답 데이터 만드는 방법
1. 정적 리소스
웹 브라우저에 정적인 HTML, css, js를 제공할 때
2. 뷰 템플릿 사용
웹 브라우저에 동적인 HTML을 제공할 때
3. HTTP 메시지 사용
HTTP API => HTML이 아니라 데이터를 전달
→ HTTP 메시지 바디에 JSON 등 형식으로 데이터 전달
- 정적 리소스 (파일 변경 없이 그대로 서비스)
스프링부트는 클래스패스의 하단 디렉토리에 있는 정적 리소스 제공함
/static , /public , /resources , /META-INF/resources
src/main/resources/static/basic/hello-form.html
=>
http://localhost:8080/basic/hello-form.html
- 뷰 템플릿 (동적 HTML)
기본 뷰 템플릿 경로 : src/main/resources/templates
- hello.html (타임리프)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="${data}">empty</p>
</body>
</html>
- ResponseViewController
package hello.springmvc.basic.response;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class ResponseViewController {
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1(){
ModelAndView mav = new ModelAndView("response/hello")
.addObject("data", "hello!"); //선택한 뷰 내 object 값에 hello! 대입
return mav;
}
// @ResponseBody X => 사용 시 페이지 반환 안 하고 String 자체를 화면에 띄움
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model){
model.addAttribute("data", "hello!");
return "response/hello"; // response/hello 로 뷰 리졸버 실행, 뷰 찾아 렌더링
}
// 불명확하므로 권장 X
@RequestMapping("/response/hello") //경로값=반환값 같으면 해당 값 페이지 자동 반환
public void responseViewV3(Model model){
model.addAttribute("data", "hello!");
}
}
※ application.properties
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
- HTTP 메시지 바디에 직접 입력 (HTTP API)
※ HTML, 뷰 템플릿도 HTTP 응답 메시지 바디에 HTML 데이터 담아 전달함
이 방식은 정적 리소스/뷰 템플릿 거치지 않고 직접 HTTP 응답 메시지 전달하는 경우
- ResponseBodyController
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 java.io.IOException;
@Slf4j
@Controller
// @RestController => @Controller + @ResponseBody
public class ResponseBodyController {
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok"); // response 객체 통해 직접 메시지 바디 입력
}
/**
* HttpEntity, ResponseEntity(Http Status 추가)
* @return
*/
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
return new ResponseEntity<>("ok", HttpStatus.OK); // HTTP 응답 코드, 메시지 바디 내용 전달
}
@ResponseBody // view 사용하지 않고 메시지 내용 입력
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
@GetMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1() { //ResponseEntity 반환 (객체 -> Json)
HelloData helloData = new HelloData();
helloData.setUsername("abc");
helloData.setAge(20);
return new ResponseEntity<>(helloData, HttpStatus.OK); ////
}
@ResponseStatus(HttpStatus.OK) // 응답 코드 설정
@GetMapping("/response-body-json-v1")
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("abc");
helloData.setAge(20);
return helloData;
}
}
HTTP 메시지 컨버터
- @ResponseBody 원리
@ResponseBody
→ HTTP BODY에 문자 내용을 직접 반환
→ viewResolver 대신 HttpMessageConverter 동작
(기본 문자 처리: StringHttpMessageConverter, 기본 객체 처리: MappingJackson2HttpMessageConverter)
※ 응답 => 클라이언트의 HTTP Accept 헤더 & 서버의 컨트롤러 반환 타입 정보 조합해 HttpMessageConverter 선택
- 스프링MVC가 사용하는 HTTP 메시지 컨버터
HTTP 요청: @RequestBody , HttpEntity(RequestEntity)
HTTP 응답: @ResponseBody , HttpEntity(ResponseEntity)
=> 이 때 HTTP 메시지 컨버터 적용
- HTTP 메시지 컨버터 인터페이스
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;
}
canRead() , canWrite() : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크
read() , write() : 메시지 컨버터를 통해서 메시지를 읽고 씀
- 기본 메시지 컨버터
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
ByteArrayHttpMessageConverter : byte[] 데이터 처리
클래스 타입: byte[] , 미디어타입: */*
요청 ex) @RequestBody byte[] data
응답 ex) @ResponseBody return byte[] + 미디어타입 application/octet-stream
StringHttpMessageConverter : String 문자로 데이터 처리
클래스 타입: String , 미디어타입: */*
요청 ex) @RequestBody String data
응답 ex) @ResponseBody return "ok" + 미디어타입 text/plain
MappingJackson2HttpMessageConverter : application/json
클래스 타입: 객체 또는 HashMap , 미디어타입: application/json 관련
요청 ex) @RequestBody HelloData data
응답 ex) @ResponseBody return helloData + 미디어타입 application/json 관련
- 적용 예시
content-type: application/json
@RequestMapping
void hello(@RequestBody String data) {}
=> StringHttpMessageConverter
content-type: application/json
@RequestMapping
void hello(@RequestBody HelloData data) {}
=> MappingJackson2HttpMessageConverter
content-type: text/html
@RequestMapping
void hello(@RequestBody HelloData data) {}
=> 오류
- 동작 과정
HTTP 요청 데이터 읽기
HTTP 요청이 오고, 컨트롤러에서 @RequestBody , HttpEntity 파라미터 사용
→ 메시지 컨버터가 메시지 읽을 수 있는지 확인하기 위해 canRead() 호출 (대상 클래스 타입, HTTP 요청의 Content-Type 미디어 타입)
→ canRead() 조건 만족 시 read() 호출
→ 객체 생성, 반환
HTTP 응답 데이터 생성
컨트롤러에서 @ResponseBody , HttpEntity 로 값이 반환
→ 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 호출 (대상 클래스 타입, HTTP 요청의 Accept 미디어 타입 - 정확히는 @RequestMapping 의 produces )
→ canWrite() 조건 만족 시 write() 호출
→ HTTP 응답 메시지 바디에 데이터 생성
요청 매핑 핸들러 어댑터 구조
- 구조
HTTP 메시지 컨버터
=> 애노테이션 기반의 컨트롤러
= @RequestMapping 을 처리하는 핸들러 어댑터
= RequestMappingHandlerAdapter 가 담당
- RequestMappingHandlerAdapter 동작 방식
- ArgumentResolver (=HandlerMethodArgumentResolver)
: 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체) 생성
스프링 => 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 만들 수 있음
- ReturnValueHandler (=HandlerMethodReturnValueHandler)
: 응답 값 변환, 처리
ex) 컨트롤러에서 String으로 뷰 이름을 반환해도 동작하도록 해줌
ex) ModelAndView , @ResponseBody , HttpEntity , String
- HttpMessageConverter
요청
@RequestBody 담당 ArgumentResolver, HttpEntity 담당 ArgumentResolver가 있음
해당 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해 필요한 객체 생성함
응답
@ResponseBody와 HttpEntity를 처리하는 ReturnValueHandler가 있음
=> HTTP 메시지 컨버터를 호출해 응답 결과 생성
@RequestBody, @ResponseBody => RequestResponseBodyMethodProcessor (ArgumentResolver) 사용
HttpEntity => HttpEntityMethodProcessor (ArgumentResolver) 사용
- 확장
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) {
//...
}
};
}