운영시스템에서는 System.out.println()같은 시스템 콘솔에서 필요한 정보를 출력하지 않고, 별도의 로깅 라이브러리를 사용해서 로그를 출력한다.
로깅 라이브러리
로그 선언
private Logger log = LoggerFactory.getLogger(get.class());private static final Logger log = LoggerFactory.getLogger(xxx.class)@Slf4j : 롬복 사용(위의 코드를 자동으로 반영한다)로그 호출
LogTestController
@RestController
public class LogTestController {
private Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/log-test")
public String LogTest(){
String name = "Spring";
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";
}
}
로그 레벨 설정
application.properties

올바른 로그 사용법
log.debug("data="+data) :log.debug("data = {}", data) : 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않는다. 따라서 앞과 같은 의미없는 연산이 발생하지 않는다. (debug 메서드를 호출하면서 파라미터만 넘기기 때문에 아무 연산이 일어나지 않는다.)로그 사용시 장점
요청 매핑이란, 요청이 왔을 떄, 어떤 컨트롤러가 호출되어야 하는지를 매핑하는 것을 의미한다. 단순히 URL을 가지고 매핑하는 것 뿐만 아니라, 여러가지 요소들을 조합해서 매핑할 수 있다.
MappingController에서 다양한 매핑을 살펴보자
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/hello-basic")
public String helloBasic(){
log.info("helloBasic");
return "OK";
}
}
매핑정보
@RestController : @Controller는 반환값이 String이면, 뷰 이름으로 인식한다. 그래서 뷰를 찾고, 뷰를 렌더링한다.
--> @RestController는 반환값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다. 따라서 실행 결과로 "OK" 메시지를 받을 수 있다.
@RequestMapping("/hello-basic")
: /hello-basic URL이 호출이 되면, 이 메서드가 실행이 되도록 매핑한다.
HTTP 메서드 매핑
PathVariable(경로 변수) 사용
: 요청 URL 자체에 값이 들어갈 수 있는 것(경로 변수)
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data){
log.info("mapping userId = {}", data);
return "OK";
}


@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable String userId){
log.info("mapping userId = {}", userId);
return "OK";
}
PathVariable 사용 - 다중 매핑
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId){
log.info("mapping userId = {}, orderId = {}", userId, orderId);
return "OK";
}


특정 파라미터 조건 매핑
쿼리 파라미터를 조건에 매핑할 수 있다.
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam(){
log.info("mappingParma");
return "OK";
}

특정 헤더 조건 매핑
: 파라미터 매핑과 비슷하지만, HTTP 헤더를 사용한다.
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader(){
log.info("mappingHeader");
return "OK";
}
미디어 타입 조건 매핑 - HTTP요청 Content - Type, consume
: HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑한다.
( 만약 Content-Type에 따른 조건(application/json인지 text/html인지 등)을 넣고 싶은 경우, consume을 사용해야 한다.
@GetMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)
public String mappingConsume(){
log.info("mappingConsumes");
return "OK";
}

(참고) consumes 예시
consumes = "text/plain"
consumes = {"text/plain", "application/*"}
consumes = MediaType.TEXT_PLAIN_VALUE
미디어 타입 조건 매핑 - HTTP 요청 Accept, produce
@PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)
public String mappingProduces(){
log.info("mappingProduces");
return "OK";
}
(참고) produces 예시
produces = "text/plain"
produces = {"text/plain", "application/*"}
produces = MediaType.TEXT_PLAIN_VALUE
produces = "text/plain;charset=UTF-8"
회원 관리 API
회원 목록 조회: GET(/users)
회원 등록 : POST(/users)
회원 조회: GET(/users/{userId})
회원 수정 : PATCH(/users/{userId})
회원 삭제 : DELETE(/userss/{userId})
회원 (목록 조회 / 등록), 회원 (조회 / 수정 / 삭제)
--> URL은 똑같이 제공하고, HTTP Method로 행위를 구분하였다.
MappingClassController
@RequestMapping("/mapping/users")
public class MappingClassController {
//1. 회원 목록 조회: GET ( /users )
@GetMapping
public String user(){
return "get users";
}
//2. 회원 등록: POST ( /users )
@PostMapping
public String addUser(){
return "post user";
}
//3 . 회원 조회: GET ( /users/{userId} )
@GetMapping("/{userId}")
public String findUser(@PathVariable String userId){
return "get userId = " + userId;
}
//4. 회원 수정: PATCH ( /users/{userId} )
@PatchMapping("/{userId}")
public String updateUser(@PathVariable String userId){
return "update userId = " + userId;
}
//5. 회원 삭제: DELETE ( /users/{userId} )
@DeleteMapping("/{userId}")
public String deleteUser(@PathVariable String userId){
return "delete useId = " + userId;
}
}
스프링 MVC가 제공하는 기본, 헤더 조회에 대해서 알아보자
--> 애노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원한다.
RequestHeanderController
@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";
}
}

