*우선 로그에 대해 간단히 알아보자
우리는 평소에 System.out.println()같은 시스템 콘솔로 결과를 확인했다.
운영 시스템에서는 이러한 시스템 콘솔을 사용해서 출력하지 않고, 로그 라이브러리를 활용한다.
스프링 부트에서는 기본적으로 Logback,Log4J,Log4J2 등 수많은 라이브러리가 있는데
이를 통합해서 인터페이스로 제공하는 SLF4J 라이브러리를 기본으로 쓴다.
즉, SLF4J는 인터페이스이고, 구현체로 Logback 같은 라이브러리를 사용한다.
private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(XXX.class)
@Slf4j
@RestController
public class LogTestController {
private final 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";
}
}
보통 @Controller을 하면 반환 값이 String이면 뷰 이름으로 인식해서 뷰를 찾고 뷰를 렌더링한다.
@RestController의 경우, 반환 값으로 뷰를 찾는 것이 아니라 HTTP 메세지 바디에 바로 입력한다.
이는 추후에 @ResponseBody와 관련이 있다. 추후 설명하겠다!
해당 trace, debug, info,warn,error의 순서대로 로그 레벨이 설정되어있다.
기본 default 설정은 info이다.
만약 내가 로그 레벨을 더 높이고 싶다면 application.properties에서 logging.level.hello.springmvc=debug 이런식으로 설정해줄 수 있다.
log.debug("data=" + data);
log.debug("data={}", data);
두 코드의 출력은 방식은 동일해보인다. 하지만 다르다.
만약 내가 로그 출력 레벨을 debug 보다 아래인 info로 했다 가정해보자.
그럼에도 위의 + 연산자는 메모리상에서 연산이 처리가 되어서 시간이 발생한다.
그래서 매우 의미없고 메모리 낭비가 되는 코드이다.
하지만 아래의 코드는 아무 일도 발생하지 않는다.
@RequestMapping은 알다시피 해당 url이 오면 그걸 매핑해주는 역할을 한다.
하나 짚고 넘어가야할 부분은
@RequestMapping("/hello-basic") 과 @RequestMapping("hello-basic/")은 엄연히 다른 URL이지만
스프링에선 같은 요청으로 매핑한다.
또, @RequestMapping에서 method속성을 따로 지정해주지 않는다면 HTTP메소드와 무관하게 호출된다.
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/hello-basic")
public String helloBasic(){
log.info("helloBasic");
return "ok";
}
/**
* PathVariable 사용
* 변수명이 같으면 생략 가능
* @PathVariable("userId") String userId -> @PathVariable userId
*/
@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";
}
/**
* 파라미터로 추가 매핑
* 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="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";
}
}
이제 HTTP API를 어떻게 매핑하는지 봐보자.
- 회원 목록 조회 GET /users
- 회원 등록 POST /users
- 회원 조회 GET /users/{userId}
- 회원 수정 PATCH /users/{userId}
- 회원 삭제 DELETE /users/{userId}
@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
/**
회원 목록 조회: GET /users
회원 등록: POST /users
회원 조회: GET /users/{userId}
회원 수정: PATCH /users/{userId}
회원 삭제: DELETE /users/{userId}
**/
@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;
}
}
HTTP 헤더 정보를 조회)
@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 정도인 것 같다.
MultiValueMap은 MAP과 유사하지만, 하나의 키에 여러 값을 받을 수 있다.
KeyA=value1&keyA=value2 이렇게 된다면 MultiValueMap을 사용해야겠지.
*HTTP 요청 파라미터
HTTP 요청 메세지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법은 크게 3가지이다.
- GET - 쿼리 파라미터
/url?username=hello&age=20
메세지 바디 없이, url의 쿼리 파라미터에 데이터를 포함해서 전달
ex) 검색, 필터, 페이징 등에서 많이 사용하는 방식 - POST - HTML Form
content-type: application/x-www-form-urlencoded
메세지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age=20
ex) 회원 가입, 상품 주문, HTML Form 사용 - HTTP message body에 데이터를 직접 담아서 요청
HTTP API에서 주로 사용, JSON, XML, TEXT
데이터 형식은 주로 JSON 사용
POST,PUT,PATCH
@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");
}
v1의 코드를 보자. request와 response를 받고 getParameter을 활용해 원하는 값을 받아오고 있다.
하지만 request와 response는 큰 객체인데 우리가 원하는 것은 상당히 적다. 이를 개선하고 싶다!
@ResponseBody //@RestController와 같은 역할. view를 찾지 않고 바로 html로 전송.
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge) {
log.info("username={}, age={}", memberName,memberAge);
return "ok";
}
인자를 HttpServletRequest과 HttpServletResponse로 받는 것이 아니라, 아예 어노테이션인 @RequestParam으로 받아들이면서 바로 값을 초기화해버렸다. 아까 v1에서의 세 줄이 한번에 인자로 처리되었다. 매우 편리해보인다.
/**
* @RequestParam 사용
* HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
*/
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam String username,
@RequestParam int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
만약 파라미터의 이름이 변수 이름과 같으면 @RequestParam의 인자값을 생략할 수 있다.
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age){
log.info("username={}, age={}", username,age);
return "ok";
}
또, 만약 단순한 String, int 등의 타입이라면 @RequestParam도 생략이 가능하다.
이 부분은 매우 중요하다. 생략을 하게 되면 스프링 MVC는 내부에서 required = false로 처리해버린다. 즉, 꼭 필요하지는 않다는 것이다.
강사님은 여기까지 생략하는 것엔 부정적인 입장이다.
왜냐하면 @RequestParam이 있어야 이것이 명확하게 요청 파라미터에서 데이터를 읽는 것을 알 수 있기 때문이다.
/**
* @RequestParam.required
* /request-param-required -> username이 없으므로 예외
*
* 주의!
* /request-param-required?username= -> 빈문자로 통과
*
* 주의!
* /request-param-required
* int age -> null을 int에 입력하는 것은 불가능, 따라서 Integer 변경해야 함(또는 다음에 나오는
defaultValue 사용)
*/
@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";
}
@RequestParam의 인자 중에 required가 있다. required = true는 기본 default 설정이다. 즉, 무조건 있어야한다.
false를 하게 되면 필수가 아니라서 인자로 안 받을 수도 있다는 소리이다.
false라고 하면 값을 입력안받게 되서 null이 될 수 도 있다.
그런데 int는 원시 타입이라 null을 받을 수 없다. 그래서 이럴 땐 Integer 같은 object 형태로 받아야한다.
@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";
}
아니면 defaultValue를 설정해버리면 만약 값을 안받으면 해당 값으로 처리하겠다라는 의미이니 이럴 땐 null을 걱정하지 않아도 된다.
@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";
}
파라미터를 아예 이렇게 Map 형태로 받을 수 있다.
파라미터의 값이 한 개로 확실하다면 Map을 쓰면 되지만, 여러 개일 것 같다면 MultiValueMap을 사용하는 것이 맞다.
Http 요청 파라미터 - @ModelAttribute)
보통 우리가 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야한다.
@RequestParam String username;
@RequestParam int age;
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
스프링은 위의 코드를 자동화해주는 @ModelAttribute 기능을 제공한다.
우선 그 전에 요청 파라미터를 받을 객체를 가정해놓고 시작해보자.
@Data
public class HelloData{
private String username;
private int age;
}
@Data는 롬복 기능이다.
@Getter, @Setter, @ToString, @EqualsAndHashCode 등을 다 자동 적용해준다.
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData){
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData){
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
위의 코드에서 @ModelAttribute를 생략해도 정상적으로 작동하는 것을 볼 수 있다.
스프링에서
- String,int, Integer 같은 단순 타입 = @RequestParam이 생략했다 가정한다.
- 나머지 = @ModelAttribute가 생략했다 가정한다.
HTTP 요청 메세지 - 단순 텍스트)
HTTP message body는 데이터를 직접 담아서 요청한다.
주로 JSON 형식일 것이다.
요청 파라미터와 다르게, 메세지 바디를 통해 데이터가 직접 넘어오는 경우엔 @RequestParam, @ModelAttribute를 사용할 수 없다. 물론 HTML FORM 형식으로 전달 되는 경우 요청 파라미터로 인정이 되긴 하지만....
@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");
}
아까와 마찬가지로, request와 response 전부가 필요는 없다.
@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");
}
InputStream(Reader) 의 경우 HTTP 요청 메세지 바디의 내용을 직접 조회하고
OutputStream(Writer)의 경우 HTTP 응답 메세지의 바디에 직접 결과를 출력하는 역할을 한다.
@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");
}
이러한 것 다 필요없고 그냥 HttpEntity를 받는 경우도 있다.
메세지 바디 정보를 직접 조회할 수 있다.
반환 할 때도 메세지 바디 정보를 직접 반환할 수 있다.
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV3(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
return "ok";
}
v3의 경우는 객체를 받아서 string으로 전환하는 작업이 필요했다.
v4의 경우는 @RequestBody를 통해서 메세지 바디 정보를 조회했다.
정리하자면
요청 파라미터를 조회하는 기능은 @RequestParam과 @ModelAttribute를 사용하고
http 메세지 바디를 조회하는 기능은 @RequestBody를 사용하면 된다.
HTTP 요청 메세지 - 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());
}
문자열로 된 JSON 데이터를 objectMapper를 사용해서 자바 객체로 변환하고 있다.
하지만 해당 request와 response를 인자로 줄 필요가 없어 보인다.
@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";
}
아까 배웠던 @RequestBody를 활용할 수 있다.
여기서, 문자로 변환하고 다시 json으로 변환하는 과정이 불편해보인다. 한번에 객체로 변환시킬 수 있는 방법이 필요해보인다.
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
아예 @RequestBody에 직접 만든 객체를 저장해버리는 것이다.
HttpEntity나 @RequestBody를 사용하면 HTTP메세지 컨버터라는 녀석이 바디의 내용을 우리가 원하는 문자나 객체로
변환해준다.
여기서 @RequestBody는 생략이 불가하다.
아까 내용에서 @RequestParam 또는 @ModelAttribute만 생략이 가능하다 했다.
따라서 여기서 @RequestBody를 생략하면 객체니까 @ModelAttribute로 스프링이 변환시켜 버려서 오류가 난다.
@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";
}
아니면 아까처럼 HttpEntity를 활용해도 된다.
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
반환 값을 아예 객체로 지정해줄 수 있다.
이렇게 되면
@RequestBody 요청
: JSON요청 -> HTTP메세지컨버터 -> 객체
@ResponseBody 응답
: 객체 -> HTTP메세지컨버터 -> JSON 이 된다.
HTTP 응답 - 정적 리소스, 뷰 템플릿)
- 정적 리소스
정적인 html,css,js - 뷰 템플릿 사용
동적인 HTML을 제공할 때 뷰 템플릿 사용 - HTTP 메세지 사용
데이터를 전달해야 하므로, 메세지 바디에 JSON 형식의 데이터 전송
정적 리소스의 경우
/static, /public, /resources/, META-INF/resources 에 있을 경우 정적으로 제공된다.
뷰 템플릿을 거쳐서 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>
<p th:text="${data}">empty</p>
해당 부분에서 text는 empty 저 부분을 가리킨다.
즉 empty부분에 data를 채워넣겠다라는 소리다.
@Controller
public class ResponseViewController {
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1(){
ModelAndView mav = new ModelAndView("response/hello")
.addObject("data", "hello!");
return mav;
}
@RequestMapping("/response-view-v2")
public String responseViewV1(Model model){
model.addAttribute("data", "hello!");
return "response/hello";
}
}
@ResponseBody가 없으면 response/hello로 뷰 리졸버가 실행되어서 뷰를 찾고 렌더링한다.
HTTP 응답 - HTTP API, 메세지 바디에 직접 입력)
@Slf4j
//@Controller
//@ResponseBody
@RestController
public class ResponseBodyController {
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok");
}
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2(){
return new ResponseEntity<>("ok", HttpStatus.OK);
}
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3(){
return "ok";
}
@GetMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1(){
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
// 만약에 상황에 따라 동적으로 HttpStatus를 지정해주려한다면 ResponseEntity<>를 사용
return new ResponseEntity<>(helloData, HttpStatus.OK);
}
//ResPonseBody를 사용할 경우 ResponseStatus를 지정할 수 없어 @ResponseStatus 사용
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2(){
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
responseBodyV1
서블릿을 직접 다룰 때처럼
HttpServletResponse 객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지를 전달한다.
responseBodyV2
ResponseEntity 엔티티는 HttpEntity 를 상속 받았는데, HttpEntity는 HTTP 메시지의 헤더, 바디 정보를 가지고 있다. ResponseEntity 는 여기에 더해서 HTTP 응답 코드를 설정할 수 있다.
responseBodyV3
@ResponseBody 를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력할 수 있다. ResponseEntity 도 동일한 방식으로 동작한다.
responseBodyJsonV1
ResponseEntity 를 반환한다. HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환된다.
responseBodyJsonV2
ResponseEntity 는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody 를 사용하면 이런 것을 설정하기 까다롭다. @ResponseStatus(HttpStatus.OK) 애노테이션을 사용하면 응답 코드도 설정할 수 있다. 물론 애노테이션이기 때문에 응답 코드를 동적으로 변경할 수는 없다. 프로그램 조건에 따라서 동적으로 변경하려면 ResponseEntity 를 사용하면 된다.
@RestController
@Controller 대신에 @RestController 애노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody 가 적용되는 효과가 있다. 따라서 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력한다. 이름 그대로 Rest API(HTTP API)를 만들 때 사용하는 컨트롤러이다. 참고로 @ResponseBody 는 클래스 레벨에 두면 전체 메서드에 적용되는데, @RestController 에노테이션 안에 @ResponseBody 가 적용되어 있다
'스프링 강의 필기 > 스프링 MVC 1편' 카테고리의 다른 글
7) 스프링 MVC - 웹 페이지 만들기 (0) | 2023.01.27 |
---|---|
5) 스프링 MVC - 구조 이해 (0) | 2022.07.30 |
4) MVC 프레임워크 만들기 (0) | 2022.07.30 |
3) 서블릿, JSP, MVC 패턴 (0) | 2022.07.27 |
1) 웹 애플리케이션 이해 (0) | 2022.07.22 |