- 스프링 로깅에 대해 알아볼 수 있다.
- HTTP 요청과 응답을 처리하는 다양한 방식 알아본다.
- HTTP 메시지 컨버터에 대해 알아본다.
6. 스프링 MVC -기본기능
1) 로깅 알아보기
운영 시스템에서 System.out.println() 같은 시스템 콘솔을 사용하여 필요한 정보를 출력하지 않고, 별도의 로깅 라이브러리를 사용해서 로그를 출력한다.
스프링부트 라이브러리 사용시 로깅 라이브러리(spring-boot-starter-logging)가 함께 포함되는데 스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.
- SLF4J
- Logback
SLF4J는 Logback, Log4J, Log4J2 등 많은 라이브러리를 통합해서 인터페이스로 제공한다.
SLF4J는 인터페이스이고 그 구현체로 Logback 같은 로그 라이브러리를 사용한다.
로그 선언 방법
- private Logger log = LoggerFactory.getLogger(getClass());
- private static final Logger log = LoggerFactory.getLogger(Xxx.class)
- @Slf4j : 롬복 사용 가능
[LogTestController.java]
@Slf4j //lombok이 제공하는 에노테이션
@RestController
public class LogTestController {
//Logger는 org.lsf4j.Logger 인터페이스를 사용하고 getClass()로 자신을 넣으면 됨.
// private final Logger log = LoggerFactory.getLogger(getClass());
//Slf4j 에노테이션이 자동으로 추가해줌
@RequestMapping("/log-test")
public String logTest() {
String name = "spring";
System.out.println("name = " + name);
log.info(" info log=" + name); //이렇게 사용하면 안 됨(연산이 일어남)
//" info log=Spring"
log.trace("trace log={}", name); //치환
log.debug("debug log={}", name); //개발서버
log.warn("warn log={}", name);
log.error("error log={}", name); //경고
log.info("info log ={}", name); //에러
return "ok";
//@RestController -> 문자열("ok")을 반환하면 String이 그대로 바로 반환됨 http 메시지 바디에 바로 들어감
}
}
@Controller가 아닌 @RestController를 사용했다. @Controller는 요청 매핑 메소드 반환 타입이 String이면 뷰 이름으로 인식이 된다. 그래서 뷰를 찾고 뷰가 랜더링 된다. 하지만 @RestController는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력을 해서 실행 결과로 ok 메시지를 받을 수 있다.
@Controller를 쓰고 메소드에 @ResponseBdoy를 붙여도 동일한 기능을 한다.
로그 레벨 순서는 TRACE > DEBUG > INFO > WARN > ERROR 순인데 개발 서버는 보통 debug를 출력하고 운영은 info를 출력한다. 예를들어 info의 경우 info, warn, error까지 상위 레벨의 로그까지 나온다.
로그 레벨 설정은 application.properties에서 할 수 있다.
[application.properties]
#전체 로그 레벨 설정(기본 info)
logging.level.root=info
#hello.springmvc 패키지와 그 하위 로그 레벨 설정(TRACE까 가장 낮은 레벨)
#운영에서는 기본적으로 info로 설정하여 info, warn, error을 보여줌
logging.level.hello.springmvc=debug
- log.debug("data=" + data);
- 로그 출력 레벨 info 설정시에도 해당 코드에 있는 + 연산이 실행되어 리소스 낭비
- log.debug("data={}", data);
- 로그 출력 레벨을 info로 설정하면 아무일도 발생 x, 의미없는 연산 발생 x
로그 사용 장점
- 쓰레드 정보, 클래스 이름 같은 부가 정보 함께 볼 수 있고, 출력 모양 조절 가능
- 로그 상황에 맞게 조절 가능
- 파일, 네트워크 등 로그 별도의 위치에 남길 수 있다. (파일로 남길 때에는 일별, 특정 용량에 따라 로그 분할도 가능)
- System.out보다 성능도 좋음
2) 요청 매핑
요청을 컨트롤러에서 매핑하는 여러가지 방법들에 대해 알아보자.
요청 매핑이란 URL 요청이 왔을 때 어떤 컨트롤러가 호출이 되어야할지 mapping해 놓는 것을 말한다.
[MappingController.java]
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
/* 1. 기본 요청
* @RequestMapping({"/hello-basic", "hello-go"}) 이렇게도 가능
* HTTP 메서드 모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE
* */
@RequestMapping(value = "/hello-basic")
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
//2.method 특정 HTTP 메서드 요청만 허용, 여기에 POST 요청하면 405상태코드 반환
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
/*
* 3. 편리한 축약 애노테이션 (코드보기)
* @GetMapping
* @PostMapping
* @PutMapping
* @DeleteMapping
* @PatchMapping
*/
@GetMapping(value = "/mapping-get-v2") //코드 내부에서 @ReuqestMapping과 method를 지정해서 사용하는 것 확인 가능
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
/*
* pathVariable(경로 변수) 사용 ->제일 자주 쓰임
* 리소스 경로에 식별자 넣는 스타일
*
* 변수명이 같으면 생략 가능
* @PathVariable("userId") String userId -> @PathVariable user Id
*
* http://localhost:8080/mapping/userA
*
* */
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId")String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
/**
* PathVariable 사용 다중
* http://localhost:8080/mapping/users/userA/orders/100
*/
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long
orderId) {
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "ok";
}
/** 특정 파라미터 조건 매핑 params = "key=value"
* value에 해당하는 url 뒤의 query string 형식을 통해 특정 파라미터가 있거나 없는 조건을 통해 추가
매핑할 수 있다.
* 파라미터로 추가 매핑
* ?mode=debug가 아닌 다른 걸 전송하면 400 Bad Request
* params="mode",
* params="!mode"
* params="mode=debug"
* params="mode!=debug" (! = )
* params = {"mode=debug","data=good"}
*
* http://localhost:8080/mapping-param?mode=debug
*/
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "ok";
}
/**
* 특정 헤더로 추가 매핑
* HTTP 요청 메세지의 header 내용을 통해 추가 매핑할 수 있다.
* 헤더에 mode=debug를 안 넣어주면 404
* headers="mode",
* headers="!mode"
* headers="mode=debug"
* headers="mode!=debug" (! = )
*/
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
log.info("mappingHeader");
return "ok";
}
/**
* Content-Type 헤더 기반 추가 매핑 Media Type
* 내가 보낼 요청 Header에 Content-Type=application/json이 아니라면 415 Unsupported Media Type
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*
* HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑
* 만약 맞지 않으면 HTTP 415 상태코드 반환
*/
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
/**미디어 타입 조건 매핑-HTTP 요청 Accept, produce
* return할 데이터 타입 명시
* Accept 헤더 기반 Media Type
* produces = "text/html"
* produces = "!text/html"
* produces = "text/*"
* produces = "*\/*"
*
* HTTP 요청의 Accept헤더를 기반으로 미디어 타입으로 매핑 ->만약 맞지 않으면 HTTP 406 상태코드 반환
*
*/
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
}
@RequestMapping("/hello-basic") : /hello-basic URL 호출이 오면 이 메서드가 실행되도록 매핑한다. {"/hello-basic", "/hello-go"} 이런 식으로 다중 매핑도 가능하다. method 속성으로 HTTP 메서드 지정하지 않으면 무관하게 호출된다.(GET, HEAD, POST, PATCH, DELETE 전부 허용된다.)
3) 요청 매핑- API 예시
회원 관리를 HTTP API로 만든다고 가정하면 다음과 같이 매핑
[MappingClassController.java]
package hello.springmvc.basic.requestmapping;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
/*
회원 관리 API
회원 목록 조회: GET /users
회원 등록: POST /users
회원 조회: GET /users/{userId}
회원 수정: PATCH /users/{userId}
회원 삭제: DELETE /users/{userId}
*/
/**
* GET /mapping/users
*/
@GetMapping
public String user() {
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;
}
}
@RequestMapping("/mapping/users") 클래스 레벨에 매핑 정보를 두면 메서드 레벨에서 해당 정보를 조합해서 사용한다.
4)HTTP 요청 - 기본, 헤더 조회
[RequestHeaderController.java]
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String headers(HttpServletRequest request,
HttpServletResponse response,
HttpMethod httpMethod, //HTTP 메서드 조회
Locale locale, //Locale 정보 조회
@RequestHeader MultiValueMap<String, String> headerMap, //모든 HTTP 헤더를 MultiValueMap 형식으로 조회
@RequestHeader("host") String host, //특정 HTTP 헤더 조회
@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";
}
}
MultiValueMap는 MAP과 유사하지만 하나의 키에 여러 값을 받을 수 있다.
keyA=value1&keyA=value2와 같은 형태로 요청을 보내면 keyA=[vlaue1,vlaue2]로 담긴다.
5) HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form
클라이언트에서 서버로 요청 데이터를 전달할 때 사용하는 방법에는 3가지가 있다.
- GET-쿼리 파라미터 : request.getParameter("파라미터명"), 메시지 바디 없음
- POST-HTML Form : 메시지 바디에 쿼리 파라미터 형식으로 전달, request.getParameter()사용 가능, 메시지 바디 있음
- HTTP message body : HTTP API에서 주로 사용 주로 JSON형태로 보냄, HTTP message Body에 직접 데이터 담아서 요청함
GET 쿼리파라미터와 POST HTML Form 전송 방식은 형식이 같아서 구분 없이 request.getParameter()를 사용해서 조회가 가능하다. @RequestParam 어노테이션을 사용하면 단순 타입 (String, int 등)을 바로 가져올 수 있다. 생략도 가능하다.
요청 파라미터가 참조형 객체일 때 @ModelAttribute를 사용할 수 있고 객체면 생략도 가능하다.
[RequestParamController.java]
@Slf4j
@Controller
public class RequestParamController {
//가장 단순한 요청 파라미터 조회 방법
//반환 타입이 없으면서 이렇게 응답에 값을 직접 집어넣으면, view 조회 X
@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");
}
/*
* @RequestParam 사용 - 파라미터 이름으로 바인딩
* @ResponseBody 추가 - View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
*
* */
@ResponseBody //View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName, //request.getParameter("username")
@RequestParam("age") int memberAge) { //@RequestParam : 파라미터 이름으로 바인딩
log.info("username={}, age={}", memberName, memberAge);
return "ok";
}
//@RequestParam 사용 -> HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
/*
만약 url이 ?username=userA&age=20이면 @RequestParam을 통해 변수 username에는 userA가, 변수 age에는
20이 들어감
*/
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam String username,
@RequestParam int age){
log.info("username={}, age={}", username, age);
return "ok";
}
//@RequestParam 사용 -> String, int 등 단순 타입이면 @RequestParam도 생략 가능
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
//
// @RequestParam.required
// /request-param-required -> username이 없으므로 예외
//
// 주의!
// /request-param-required?username= -> 빈문자로 통과 (null 아님)
//
// 주의!
// /request-param-required
// int age-> null을 int에 입력하는 것 불가능, 따라서 Integer 변경해야 함(또는 defaultValue 사용)
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(required = true) String username, //username이 필수로 들어가야함
@RequestParam(required = false) Integer age) {
log.info("username={}, age={}", username, age);
return "ok";
}
/*
@RequestParam-defaultValue 사용
참고: defaultValue는 빈 문자의 경우에도 적용 -> guest로 들어옴
/request-param-default?username=은 guest
*/
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
@RequestParam(required = true, defaultValue = "guest") String username, //username이 필수로 들어가야함
@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])
*/
@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
log.info("username={}, age={}", paramMap.get("username"),
paramMap.get("age"));
return "ok";
}
/*
* public String modelAttributeV1(@RequestParam String username, @RequestParanm int age)
* HelloData helloData = new HelloData();
* helloData.setUsername(username);
* helloData.setAge(age);
*
* @ModelAttribute 사용 -> HelloData 객체가 생성되고, 요청 파라미터 값도 전부 들어가 있음
* 객체 한 번에 받아서 사용 가능
* age=abc처럼 타입이 안맞는 경우에는 BindException발생
* */
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(),
helloData.getAge());
return "ok";
}
/*
@ModelAttribute는 생략 가능한데 @RequestParam도 생략할 수 있으니 혼란 발생
*/
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(),
helloData.getAge());
return "ok";
}
}
@RequestParam("파라미터 이름") 자료형 변수명 이렇게 선언하고 만약 파라미터 이름과 변수명이 같은 경우에는 ("파라미터 이름")을 생략할 수 있다.
@RequestParam은 Servlet의 response.getParam()과 동일한 역할을 한다. response.getPram()의 return형은 String이어서 Integer.ParseInt()함수를 통하여 형변환을 해주어야 하지만 @RequestParam은 형변환을 알아서한 후에 변수에 넣어주어서 따로 신경 쓰지 않아도된다.
6) HTTP 요청 메시지- 단순 텍스트
요청 파라미터와는 다르게 HTTP message body를 통해서 데이터가 직접 넘어오는 경우는 @RequestParam과 @ModelAttribute를 사용할 수 없다.
HTTP message body의 데이터를 InputStream을 사용해서 직접 읽을 수 있다.
[RequestBodyStringController.java]
@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");
}
/*
* 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 적용
*
* 응답에서도 HttpEntity 사용 가능
* -메시지 바디 정보 직접 반환(view 조회x)
* -HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
* */
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
/*
요청 메시지를 @RequestBdoy로 처리가 가능
헤더 정보 필요시 @RequestHeader, HttpEntity 추가 사용 가능
메시지 바디 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와는 전혀 관계 없음
@RequestBody
-메시지 바디 정보 직접 조회
-HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
@ResponseBody
-메시지 바디 정보 직접 반환(view 조회x)
-HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return "ok";
}
}
스프링 MVC 내부에서 HTTP 메시지 바디를 읽어 문자 또는 객체로 변환해서 전달해주는데, 이때 HttpMessageConverter라는 기능을 사용한다.
요청 파라미터를 조회하는 기능 : @RequestParam, @ModelAttribute
HTTP 메시지 바디를 직접 조회하는 기능 : @RequestBody
7)HTTP 요청 메시지 - JSON
단순 텍스트의 경우는 InputStream이나 @RequestBody등으로 처리가 가능하지만 JSON 요청을 보낼 경우에는 Jackson라이브러리의 ObjectMapper를 통하여 자바 객체로 변환하는 작업이 필요하다.
[RequestBodyJsonController.java]
/**
* {"username":"hello", "age":20}
* content-type: application/json
*/
@Slf4j
@Controller
public class RequestBodyJsonController {
private ObjectMapper objectMapper = new ObjectMapper();
//HttpServletRequest로 HTTP 메시지 바디에서 데이터 읽어온 후 문자 변환 ->Jackson라이브러리로 자바 객체 변환
@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);
//json형식 요청 데이터를 그대로 출력
//messageBody={"username":"hello", "age"="20"}
log.info("messageBody={}", messageBody);
//json 형식으로 요청 온 데이터를 HelloData로 역직렬화
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
//역직렬화된 HelloData 데이터 출력
//username=hello, age=20
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
response.getWriter().write("ok");
}
/*
* @RequestBody
* HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* @ResponseBody
* -모든 메서드에 @ResponseBody 적용
* -메시지 바디 정보 직접 반환(view 조회 x)
* -HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
* */
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody)throws IOException{
log.info("messageBdoy={}", messageBody);
//json 형식으로 요청 온 데이터를 HelloData로 역직렬화
HelloData data = objectMapper.readValue(messageBody, HelloData.class);
//역직렬화된 HelloData 데이터 출력
//username=hello, age=20
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
/*
* @RequestBody 생략 불가능(@ModelAttribute가 적용되어 버림)
* 요청 데이터를 objectMapper 없이 바로 객체로 받을 수 있음
* json 데이터 형식(content-type:application/json)을 받으면 HTTP메시지컨버터가 ObjectMapper역할을 해줌
* */
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
/**
* HttpEntity로도 json형식을 받을 수 있다.
* json 데이터 형식(content-type:application/json)을 받으면 HTTP메시지컨버터가 ObjectMapper역할을 해줌
*/
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
HelloData data = httpEntity.getBody();
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
/*
* @RequestBody 생략 불가능(@ModelAttribute가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter
* (content-type : application/json)
*
* @ResponseBody 적용
* -메시지 바디 정보 직접 반환(view 조회x)
* -HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용
* (Accept : application/json)
* */
/*
* 반환타입도 HelloData타입으로 가능
* HttpMessageConverter에 의해 객체가 json문자형태로 변환되어 반환
* 요청시 json to object / 응답시 object to json
* 응답 나갈 때 json으로 나갈지 판단은 요청시 accept 확인해야 함
*/
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) { //json이 그대로 hellodata로 들어옴
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
}
requestBodyJsonV3과 같이 문자로 변환하는 작업 없이 바로 객체로 받을 수 있다.
HttpEntity, @RequestBody등을 사용하면 HTTP 메세지 컨버터가 HTTP 메시지 바디 내용(문자나 json)을 문자, 객체 등으로 변환해준다.
JSON처리할 수 있는 메시지 컨버터 실행하기 위해 요청시에는 content-type : application/json여부를 확인해야한다.
8) HTTP 응답 -정적 리소스, 뷰 템플릿
스프링(서버)에서 응답데이터를 만드는 방법
- 정적 리소스 : 웹 브라우저에 정적인 HTML, css, js를 제공할 경우 정적 리소스를 사용한다.
- 뷰 템플릿 사용 : 웹 브라우저에 동적인 HTML 제공할 경우 뷰 템플릿을 사용한다.
- HTTP 메시지 사용 : HTTP API를 제공하는 경우 HTML이 아닌 데이터를 전달해야하니까 HTTP 메시지 바디에 JSON같은 형식으로 데이터 실어 보낸다.
스프링 부트는 class path의 다음 디렉토리에 있는 정적 리소스를 제공한다. (/static, /public, /resources, /META-INF/resources)
src/main/resources는 리소스를 보관하는 곳이자 클래스 패스의 시작 경로이다.
//정적 리소스 경로
src/main/resources/static
//파일
src/main/resources/static/basic/hello-form.html
//실행 주소
http://localhost:8080/baisc/hello-form.html
정적 리소스는 해당 파일 변경 없이 그대로 서비스하는 것이다.
뷰 템플릿은 HTML을 동적으로 생성하여 전달한다.
//뷰 템플릿 경로
src/main/resources/templates
//뷰 템플릿 생성
src/main/resources/templates/response/hello.html
[src/main/resources/templates/response/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.java] - 뷰 템플릿을 호출하는 컨트롤러
@Controller
public class ResponseViewController {
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1() {
ModelAndView mav = new ModelAndView("response/hello")
.addObject("data", "hello!");
return mav;
}
//만약 여기서 @ResponseBody를 쓰면 view 안 찾고 response/hello가 응답 메시지 코드로 나감 (화면에 보여짐)
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model) {
model.addAttribute("data", "hello!");
return "response/hello";
}
//절대 권장 X
@RequestMapping("/response/hello")
public void responseViewV3(Model model) {
model.addAttribute("data", "hello!");
}
//컨트롤러의 경로 이름과 뷰의 논리적 이름이 같고 아무것도 반환을 안 하면 앞의 슬래쉬 떼고 response/hello가 논리적 뷰의 이름으로 진행됨
}
String을 반환하는 경우에는 @ResponseBody가 없으면 response/hello로 뷰리졸버가 실행되어 뷰를 찾고 랜더링한다.
@ResponseBody가 있으면(또는 HttpEntity 반환시) response/hello라는 문자가 HTTP 메시지 바디에 직접 입력된다.
response/hello는 논리명이기 때문에 실제 실행은 "template/resources/hello.html"로 된다.
그리고 스프링 프로젝트 생성시 타임리프(thymeleaf)를 의존성주입으로 추가해뒀기 때문에 자동으로 ThymeleaftViewResolver와 스프링 빈들을 등록한다.
9) HTTP 응답 - HTTP API, 메시지 바디에 직접 입력
HTTP API를 제공하는 경우 HTML이 아닌 데이터를 전달해야 하므로, HTTP Message Body에 JSON 형식으로 데이터를 실어 보낸다.
[ResponseBodyController.java]
@Slf4j
//@Controller
//@ResponseBody
@RestController //@Controller + @ResponseBody
public class ResponseBodyController {
//------------------------------문자 처리------------------------------
//HttpServletResponse 객체를 통해서 Http 메시지 바디에 직접 응답 메시지 전달
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException{
response.getWriter().write("ok");
}
//HttpEntity는 Http 메시지의 헤더, 바디 정보를 갖고 있다.
//ResponseEntity는 HTTP 응답 코드를 설정할 수 있다.
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() throws IOException{
return new ResponseEntity<>("ok", HttpStatus.OK);
}
//@ResponseBody를 사용하면 view 사용하지 않고 HTTP 메시지 컨버터를 통해서 HTTP 메시지 직접 입력 가능
//@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
//------------------------------JSON 처리------------------------------
//HTTP 메시지 컨버터를 통하여 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는 응답 코드 설정할 수 있는데, @ResponseBody를 사용하면 이런 것을 설정하기
* 까다롭기 때문에 @ResponseStatus(HttpStatus.OK) 애노테이션 사용
*/
//제일 많이 쓰이는 스타일
@ResponseStatus(HttpStatus.OK)
//@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
@ResponseBody를 클래스 레벨에 설정해주면 전체 메서드에 적용이된다.
@RestController는 @Controller과 @RestController가 같이 적용이 됨
10) HTTP 메시지 컨버터
HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용한다.
HttpMessageConverter가 작동하는 예시는
- localhost:8080/hello-api호출
- helloController의 hello-api URL을 처리할 메소드 실행
- @ResponseBody가 붙어있으면 HttpMessageConverter가 실행돼서 return값 반환
@ResponseBody를 사용하면 HTTP의 BODY에 문자 내용을 직접 반환하며, viewResolver 대신 HttpMessageConverter가 동작한다. 요청과 다르게 응답의 경우 클라이언트의 Http Header의 Accept와 서버 컨트롤러 반환타입 정보를 조합하여 컨버터의 종류 (StringHttpMessageConverter-기본 문자 처리, MappingJackson2HttpMessageConverter-기본 객체처리 등) 선택한다.
스프링 MVC가 HTTP 메시지 컨버터를 적용하는 경우는 HTTP 요청, 응답 둘 다 사용된다
- HTTP 요청 : @RequestBody, HttpEntity(RequestEntity)
- HTTP 응답 : @ResponseBody, HttpEntity(ResponseEntity)
HttpMessageConverter 기능에는 크게 2가지가 있는데
- canRead(), canWrite() : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크
- read(), write() : 메시지 컨버터를 통하여 메시지 읽고 쓰는 기능
메세지 컨버터 작동 순위도 있는데 다음과 같은 메시지 컨버터들은 HttpMessageConverter 인터페이스를 상속한 컨버터들이다.
- 0 순위 : ByteArrayHttpMessageConverter
- 1 순위 : StringHttpMessageConverter
- 2 순위 : MappingJackson2HttpMessageConverter
몇가지 주요한 메시지 컨버터가 있는데
ByteArrayHttpMessageConverter의 경우 byte[]를 처리하며 클래스타입이 byte[]이고 미디어타입이 */*인 경우 동작한다.
- Request 예시 : @RequestBody byte[] data
- Reponse 예시 : @ResponseBody return byte[] / 쓰기 미디어 타입은 application/octet-stream(자동 결정)
StringHttpMessageConverter의 경우 String문자를 처리하며 클래스타입은 String, 미디어타입은 */*인 경우 동작한다.
- Request 예시 : @RequestBody String data
- Response 예시 : @ResponseBody return "ok" / 쓰기 미디어 타입 text/plain
MappingJackson2HttpMessageConverter는 클래스타입은 객체나 JSON타입을 주로 처리하며 미디어타입은 application/json이다.
- Request 예시 : @RequestBody HelloData data
- Response 예시 : @ReponseBody return helloData / 쓰기 미디어 타입 application/json
요청데이터를 읽는 방법에 대한 정리를 다음과 같이 할 수 있다.
- HTTP 요청이 오고 컨트롤러에서 @RequestBody, HttpEntity 파라미터를 사용하는지 체크한다.
- 메시지 컨버터의 canRead()가 호출되어 메시지를 읽을 수 있는지 확인한다.
- 1)대상 클래스 타입을 확인 : byte[], String, HelloData
- 2)HTTP 요청의 Content-Type(미디어타입) 체크 : text/plain, application/json, */*
- canRead() 조건 만족시 read()를 호출하여 객체 생성하여 반환한다.
응답데이터를 읽는 방법에 대한 정리를 다음과 같이 할 수 있다.
- 컨트롤러 반환시 @ResponseBody, HttpEntity 반환타입 체크한다.
- 메시지 컨버터의 canWrite()가 호출되어 메시지를 쓸 수 있는지 확인한다.
- 1)대상 클래스 타입을 확인 : byte[], String, HelloData
- 2)HTTP 요청의 Accept 미디어타입 지원여부 확인(@RequestMapping의 produces)
- canWrite() 조건 만족시 write()를 호출하여 HTTP 응답메시지 바디에 데이터를 생성한다.
*/* : 아무거나 다 됨
'Spring' 카테고리의 다른 글
[Spring MVC] 로그인 처리 (1) - 쿠키, 세션 (1) | 2023.10.10 |
---|---|
[Spring MVC] 스프링 MVC(5) (0) | 2023.06.13 |
[Spring MVC] 스프링 MVC (3) (0) | 2023.05.15 |
[Spring MVC] 스프링 MVC (2) (0) | 2023.05.15 |
[Spring MVC] 스프링 MVC (1) (0) | 2023.05.09 |