MultiValueMap

**@Slf4j
log라고 사용하면 된다. private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RequestHeaderController.class);클라이언트에서 서버로 요청 데이터를 전달할 떄는 주로 다음 3가지 방법을 사용한다
GET - 쿼리 파라미터
POST-HTML Form
HTTP message body에 데이터를 직접 담아서 요청
요청 파라미터 - 쿼리 파라미터, HTML Form
HttpServletRequest의 request.getParameter()를 사용하면 다음 두가지 요청 파라미터를 조회할 수 있다.
http://localhost:8080/request-param?username=hello&age=20
GET 쿼리파라미터 전송 방식이든, POST HTML Form 전송 방식이든 둘다 형식이 같으므로 구분없이 조회할 수 있다.
--> 요청 파라미터 (request parameter)조회차고 한다.
RequestParamController
@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");
}
}

Post Form 페이지 생성
먼저 테스트용 HTML Form을 만들어야 한다.
--> 리소스는 /resources/static 아래에 두면 스프링 부트가 자동으로 인식한다. (/resources/static 은 외부에 공개되는 경로이다.)

스프링이 제공하는 @RequestParam을 사용하면 요청 파라미터를 매우 편리하게 사용할 수 있다
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(@RequestParam("username") String username,
@RequestParam("age") int age) {
log.info("username = {}, age = {}", username, age);
return "OK";
}
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(@RequestParam String username,
@RequestParam int age) {
log.info("username = {}, age = {}", username, age);
return "OK";
}
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(@RequestParam String username, int age){
log.info("username = {}, age = {}", username, age);
return "OK";
}
파라미터 필수 여부 requestParamRequired
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age){
log.info("username = {}, age = {}", username, age);
return "OK";
}
/request-param?username = 으로 요청하게 되면, 빈문자 형태 ""로 null이 아니라 빈문자 형태로 출력하게 된다.기본 값 적용 - requestParamDefault
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age){
log.info("username = {}, age = {}", username, age);
return "OK";
}
파라미터를 Map으로 조회하기
@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";
}
실제 개발을 하다보면, 요청 파라미터를 받아서 필요한 객체를 마들고, 그 객체에 값을 넣어주어야 한다.
요청 파라미터를 바인딩 받을 객체를 만들고, @ModelAttribute를 적용해보자
@Data
public class HelloData {
private String username;
private int age;
}
* @Data(롬복) : @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 자동으로 적용해준다.
modelAttributeV1
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData){
log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
return "OK";
}
스프링 MVC는 @ModelAttribute가 있으면 다음을 실행한다.
modelAttributeV2
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData){
log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
return "OK";
}
@ModelAttribute는 생략할 수 있다. 하지만, @RequestParam도 생략할 수 있어 혼란이 발생할 수도 있다.
HTTP message body에 데이터를 직접 담아서 요청하는 경우
가장 단순한 텍스트 메시지를 HTTP 메시지 바디에 담아서 전송하고 읽어보자
(HTTP메시지 바디의 데이터를 InputStream을 사용해서 직접 읽을 수 있다)
requestBodyString
@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");
}
}
requestBodyStringV2
@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");
}
스프링 MVC는 다음 파라미터를 지원한다
requestBodyStringV3
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity){
String messageBody = httpEntity.getBody();
log.info("messageBody = {}", messageBody);
return new HttpEntity<>("OK");
}
스프링 MVC는 다음 파라미터를 지원한다
@PostMapping("/request-body-string-v3-v1")
public HttpEntity<String> requestBodyStringV3V1(RequestEntity<String> httEntity){
String messageBody = httEntity.getBody();
log.info("messageBody = {}", messageBody);
return new ResponseEntity<>("OK", HttpStatus.CREATED);
}
requestBodyStringV4
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody){
log.info("messageBody = {}",messageBody);
return "OK";
}
@RequestBody
요청 파라미터 조회 VS HTTP 메시지 바디 조회
HTTP API에서 주로 사용하는 JSON 데이터 형식을 조회해보자
requestBodyJsonV1
@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");
}
objectMapper를 사용해서 자바 객체로 변환한다. @RequestBody - requestBodyJsonV2
@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";
}
requestBodyJsonV3 (@RequestBody 객체 변환)
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) throws IOException{
log.info("messageBody = {}", data);
log.info("username = {}, age = {}", data.getUsername(),data.getAge());
return "OK";
}
@RequestBody 파라미터 :
@RequestBody는 생략 불가능
requestBodyJsonV4 - HttpEntity
@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";
}
requestBodyJsonV5
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData helloData){
log.info("messageBody = {}",helloData);
log.info("username = {}, age = {}",helloData.getUsername(),helloData.getAge());
return helloData;
}
@ResponseBody
정리
- @RequestBody 요청
--> JSON요청 -> HTTP 메시지 컨버터 -> 객체- @ResponseBody 요청
--> 객체 -> HTTP 메시지 컨버터 -> JSON 응답
스프링(서버)에서 응답 데이터를 만드는 방법은 크게 3가지이다.
정적 리소스
뷰 템플릿 사용
HTTP 메시지 사용
뷰 템플릿 생성
1. 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>
2. ResponseViewController - 뷰 템플릿을 호출하는 컨트롤러
@Controller
public class ResponseViewController {
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1(){
ModelAndView mav = new ModelAndView("response/hello");
mav.addObject("data","hello!");
return mav;
}
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model){
model.addAttribute("data", "hello!");
return "response/hello";
}
}
String을 반환하는 경우 - View or HTTP 메시지
@Response가 없으면, response/hello로 뷰 리졸버가 실행되어서 뷰로 찾고, 렌더링한다.
@ResponseBody가 있으면, 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 response/hello라는 문자가입력된다.
여기서 뷰의 논리 이름인 response/hello를 반환하면 뷰 템플릿이 렌더링 되는 것을 확인할 수 있다!
HTTP API를 제공하는 경우에넌 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.
HTML이나 뷰 템플릿을 사용해도 그것도 결과적으로는 HTTP 응답 메시지 바디에 HTML 데이터가 담겨서 전달된다. 여기서 설명하는 내용은 정적 리소스나 뷰 템플릿을 거치지 않고, 직접 HTTP 응답 메시지를 전달하는 경우를 말한다.
responseBodyV1 메서드
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("OK");
}
response.getWriter().write("OK")responseBodyV2 메서드
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2(){
return new ResponseEntity<>("OK", HttpStatus.OK);
}
responseBodyV3 메서드
@GetMapping("/response-body-string-v3")
@ResponseBody
public String ResponseBodyV3(){
return "OK";
}
ResponseBodyJsonV1 메서드
@GetMapping("/response-body-string-json-v1")
public ResponseEntity<HelloData> ResponseBodyJsonV1(){
HelloData helloData = new HelloData();
helloData.setUsername("이서연");
helloData.setAge(25);
return new ResponseEntity<>(helloData,HttpStatus.OK);
}
responseBodyJsonV2 메서드
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-string-json-v2")
public HelloData ResponseBodyJsonV2(){
HelloData helloData = new HelloData();
helloData.setUsername("이서연");
helloData.setAge(25);
return helloData;
}
@RestController
뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라, HTTP API 처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우, HTTP 메시지 컨버터를 사용하면 편리하다.
@ResponseBody 사용 원리

