스프링 MVC-스프링 MVC 요청과 응답
스프링의 로깅
시작하기 전에 스프링에서 로깅을 어떻게 하는지에 대해 간단히 알아보고 가자. 기본적으로 스프링에서 로깅은 다음과 같이 사용한다.
private final Logger log = LoggerFactory.getLogger(getClass());
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);
위와 같이 LoggerFactory 클래스에서 getLogger() 메서드로 Logger 객체를 받아와 사용하는데, lombok의 @Slf4j 어노테이션을 사용하면 자동으로 log라는 이름의 Logger 객체를 생성해주므로 편리하게 사용할 수 있다.
로깅은 위와 같이 5가지 단계로 나뉘는데, application.properties 파일에서 설정값을 변경하여 어느 단계의 로그부터 볼 것인지를 설정할 수 있다.
이를 통해 개발 서버에서는 모든 로그를 보고, 운영 서버에서는 중요한 warn, error 로그부터 보는 등의 사용이 가능하다.
@RequestMapping
우리는 지금까지 @RequestMapping 어노테이션을 통해 컨트롤러에 대한 접근을 맵핑했었다. 스프링에는 HTTP 메서드, PathVariable, 파라미터 등 여러 조건을 더해 매핑이 가능하다. 아래의 예시를 통해 하나씩 알아보자.
HTTP 메서드 매핑
/**
* 편리한 축약 애노테이션 (코드보기)
* @GetMapping
* @PostMapping
* @PutMapping
* @DeleteMapping
* @PatchMapping
*/
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
위와 같이 @RequestMapping 대신 원하는 HTTP 메서드의 어노테이션을 사용하면 해당 메서드로만 컨트롤러에 접근이 가능하도록 맵핑할 수 있다. 해당 메서드가 아닌 메서드로 접근하면 HTTP 405 상태메세지를 반환한다.
PathVariable 사용
최근 HTTP API는 URI에 식별자를 넣는 스타일이 선호된다. 따라서 PathVariable을 사용하면 Request에서 따로 식별자를 파싱해오는 일 없이 손쉽게 식별자를 가져올 수 있다.
/**
* 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";
}
PathVariable로 설정한 userId, orderId를 URI로부터 받아오는 코드이다.
파라미터, 헤더, 미디어 타입
HTTP 요청의 파라미터, 헤더, 미디어 타입 등의 조건으로도 맵핑이 가능하다. 내부 코드는 대부분 비슷하므로 어노테이션만 가지고 비교해보도록 하겠다.
@GetMapping(value = "/mapping-param", params = "mode=debug")
@GetMapping(value = "/mapping-header", headers = "mode=debug")
@PostMapping(value = "/mapping-consume", consumes = "application/json")
@PostMapping(value = "/mapping-produce", produces = "text/html")
위의 두 개, 파라미터와 헤더 맵핑에 대해선 쉽게 이해할 수 있을 것이다. 하지만 아래의 미디어 타입 매핑 두 가지엔 큰 차이가 있다.
위의 consumes를 사용하는 미디어 타입 맵핑은 서버가 HTTP 요청을 읽고 요청의 헤더에 있는 Content-Type이 맞지 않으면 HTTP 상태코드 415를 반환한다.
하지만 아래의 produces를 사용하는 맵핑은 클라이언트가 서버의 응답을 받아들일 수 있는지를 HTTP 요청에 들어온 Accept 헤더를 읽고 판단한다.
Accept 헤더에 해당하는 컨텐츠 타입이 없다면, 즉 클라이언트가 서버가 생산한 컨텐츠 타입을 받아들일 수 없다면 HTTP 상태코드 406을 반환한다.
헤더 조회
스프링에서 HTTP 요청의 헤더를 조회하는 데에는 여러 방법이 있다. 아래의 코드를 통해 알아보자.
@RequestMapping("/headers")
public String headers(HttpServletRequest request,
HttpServletResponse response,
HttpMethod httpMethod, //HTTP 메서드를 조회한다.
Locale locale, //Locale(국가 등) 정보를 조회한다.
@RequestHeader MultiValueMap<String, String> headerMap, //모든 헤더를 조회한다.
@RequestHeader("host") String host, //특정 헤더를 조회한다.
@CookieValue(value = "myCookie", required = false) String cookie //특정 쿠키를 조회한다.
) {}
파라미터와 어노테이션을 통해 헤더의 모든 정보를 쉽게 가져올 수 있는 것을 볼 수 있다.
HTTP 요청
맵핑, 헤더 조회에 대해 알아봤으니 이제 HTTP 요청에서 보내는 진짜 데이터를 조회하는 방법을 알아보자.
이전에 배웠듯이 HTTP 요청이 서버에 데이터를 전달하는 방식은 크게 세 가지로 나눌 수 있다.
- GET - 쿼리 파라미터
- POST - HTML Form
- HTTP message body
위의 두 가지는 똑같이 쿼리 파라미터를 사용하므로 같은 방법으로 조회할 수 있다.
쿼리 파라미터 조회
서블렛을 통해서도 조회가 가능했지만, 이제 스프링 MVC의 @RequestParam 어노테이션을 사용하면 손쉽게 조회가 가능하다.
@RequestMapping("/headers")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge) {}
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age) {}
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap) {}
파라미터의 이름을 @RequestParam의 매개변수로 넣어주면 value를 파라미터로 반환해준다. 또한 아래와 같이 쿼리 파라미터의 이름과 변수 이름이 같다면 어노테이션의 매개변수를 생략하는 것도 가능하다.
primitive 타입이라면 아예 어노테이션 자체도 생략이 가능하지만 가독성이 불분명해지므로 지양하자.
또한 어노테이션의 매개변수로 필수값인지를 나타내는 required, 기본 값을 나타내는 defaultValue를 설정해줄 수도 있다. 위에서 헤더를 모두 조회할 때 사용했던 MultiValueMap 또는 Map을 이용하여 파라미터 값을 모두 조회하여 Map으로 받아올 수도 있다.
파라미터를 통한 객체 생성
원래는 쿼리 파라미터를 받아와서 내가 필요한 객체에 값을 직접 입력하여 생성해줘야 했다. 하지만 이젠 스프링 MVC의 @ModelAttribute 어노테이션이 해당 기능을 자동화하여 수행한다.
package hello.springmvc.basic;
import lombok.Data;
@Data
public class HelloData {
private String username;
private int age;
}
먼저 파라미터 데이터를 넣어서 만들어줄 객체의 코드이다. 이전과 같이 username과 age가 있다. lombok의 @Data 어노테이션으로 getter와 setter가 자동으로 입력되어있다. getter와 setter가 무조건 있어야한다.
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(),
helloData.getAge());
return "ok";
}
@ModelAttribute 어노테이션을 사용한 코드이다. 기존에는 파라미터를 파싱해서 객체에 직접 넣어줘야 했지만, 이렇게 하면 마법같이 객체에 필요한 파라미터가 모두 입력되어 있다.
@ModelAttribute는 다음과 같이 작동한다.
- 어노테이션이 붙은 HelloData 객체를 생성한다.
- 쿼리 파라미터의 이름(username,age)으로 객체 내부의 프로퍼티를 찾는다.
- 해당 프로퍼티의 setter를 호출하여 값을 입력한다.
@ModelAttribute도 생략이 가능하지만, primitive 타입이 아닐때만 가능하다.
HTTP body 조회
이제 쿼리 파라미터가 아닌 HTTP 요청의 body에 담긴 데이터를 읽어보자. 기존에는 서블렛의 InputStream을 사용하여 조회했었다.
이제 스프링 MVC에서는 두 가지 방법을 추가로 지원한다.
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
HttpEntity 객체는 HTTP의 헤더, 바디의 정보를 편리하게 조회할 수 있도록 해준다. 반환으로 사용하면 응답의 body에 직접 데이터를 입력할 수도 있다.
이를 상속받은 RequestEntity, ResponseEntity 객체도 있는데, 각각 HTTP 메서드와 url의 조회, 그리고 HTTP 상태코드 설정 기능을 추가로 지원한다.
더 편한 방법으로는 어노테이션을 사용하는 방법이 있다.
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return "ok";
}
@ResponseBody 어노테이션을 달아주면 해당 데이터를 응답의 body에 직접 입력한다. 위의 메서드를 실행하면 응답의 body에 “ok”가 입력될 것이다.
@RequestBody는 요청의 body 데이터를 해당 변수로 변환하여 입력해준다.
자세한 url의 조회나 HTTP 상태코드 설정 등의 기능이 필요하다면 위의 HttpEntity를, 간단하게 처리하고자 한다면 아래의 @RequestBody 어노테이션을 상황에 맞게 선택하여 사용하면 된다.
만약 json 데이터를 객체로 읽어오는 상황에서도 @ModelAttribute 어노테이션을 사용하는 것처럼 String 대신 클래스를 입력하여 받을 수 있는데, 이는 스프링의 HTTP 메세지 컨버터가 메세지 body의 미디어 타입, 변수의 타입 등을 고려하여 적절하게 변환해주기 때문이다. 자세한건 추후에 다루도록 하겠다.
만약 객체를 @ResponseBody로 반환한다면 여기서도 메세지 컨버터가 작동하여 적절히 json 타입으로 변환하여 body에 입력해준다.
HTTP 응답
지금까지는 HTTP 요청의 데이터를 읽어오는 방식들을 알아보았다. 이제는 응답 데이터를 작성하는 방법에 대해서 알아보자. 응답 데이터의 작성은 크게 3가지 방법이 있다.
- 정적 리소스
- 뷰 템플릿 사용(Thymeleaf 등)
- HTTP 메시지 바디 사용
정적인 웹페이지를 띄울 때에는 정적 리소스를, 동적인 웹페이지가 필요하다면 뷰 템플릿을, API 등에서 데이터만 보내고자 한다면 HTTP body를 사용하는 식이다.
정적 리소스
정적 리소스를 띄우는건 아주 간단하다. ‘src/main/resources/static’ 경로에 html 파일 등을 넣어놓으면, URI를 통해 웹 브라우저에서 파일에 접근하면 해당 파일을 그대로 웹에 띄워준다.
뷰 템플릿
뷰 템플릿도 ‘src/main/resources/templates` 경로에 있는 html 파일로 접근하는 것은 똑같다. 대신 URI로 웹에서 직접 접근하는 것이 아닌, 컨트롤러를 통해 View의 논리 이름을 반환받아 접근한다. 자세한 것은 아래의 코드를 보자.
<!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>
뷰 템플릿을 통해 띄울 html의 코드이다. 저 empty 파트의 문자를 뷰 템플릿에서 data란 키의 value를 넣어줄 것이다.
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.servlet.ModelAndView;
@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 responseViewV2(Model model) {
model.addAttribute("data","hello");
return "response/hello";
}
@RequestMapping("/response/hello")
public void responseViewV3(Model model) {
model.addAttribute("data","hello");
}
}
뷰 템플릿을 사용할 수 있도록 view의 논리 이름을 반환하는 컨트롤러이다.
위의 코드를 보면 총 3가지 방법이 있는데, 첫 번째는 ModelAndView 객체에 데이터와 view 이름을 넣어 반환하는 방법이다.
두 번째는 String으로 view의 이름을 반환하는 것이다. @ResponseBody 어노테이션이 없다면 해당 String의 이름으로 ViewResolver가 작동하여 view를 렌더링 해준다.
세 번째는 그다지 권장되지 않는 방법인데, URI 맵핑을 view 이름으로 맵핑해주고, @Controller 어노테이션을 사용하며 따로 HTTP 바디를 사용하는 파라미터가 없다면 해당 URI 이름을 view 이름으로 사용한다.
딱봐도 매우 조건이 많고 명시성이 불분명하므로 쓰지 말자.
Thymeleaf 세팅
참고로 타임리프의 라이브러리를 추가하면 application.properties 파일에
‘spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html’
이렇게 어디서 본 적 있는 접두어 접미어 설정이 생긴다. 이렇게 기본 설정이 되어있기 때문에 ‘resources/hello’와 같이 논리 이름만을 반환해도 제대로 view를 띄울 수 있는 것이다.
메세지 바디 응답
이제 뷰 템플릿 등을 거치지 않고 직접 메세지 바디에 데이터를 담아 응답하는 방법을 알아보자.
@Slf4j
@Controller
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);
return new ResponseEntity<>(helloData,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;
}
}
총 5가지의 방법으로 응답을 해 본 코드이다. 하나씩 살펴보자.
- 이전과 같이 서블릿을 사용하여 HttpServletResponse를 통해 “ok”을 전달했다.
- HttpEntity를 상속받은 ResponseEntity 객체를 사용하여 응답과 HTTP 상태코드를 넣어 반환했다.
- @ResponseBody 어노테이션을 통해 메시지 컨버터가 작동하여 String을 바디에 직접 전달해준다.
- json 데이터를 반환해야 하는데, ResponseEntity에 이전과 같이 객체와 상태코드를 넣어 반환했다.
- @ResponseBody 어노테이션을 사용하면 객체를 반환했을 때 json으로 변환하여 응답에 넣어준다.
HTTP 메세지 컨버터
지금까지 우리가 어노테이션을 사용해 편리하게 변수에 값을 바인딩 했던 것도, 반환 값이 자동으로 view의 논리 이름이나 json 데이터로 변환되던 것도, 모두 HTTP 메세지 컨버터를 사용했기 때문이다.
그런데 이러한 메세지 컨버터는 어느 타이밍에 어떻게 작동하는 것일까? 아래의 그림을 통해 알아보자.
위의 그림은 이전에 본 적이 있을 것이다. 메세지 컨버터는 @RequestMapping과 같은 어노테이션 기반의 컨트롤러를 처리하는 핸들러 어댑터인 ‘RequestMappingHandlerAdapter’에서 사용된다.
먼저 프론트 컨트롤러 역할인 DispatcherServlet이 어노테이션들을 처리해야 할 때가 오면, RequestMappingHandlerAdapter로 접근한다.
핸들러 어댑터는 ‘ArgumentResolver’를 사용하여 어노테이션과 파라미터를 기반으로 실제 데이터를 생성한다. 우리가 @ModelAttribute, @RequestParam과 같은 어노테이션으로 파라미터를 유연하게 처리할 수 있었던 것이 이 ArgumentResolver 덕분이다.
ArgumentResolver는 supportsParameter() 메서드를 호출하여 해당 파라미터를 지원하는지 체크한 후, resolveArgument() 메서드를 실행하여 실제 객체를 생성하여 반환한다.
이 때 ArgumentResolver는 HTTP 메세지 컨버터를 사용한다. HTTP 메세지 컨버터는 정확히는 HTTP 메세지의 데이터를 적절하게 파싱해주는 역할을 한다. 따라서 ArgumentResolver가 @RequestBody, HttpEntity 등을 사용하며 HTTP 요청의 데이터를 읽어와야 할 때가 오면, HTTP 메세지 컨버터를 사용하여 값을 받아와 처리하는 것이다.
ReturnValueHandler는 응답 값을 변환하고 처리하는데 사용한다. 컨트롤러에서 view 이름을 String으로 반환해도 동작하는 이유가 ReturnValueHandler이다. ReturnValueHandler에서는 @ResponseBody, HttpEntity 등의 처리를 하고 메세지 컨버터의 도움을 받아 응답 결과를 작성한다.
위의 기능들은 인터페이스로 제공되므로 확장이 용이하다. 만약 general 하지 않은 경우에 필요하다면 ‘WebMvcConfigurer’를 검색해보면 확장에 대해 알아볼 수 있다.
끝
스프링 MVC의 기본 기능을 배우는 것인데도 깊이 있게 들어갔기 때문인지 상당히 내용이 많고 어려웠다. 그래도 구조에 대해 확실하게 이해가 되는 느낌이다.
다음 포스팅에서는 이러한 기능들을 바탕으로 실제 웹을 간단하게 제작해보는 것에 대해 포스팅하도록 하겠다.