728x90
1. 로깅 알아보기
- 실무에선 System.out.println() 사용하지 않고 로깅 라이브러리 사용해서 log 출력
- println을 사용하지 않는 이유
- log가 더 자세한 정보를 넘긴다.
- log는 level 설정이 가능해서 log를 선별해서 받을 수 있다. 반면 println은 선별해서 값을 받을 수 없어서 운영시스템이 더러워진다.
- 로깅 라이브러리
- 스프링 부트 라이브러리 사용시, 스프링 부트 로깅 라이브러리가 포함
- 인터페이스 : SLF4J 제공
- 구현체 : Logback, Log4J, Log4J2 등등 존재 -> spring boot는 Logback 주로 사용
- log 장점
- application.properties를 이용해서 관리할 수 있어서 로그 레벨에 따라 개별 서버에서 상황에 맞는 log 범위 설정이 가능
- 성능도 println 보다 좋다
- log 선언 종류
- private Logger log = LoggerFactory.getLogger(getClass());
- private static final Logger log = LoggerFactory.getLogger(Xxx.class);
- @Slf4j
package hello.springmvc.basic;
// 3가지를 다 보여줬는데 여기서 1개만 쓰면 된다.
@Slf4j
@RestController
public class LogTestController {
private final Logger log = LoggerFactory.getLogger(getClass()); // getClass() == LogTestController.class
private static final Logger log = LoggerFactory.getLogger(LogTestController.class);
@RequestMapping("/log-test")
public String logTest() {
String name = "Spring";
// 정보도 적고 level에 상관없이 항상 나오기 때문에 좋지 않다.
System.out.println("name = " + name);
/* log에 + 연산자를 사용하지 않는 이유
* properties의 log-level과 관계 없이 연산을 수행한 후 출력이 안됨
* 연산하는 리소스 비용이 발생
* */
log.trace("trace log={}" + name);
// 해당 log 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); // 에러
return "ok";
}
}
1.1. 로그 레벨 설정
- 로그 단계: TRACE > DEBUG > INFO > WARN > ERROR
- 로그 경로 설정: loging.level.root가 최상단
# 어느 패키지부터 log를 적용할지, 어떤 level의 log를 출력할지 설정
## root부터 info level의 log 출력 - info, warn, error 출력
logging.level.root=info
## springmvc 패키지부터 설정한 level의 log 출력 , 해당 level부터 상위 level까지 다 출력
logging.level.hello.springmvc=trace
logging.level.hello.springmvc=debug # 개발 서버
logging.level.hello.springmvc=info # 운영 서버
logging.level.hello.springmvc=warn
logging.level.hello.springmvc=error
1.2. 로그 사용 소스파일
- 로그 사용시 + 를 사용하지 않는 이유 : log.debug("data="+data) - 절대 안됨.
- java 특성상 + 연산부터 진행
- 출력되지 않는 log도 연산부터 하기 때문에 메모리, cpu 사용량이 증가
package hello.springmvc.basic;
@Slf4j
@RestController
public class LogTestController {
// getClass는 해당 class의 class type 의미
// private final Logger log = LoggerFactory.getLogger(getClass()); // @Slf4j와 동일 역할
// private final Logger log = LoggerFactory.getLogger(LogTestController.class); // @Slf4j와 동일 역할
@RequestMapping("/log-test")
public String logTest() {
String name = "Spring";
// 정보도 적고 level에 상관없이 항상 나오기 때문에 좋지 않다.
System.out.println("name = " + name);
/* log에 + 연산자를 사용하지 않는 이유
* properties의 log-level과 관계 없이 연산을 수행한 후 출력이 안됨
* 연산하는 리소스 비용이 발생
* */
log.trace("trace log={}" + name);
// 해당 log 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); // 에러
return "ok";
}
}
1.3. log 추가 공부 자료
- 로그에 대해서 더 자세한 내용은 slf4j, logback을 검색
- SLF4J - http://www.slf4j.org
- Logback - http://logback.qos.ch
- 스프링 부트가 제공하는 로그 기능은 다음을 참고
- 파일, 네트워크 등 로그를 별도 위치에 남길 수 있고 특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 기능도 가능하니 나중에 공부하기
2. 요청 매핑
2.1. 특이 케이스
- /hello-basic/ 이랑 /hello-basic/ 은 다른 url이지만 스프링은 같은 요청으로 매핑
- 배열로 다중 url 받아올 수 있다.
@Requestmapping({"/hello-basic", "/hello-go}) // 배열로 다중 설정이 가능
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
2.2. 핵심
- PathVariable
- 요즘 pathvariable을 많이 사용
- 다중 PathVariable 사용 가능
- Content-type 헤더
- consume - http 요청에서 온 data 정보 type이 consume의 type과 일치할때만 받을 것임
- Accept 헤더
- produce - http 응답으로 produce에서 작성한 type으로 보냄
- connsume, produce 설정 타입을 MediaType.상수 로 작성 권장
2.3. 요청 매핑 종류
package hello.springmvc.basic.requestmapping;
// restController는 view로 안가고 바로 browser에게 반환
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping(value = "/hello-basic")
public String helloBasic(){
log.info("helloBasic");
return "ok";
}
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
/**
* 편리한 축약 애노테이션 (코드보기)
* @GetMapping
* @PostMapping
* @PutMapping
* @DeleteMapping
* @PatchMapping
*/
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
/**
* PathVariable 사용
* 변수명이 같으면 생략 가능
* @PathVariable("userId") String userId -> @PathVariable userId * 이름과 params가 같으면 생략가능
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
/**
* PathVariable 사용 다중, 변수명이 같아서 생략
*/
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "ok";
}
/**
* 파라미터로 추가 매핑 - url에 params의 정보까지 있어야 controller에 접근이 가능, 요즘 거의 안씀
* params="mode",
* params="!mode"
* params="mode=debug"
* params="mode!=debug" (! = )
* params = {"mode=debug","data=good"}
*/
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "ok";
}
/**
* 특정 헤더로 추가 매핑
* 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 - 요청형태가 어떻게 생겼든지 해당 consumes type이면 ok
* spring에서는 Content-Type형태의 data를 소비한다고 Consumes라고 params명을 지정함
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
/**
* Accept 헤더 기반 Media Type - 반환 값이 어떻게 생겼든지 accept를 text/html로 보낸 것임
* 응답할 때 해당 조건의 data를 생성해서 보낸다는 의미로 produces
* produces = "text/html"
* produces = "!text/html"
produces = "text/*"
* produces = "*\/*"
*/
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
}
3. 요청 매핑 - API 예시
- 회원 관리를 http Api로 만든다고 생각하고 생성한 mapping 예시
package hello.springmvc.basic.requestmapping;
@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
@GetMapping
public String user(){
return "get users";
}
@PostMapping
public String addUser() {
return "post user";
}
@GetMapping("/{userId}")
public String findUser(@PathVariable String userId){
return "get userId=" + userId;
}
@PatchMapping("/{userId}")
public String updateUser(@PathVariable String userId){
return "update userId=" + userId;
}
@DeleteMapping("/{userId}")
public String deleteUser(@PathVariable String userId){
return "delete userId=" + userId;
}
}
4. HTTP 요청 - 기본, 헤더 조회
4.1. MultiValueMap
- 하나의 key에 여러 value를 배열로 받을 수 있다.
MultiValueMap<String, String> map = new LinkedMultiValueMap();
map.add("keyA", "value1");
map.add("keyA", "value2");
//[value1,value2]
List<String> values = map.get("keyA");
4.2. 요청 기본, 헤더 조회
- Servlet이 제공하는 method보다 더 좋은 method를 spring이 제공
- @CookieValue(value = "myCookie", required = false) String cookie
- 특정 쿠키를 조회
- 속성
- 필수 값 여부: required
- 기본 값: defaultValue
package hello.springmvc.basic.request;
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String header(HttpServletRequest request,
HttpServletResponse response,
HttpMethod httpMethod,
Locale locale,
@RequestHeader MultiValueMap<String, String> headerMap, // 모든 헤더 다 받을 때
@RequestHeader("host") String host, // 필수 헤더 받을 때, 헤더 key 값을 넣는다.
@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); // 모든 header 정보
log.info("header host={}", host); // 내가 설정한 헤더 정보 받기
log.info("myCookie={}", cookie);
return "ok";
}
}
5.HTTP 요청 데이터 조회
- 클라이언트에서 서버로 요청 데이터를 전달할 때 3가지 방법
- GET - 쿼리 파라미터
- /url?username=hello&age=20
- 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
- POST - HTML Form
- content-type: application/x-www-form-urlencoded 메시지 바디에 쿼리 파리미터 형식으로 전달
- username=hello&age=20
- HTTP message body에 데이터를 직접 담아서 요청
- HTTP API에서 주로 사용, JSON, XML, TEXT
- 데이터 형식은 주로 JSON 사용 POST, PUT, PATCH
- GET - 쿼리 파라미터
5.1. 제일 기본 형식
package hello.springmvc.basic.request;
@Slf4j
@Controller
public class RequestParamController {
@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"); // accept=text/plain, accept=application/json 둘다 허용
}
}
6. Http 요청 파라미터 - 쿼리 파라미터, HTML Form
6.1. summary
- query Params 형식의 요청을 처리하는 경우에 한해서 해당
- json 형식과 같은 message body로 받는 경우는 아래 경우가 적용되지 않는다.
- 애노테이션 종류 2가지
- @RequestParam
- @ModelAttribute
- 생략 조건
- 단순 타입(자료형) String, int, Integer -> @RequestParam 생략
- 나머지 객체 -> @ModelAttribute 생략
- argument resolver 지정 type은 생략 하면 X -> @ModelAttribute로 간주
6.2. @RequestParam
- 참고
- @ResponseBody: View 조회 무시하고 HTTP message body에 직접 해당 내용 입력
- @ResponseBody+@Controller = @RestController
- response.getWriter().write()와 거의 동일 작동
- 정리
- @RequestParam: 파라미터 이름으로 바인딩
- 사용
@RequestParam("username") String memberName- "username": 실제 request params의 key와 동일
- memberName: key의 value를 담는 변수명
- 애노테이션 생략 1
request params를 받는 name과 String 변수명이 동일할 경우
- @RequestParam("username") String username
- -> @RequestParam String username
- 애노테이션 생략 2
애노테이션 생략 1 조건 만족하고 request params를 받는 type이 기본형 일 경우 @RequestParam 생략 가능
- @RequestParam String username
- -> String username
- 사용
- Params 필수 여부
- @RequestParam(required = true) String username // true가 default
- client는 요청 params에 username을 필수로 넣어야함
- null vs ""
- localhost:8080/?age=123 : null이 들어가기 때문에 required true 인경우 예외 발생
- localhost:8080/?username=&age=123 : username이 null이 아닌 빈문자열로 입력. 주의 할 것
- @RequestParam(required = false) Integer age
- null을 받기 위해선 객체 형태인 Integer type이 와야함
- int 는 null을 받을 수 없기 때문에 사용 불가
- @RequestParam(required = true) String username // true가 default
- defaultValue
- required 필요 없음 -> 값 유무 상관없이 default값이 들어가기 때문,
- 빈문자("")도 default 값으로 치환
- @RequestParam(required = true, defaultValue = "guest") String username
- @RequestParam(required = false, defaultValue = "-1") int age
- params Map으로 조회하기
- request 모든 정보가 아닌 request의 params의 모든 정보를 Map으로 조회 가능
- 단일 params명일 경우
- username=a
- MultiValueMap 사용가능
- 중복 다중 params명일 경우 사용
- username=a&username=b
- @RequestParam Map<String, Object> paramMap -> paramMap.get("key")
- request 모든 정보가 아닌 request의 params의 모든 정보를 Map으로 조회 가능
- @RequestParam: 파라미터 이름으로 바인딩
package hello.springmvc.basic.request;
@Slf4j
@Controller
public class RequestParamController {
@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"); // accept=text/plain, accept=application/json 둘다 허용
}
@ResponseBody // @RestController 같이 view로 이동안하고 바로 값이 넘어간다.
@RequestMapping("/request-param-v2")
public String requestParamV2(@RequestParam("username") String memberName,
@RequestParam("age") int memberAge){
log.info("username={}, age={}", memberName, memberAge);
return "ok"; // accept=text/plain, accept=application/json 둘다 허용
}
@ResponseBody
@RequestMapping("/request-param-v3")
// request params로 부터 받는 key = name이 변수명과 동일할 때 생략가능
public String requestParamV3(@RequestParam String username,
@RequestParam int age){
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v4")
// 요청 params와 변수명이 동일해야 함 - 해당 경우는 기본형 type일 때만 @RequestParam이 생략 가능
public String requestParamV4(String username, int age){
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
// default가 true : 해당 request params가 꼭 들어와야함
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age){ // int는 안됨. false인 경우 null을 반환
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
// false에서 값이 없거나 true에서 값이 안들어오면 default가 넘어감, null 뿐만이 아니라 빈문자열도 defaultValue 적용한다.
@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age){
log.info("username={}, age={}", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-map")
// 해당 requestParams를 통채로 Map으로 가져온다. multiMap도 있음
public String requestParamMap(@RequestParam Map<String, Object> paramMap){
log.info("username={}, age={}", paramMap.get("username"),paramMap.get("age"));
return "ok";
}
6.3. @ModelAttribute
- 실제 개발시 요청 params를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어줘야 하는 경우 사용
6.3.1. HelloData
- params를 저장할 HelloData 객체
- 해당 변수명은 request params의 변수명과 동일해야 한다.
- @Data
- lombok 기능
- @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor 자동 적용
package hello.springmvc.basic;
import lombok.Data;
@Data
public class HelloData {
// helloData의 변수명과 실제 들어오는 request params의 key 명이 동일해야 잘 작동을 한다. json 또한 마찬가지
private String username;
private int age;
}
6.3.2. @ModelAttribute
- 객체 생성 및 해당 객체 변수에 값 넣는 역할
- @RequestParam과 동일하게 생략가능
- Spring 생략 규칙 : @RequestParam @ModelAttribute
- 단, argumentResolver로 지정된 타입은 생략 불가 ex)HttpServletResponse
- 원리
- @ModelAttribute 존재시 springboot가 HelloData 객체 생성
- HelloData 객체에서 프로퍼티 찾고(getter, setter) params 값을 입력한다 = 바인딩 한다.
- 예외
- 만약 int type 변수에 String type의 값을 넣게 되면 BindException 발생
@Slf4j
@Controller
public class RequestParamController {
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData){
log.info("username={}, age={}", helloData.getUsername(),helloData.getAge());
log.info("helloData={}",helloData); // ToString 때문에 가능
return "ok";
}
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData){ // @ModelAttribute 생략가능
log.info("username={}, age={}", helloData.getUsername(),helloData.getAge());
log.info("helloData={}",helloData);
return "ok";
}
}
- modelAttribute의 추가 기능
- @ModelAttribute가 method()영역에 존재시, 그 자체를 항상 view로 보낸다.
참고 글 : 2. thymeleaf - 스프링 통합과 폼 - validation 의 @BindingResult와 같이 쓰인다
참고 글 : 스프링 mvc2 - 2 검증 1 (Validation)
- @ModelAttribute가 method()영역에 존재시, 그 자체를 항상 view로 보낸다.
7. HTTP 요청 메시지 - HTTP message body에 데이터를 직접 담아서 요청(단순 text)
7.1. summary
- Http message body 조회 기능
- HttpEntity<>
지네릭스 사용 안할 시, object로 반환 - @RequestBody -> 생략 하면 안됨
spring이 요청 params 조회 애노테이션 생략으로 판단
- HttpEntity<>
- request params vs Http message body
- 요청 params 조회 기능
- @RequestParam
- @ModelAttribute
- Http message body 조회 기능
- HttpEntity<>
- @RequestBody
- 요청 params 조회 기능
- HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam , @ModelAttribute 를 사용할 수 없다.
- Post 형식의 form은 메시지 바디를 통해서 오는 쿼리파람 형식의 data로 @RequestParam , @ModelAttribute, HttpEntity<>, @RequestBody 모두 사용 가능하다.
하지만 form을 사용한다는 의미는 @RequestParam , @ModelAttribute를 이용한다는 의미니깐 정상인처럼 사용할 것.
7.2. 사용 방식
- v1
- InputStream 사용: request.getInputStream() - http message body의 내용을 byte type code로 읽어온다.
- InputStream 사용: request.getInputStream() - http message body의 내용을 byte type code로 읽어온다.
- v2
- java.io.InputStream, java.io.Reader: Http 요청 메시지 바디의 내용을 직접 조회
- java.io.OutputStream, java.io.Writer: Http 응답 메시지바디에 직접 결과 출력
- v3
- HttpEntity<>
- Http header, body 정보를 편리하게 조회
- 메시지 바디 정보 직접 조회 - 요청 params와 관련 X
- http 응답에도 사용가능 - 메시지 바디 정보 직접 반환
- 헤더 정보 포함 가능, view 조회 불가
- HttpEntity<> 상속 받은 class
- RequestEntity<>: HttpMethod, url 정보
- ResponseEntity<>: Http 상태 코드 설정 가능, 응답에 사용
- HttpEntity<>
- v4 (제일 많이 사용)
- @RequestBody
- http message body 정보를 편리하게 조회, body의 type 그대로 꺼내옴 - text는 text로 json은 json으로 꺼냄
- header정보 필요시
- HttpEntity형식을 사용하는 v3 형태로 코드를 작성하는 방법
- v4 방식을 유지하고 싶으면 @RequestHeader 사용
- @RequestBody
- 참고
- v3, v4의 HttpEntity, @RequestBody -> HttpMessageConverter 사용
- @ResponseBody도 http:MessageConverter 사용
- HttpMessageConverter: 'Http message body'를 읽어서 문자 or 객체로 변환해서 controller에게 전달해주는 것
package hello.springmvc.basic.request;
@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(HttpEntity<String> httpEntity) throws IOException{
String body = httpEntity.getBody();
log.info("messageBody={}", body);
return new HttpEntity<>("ok");
}
/*
public HttpEntity<String> requestBodyStringV3(RequestEntity<String> httpEntity) throws IOException{
String body = httpEntity.getBody();
log.info("messageBody={}", body);
return new ResponseEntity<>("ok", HttpStatus.CREATED);
}
*/
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) { // Integer도 가능
log.info("messageBody={}", messageBody);
return "ok";
}
}
8. HTTP 요청 메시지 - HTTP message body에 데이터를 직접 담아서 요청(JSON)
8.1. 참고 정보
- JSON
- 데이터 포맷일 뿐이며 어떠한 통신 방법도, 프로그래밍 문법도 아닌 단순히 데이터를 텍스트로 표시하는 표현 방법
- data를 표현하는 text 의 한 종류 -> 그래서 string으로 갈수도 있고 Json 형식으로 그대로 갈수 있다.
- new ObjectMapper()
- json data를 파싱해서 사용할 수 있는 java object로 변환하기 위한 Jackson library
- json <-> JAVA obejct (양방향 가능)
- objectMapper.readValue(messageBody, HelloData.class): json 형식의 string을 java 객체에 담는다.
- objectMapper.writeValueAsString(object): java 객체를 json 문자로 변경한다.
- 참고자료 : 스프링 MVC 1 - 서블릿 - 3.4.2 목차 내용
- JSON 형식 -> 객체 converter 조건 (http 요청 header)
- contept-type: application/json
- 객체 -> JSON 형식 converter 조건 (http 요청 header)
- Accept : application/json
// json -> 객체로 변환시 content-type: application/json 필수
public String requestBodyJsonV3(@RequestBody HelloData helloData) {
// 어떠한 body data -> String으로 변환시 content-type 상관 없음
public String requestBodyJsonV3(@RequestBody String messageBody) {
8.2. 사용방식 예제
- request body로 부터 오는 data가 json 형식이고 이를 객체로 변환해서 받는 방법
- v2: @RequestBody String messageBody(json을 string으로 받음)
- http body에서 json 문자를 그대로 꺼냄(String type으로 꺼낸다고 지정함)
json의 괄호까지 string으로 나옴 - 그리고 json문자를 다시 객체로 변환(ObjectMapper 이용)
- http body에서 json 문자를 그대로 꺼냄(String type으로 꺼낸다고 지정함)
- v3: @RequestBody HelloDatahelloData (json을 객체로 받음)
- @ModelAttribute처럼 한번에 객체로 변환 가능
- converter가 json 형식을 인지하고 이를 바로 객체로 변환 후 controller에게 반환
- v4: HttpEntity<변수로 받을 type>
- @RequestBody 처럼 HttpMessageConverter가 request header와 request body를 컨버터 해줌
- @RequestBody 처럼 HttpMessageConverter가 request header와 request body를 컨버터 해줌
- v5: 응답까지 json형태로 보낼때를 잠깐 보여주는 예제 : 반환타입이 HelloData
- @RequestBody 요청
json -> http 메시지 컨버터 -> 객체 - @ResponseBody 응답
객체 -> http 메시지 컨버터 -> json
- @RequestBody 요청
package hello.springmvc.basic.request;
/**
* content-type: application/json
*/
@Slf4j
@Controller
public class RequestBodyJsonController {
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);
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) {
log.info("messageBody={}", helloData);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> data) {
HelloData helloData = data.getBody();
log.info("messageBody={}", helloData);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return helloData; // http message converter를 통해서 바뀜
}
}
8.3. httpMessage body 요청, 응답 종류 정리
- HttpEntity, @RequestBody사용 시,
- HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 1) 문자나 2) 객체 등으로 변환
- @RequestBody는 생략 불가
생략시 -> @ModelAttribute로 작동 - 원하는 data 형식으로 받고 보낼 때 content type, accept 가 알맞게 설정 되어야 한다.
request body data 종류 | 세부 종류 | controller에서 해당 data 처리 방법 | response 보내는 방법 |
String | text | 자료형인 String, Integer로 받기 @RequestBody String / Integer |
string / json |
post form | - | - | |
Json | - | Object : JSON data의 경우 객체로 받기 @RequestBody HelloData |
string / json |
9. HTTP 응답 개요
9.1. summary
응답도 총 3가지 방식이 존재
- 정적 리소스
예) 웹 브라우저에 정적인 HTML, css, js를 제공할 때는, 정적 리소스를 사용한다.
- 뷰 템플릿 사용
예) 웹 브라우저에 동적인 HTML을 제공할 때는 뷰 템플릿을 사용한다. - HTTP 메시지 사용
HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.
9.2. 정적리소스
- 스프링 부트가 제공하는 정적리소스
- src/main/resources/static
- src/main/resources/public
- src/main/resources
- src/main/META-INF/resources
- 경로
- src/main/resources/static/basic/hello-form.html
- src/main/resources/static/basic/hello-form.html
- 실행 url
- http://localhost:8080/basic/hello-form.html
9.3. 뷰 템플릿
9.3.1. 기본 정보
- spring boot view template 특징
- webapp 경로 존재 안함
- 정적 리소스를 제외하곤 WEB-INF 처럼 controller를 통해서 접근이 가능
- html 파일 외에 css, js, img 등 정적 소스가 들어갈 수 있다.
- 스프링 부트가 제공하는 default 뷰 템플릿 경로
- src/main/resources/templates
- viewName의 물리적 경로
- 스프링부트가 자동으로 application.properties에 default값을 등록
- 필요시 수정 가능
# application.properties
# default 값이여서 실제 application.properties에 작성이 되어있지 않음
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
9.3.2. 뷰 템플릿 호출하는 컨트롤러
- v1
- viewName(논리 이름): "response/hello"
- Model 객체에 들어갈 params: "data", "hello!" -> ModelAndView 내부적으로 Map으로 변환함
- v2
- render에 같이 보낼 request.setAttribure를 Model로 담는다. - ModelAndView의 Model과 다른 것
- viewName을 string 리턴값으로 보낸다. - @ResponseBody @RestController가 없으므로 @RequestMapping의 return 값은 view 경로를 찾는다.
package hello.springmvc.basic.response;
@Controller
public class ResponseViewController {
// 1. modelAndView 반환
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1(){
ModelAndView mav = new ModelAndView("response/hello").addObject("data","hello!");
return mav;
}
// 2. string 반환
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model){
model.addAttribute("data", "hello");
return "response/hello";
// 경로는 defalute로 resources/templates + "논리 이름" + .html 설정
// 그리고 templates에 없으면 static에서 찾음
// static은 url 경로 작성시 접근가능하지만 templates는 url로 접근시 접근 불가
// static도 default로 resources/static + "논리 이름" + .html
}
// 권장 안함. 관례적으로 mapping url과 view의 논리 경로가 동일할 시 view로 이동 가능
@RequestMapping("/response/hello")
public void responseViewV3(Model model){
model.addAttribute("data", "hello!");
}
}
- thymeleaf
<!-- 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>
9.4. HTTP 응답 - HTTP API, 메시지 바디에 직접 입력
- 정적 리소스나 뷰 템플릿을 거치지 않고, 직접 HTTP 응답 메시지를 전달하는 경우 - 바디로 전달
- 정리
- text 반환
- json 반환
- 방법
- response.getWriter()
- ResponseEntity<>("보낼 data", 상태코드)
- @ResponseBody @ResponseStatus
package hello.springmvc.basic.response;
@Slf4j
@Controller
//@ResponseBody // class level에서도 작동 가능
//@RestController = @ResponseBody + @Controller
public class ResponseBodyController {
// 1. 문자열 처리
@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(){
return new ResponseEntity<>("ok", HttpStatus.OK);
}
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
// 2. 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);
}
@ResponseStatus(HttpStatus.OK) // responseEntity와 달리 상태코드 설정 못하기 때문
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2(){
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
10. Http 메시지 컨버터
10.1. 개요
- 참고
- argument Resolver 라는 것이 아래의 경우 http Message converter를 사용
- 이 외 모든 경우는 argument Resolver가 다 처리한다.
- handler adapter - argument resovler - http message converter - handler가 원하는 객체 생성 후 handler adapter 에게 객체 반환
- Spring MVC : Http 메시지 컨버터를 적용하는 경우
- http 요청 : @RequestBody, HTTPEntity(RequestEntity)
- http 응답 : @ResponseBody, HTTPEntity(ResponseEntity), @RestController = @ResponseBody + @Controller
- 위의 Http 메시지 컨버터를 적용하는 경우들은 http 메시지 컨버터를 이용해서 필요한 객체 ,String, byte 객체를 생성해서 dispatcherServlet에게 제공
10.2. http message converter
- Http 메시지 컨버터 method
- canRead(), canWrite() : 메시지 컨버터가 해당 1) 클래스타입,2) 미디어타입(content-type, accept) 지원하는지 체크
- read(), write() : 메시지 컨버터를 통해서 메시지 읽고 쓰는 기능 - 객체 생성
- spring boot http message converter 순위
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
- 클래스 타입, 미디어타입 지원 여부를 체크
- 클래스 타입
- 요청 : handler에서 params(arguments)로 받는 값의 type
- 응답 : return하는 값의 type
- 미디어 타입
- 요청 : content-type
- 응답 : accept
- 클래스 타입
- 스프링부트 기본 메시지 컨버터 종류
- ByteArrayHttpMessageConverter
- 요청: 클래스타입( byte[] ) 미디어타입( */* )
- 응답: 클래스타입( byte[] ) 미디어타입(application/octet-stream)
- StringHttpMessageConverter
- 요청: 클래스타입(String) 미디어타입( */* )
- 응답: 클래스타입( String ) 미디어타입(text/plain)
- MappingJackson2HttpMessageConverter
- 요청: 클래스타입(객체, HashMap) 미디어타입( application/json )
- 응답: 클래스타입(객체, HashMap) 미디어타입(application/json)
- ByteArrayHttpMessageConverter
// StringHttpMessageConverter 예시
// http request header 정보
미디어 타입: content-type: application/json
// 서버측 controller code
void hello(@RequestBody 클래스 타입: String data){}
// MappingJackson2HttpMessageConverter
// http request header 정보
미디어 타입: content-type: application/json
// 서버측 controller code
void hello(@RequestBody 클래스 타입: HelloData data){}
10.3. 정리
1. HTTP 요청 데이터 읽기
- HTTP 요청이 오고, 컨트롤러에서 @RequestBody , HttpEntity 사용
- 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead()호출 - byteConverter로 확인 없으면 stringConverter로 확인 없으면 jackson 확인
- 대상 클래스 타입을 지원하는가.
- 예) @RequestBody 의 대상 클래스 ( byte[] , String , HelloData )
- HTTP 요청의 Content-Type 미디어 타입을 지원하는가.
- 예) text/plain , application/json , */*
- 대상 클래스 타입을 지원하는가.
- canRead() 조건을 만족하면 read() 를 호출해서 객체 생성 or String 생성하고 반환.
2. HTTP 응답 데이터 생성
- 컨트롤러에서 @ResponseBody, HttpEntity 로 값이 반환
- 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 를 호출
- 대상 클래스 타입을 지원하는가.
- 예) return의 대상 클래스 ( byte[] , String , HelloData )
- HTTP 요청의 Accept 미디어 타입을 지원하는가.(더 정확히는 @RequestMapping 의 produces )
- 예) text/plain , application/json , */*
- 대상 클래스 타입을 지원하는가.
- canWrite() 조건을 만족하면 write() 를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성한다.
11. 요청 매핑 헨들러 어뎁터 구조
- 작동 방식
- 핸들러에서 요구하는 params(=argument)를 읽는다.
- 핸들러 어댑터에서 argument resolver를 이용해서 요구 params의 객체를 받아온다.
- 만약 10 chaper와 같은 경우는 Http Message Converter를 통해서 객체를 받아온다.
- 핸들러 어댑터는 받아온 arguments 객체를 가지고 핸들러를 작동시킨다.
- 요청 매핑 핸들러 어뎁터 위치
- 핸들러 어뎁터 중에서 @RequestMapping(=handler)을 처리하는 핸들러 어댑터인 requestMappingHandlerAdapter에 위치
- HandlerMethodArgumentResolver
- HttpServletRequest, Model, @RequestParam, @ModelAttribute, @RequestBody, @HttpEntity 등등
handler가 요구하는 params(=argument)를 처리 - 인터페이스이고 스프링이 30개 이상의 구현체를 제공한다.
- HttpServletRequest, Model, @RequestParam, @ModelAttribute, @RequestBody, @HttpEntity 등등
- requestMappingHandlerAdapter이 ArgumentResolver 를 호출해서 @RequestMapping 으로 구현된 handler가 필요로하는 다양한 params를 생성
- 동작방식
- RequestMappingHandlerAdapter가 @RequestMapping된 controller의 params를 확인한 후 HandlerMethodArgumentResolver로 넘겨준다.
- HandlerMethodArgumentResolver는 아래 2개 logic을 통해서 실제 객체를 생성
1) supportsParameter()를 호출해서 해당 params를 지원하는 체크
2) resolveArgument()를 호출해서 실제 객체를 handlerAdapter인 RequestMappingHandlerAdapter에게 전달 - 객체를 받은 handlerAdapter인 RequestMappingHandlerAdapter @requestMapping된 handler에게 해당 객체를 params==argument로 전달
- RequestMappingHandlerAdapter가 @RequestMapping된 controller의 params를 확인한 후 HandlerMethodArgumentResolver로 넘겨준다.
- 동작방식
- ReturnValueHandler: RequestMappingHandlerAdapter 랑 동일, 요청 data, 응답 data 차이점
정리
요청 params나 return 값은 디스패처 서블릿에 의해 정형화 되어있다. 각각의 케이스를 맞춤형으로 쓰려고 어뎁터가 존재하고 어댑터도 어느정도 정형화 되어있는데 요청의 경우 다양한 params를 다룰려고 argument resolver가 쓰이고 그중 @HttpRequestBody의 경우는 http 메시지 컨버터를 사용해서 params의 값을 만들어서 반환해줌
반대로 응답의 경우도 여러가지 값을 반환할 수 있는데 이를 1차적으로 핸들러 어댑터에 맞춤형으로 주기 위해서 return value handeler를 쓰고 그중 @HttpResponsebody의 경우 http 메시지 컨버터를 써서 1차적으로 정형화되가 만들어서 어댑터한테 준다. 이제 어뎁터는 최고 정형화된 디스패쳐 서블릿에 맞게 값을 만들어서 반환한다
출처 인프런 김영한의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술