스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용한다
HTTP 메시지 컨버터 인터페이스
HTTP 메시지 컨버터는 HTTP 요청, HTTP 응답 둘 다 사용된다
스프링 부트 기본 메시지 컨버터 (일부 생략)
스프링 부트는 기본적으로 메시지 컨버터를 스프링 부트가 올라올 때 몇가지를 등록해둔다.
0 = ByteArrayHttpMessageConverter (바이트로 변환)
1 = StringHttpMessageConverter (스트링으로 변환)
2 = MappingJackson2HttpMessageConverter (객체 -> Json or Json -> 객체 변환)
스프링 부트는 다양한 메시지 컨버터를 제공하는데, 대상 클래스 타입과 미디어 타입 둘을 체크해서 사용여부를 결정한다. 만약 만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어간다.
ByteArrayHttpMessageConverter : byte[] 데이터를 처리한다.
클래스 타입: byte[] , 미디어타입: / (아무 미디어 타입이나 다 받아들일 수 있다.)
요청 예) @RequestBody byte[] data
응답 예) @ResponseBody return byte[]
위와 같이 응답하면 HTTP 응답 미디어 타입이 application/octet-stream 으로 반환된다. ( 쓰기 미디어타입 application/octet-stream )
StringHttpMessageConverter : String 문자로 데이터를 처리한다
클래스 타입: String , 미디어 타입: /
요청 예) @RequestBody String data
응답 예) @ResponseBody return "ok"
위와 같이 응답하면 HTTP 응답 미디어 타입이 text/plain 으로 반환된다. ( 쓰기 미디어타입 text/plain )
MappingJackson2HttpMessageConverter: application/json을 주로 처리
클래스 타입: 객체 또는 HashMap , 미디어타입 application/json 관련
요청 예) @RequestBody HelloData data
응답 예) @ResponseBody return helloData
위와 같이 응답하면 HTTP 응답 미디어 타입이 application/json 관련 정보로 반환된다. ( 쓰기 미디어타입 application/json 관련 )
HTTP 요청 데이터 읽기
HTTP 응답 데이터 생성
(예시) 


HTTP 메시지 컨버터는 스프링 MVC의 어디쯤에서 사용되는 걸까?
모든 비밀은 애노테이션 기반의 컨트롤러, @RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter(요청 매핑 핸들러 어뎁터) 에 있다
RequestMappingHandlerAdaper 동작 방식
(참고)
Argument Resolver
애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다. HttpServletRequest , Model 은 물론이고, @RequestParam , @ModelAttribute 같은 애노테이션 그리고 @RequestBody , HttpEntity 같은 HTTP 메시지를 처리하는 부분까지 매우 큰 유연함을 보여주었다.
-> 유연하게 처리할 수 있는 이유가 바로 Argument Resolver이다.
정확히는 HandlerMethodArgumentResolver 인데 줄여서 ArgumentResolver 라고 부른다.
동작 방식
ReturnValueHandler
파라미터로 넘어가는 것은 ArgumentResolver가 생성하는 것을 확인했다.
그러면 HTTP 메시지 컨버터는 어디에서 동작할까 ? HTTP 메시지 컨버터는 어디쯤 있을까?
HTTP 메시지 컨버터 위치

요청의 경우 @RequestBody 를 처리하는 ArgumentResolver 가 있고, HttpEntity 를 처리하는 ArgumentResolver 가 있다. 이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용(이전에 봤던 주요 3개 메시지 컨버터 동작 방식 참고)해서 필요한 객체를 생성하는 것이다.
응답의 경우 @ResponseBody 와 HttpEntity 를 처리하는 ReturnValueHandler 가 있다. 그리고 여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.