본문 바로가기

Develop/SPRING FRAMEWORK

Spring MVC가 제공하는 기능 (어노테이션 기반)

1. 요청 매핑

URL 요청에 따라 컨트롤러가 매핑되어 해당 메소드가 실행된다. @RequestMapping 어노테이션을 활용하여 다양한 속성들을 지정해줄 수 있다. 이때 대부분의 속성은 배열로 제공하기 때문에 다중 설정이 가능하다.

@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
}

@GetMapping("/mapping-get-v2")
public String mappingGetV2() {
}

@GetMapping, @PostMapping처럼 특정 HTTP 메소드를 축약한 어노테이션도 존재한다. 당연히 훨씬 직관적이다.

 

@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
}

@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable String userId) {
}

@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
}

@PathVariable 어노테이션을 사용하여 경로 변수를 활용할 수 있다. 물론 여러 개의 @PathVariable을 사용할 수도 있다.

이때 만약 path의 변수명과 파라미터 이름이 같으면 @PathVariable()과 같이 이름값을 생략할 수 있다. (여기서 @PathVariable은 꼭 작성해주어야 한다. 뒤에 나올 @RequestParam 사용법과 헷갈리지 말자)

 

@GetMapping(value = "/mapping-param", params = "!mode")
public String mappingParam() {
}

@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
}

특정 파라미터 또는 특정 헤더 조건을 걸어줄 수도 있다. 

만약 조건을 만족하지 않는 경우 각각 위와 같은 에러가 발생하게 된다.

 

@PostMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)
public String mappingConsumes() {
}

@PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)
public String mappingProduces() {
}

consumes 속성을 통해, Content-Type 헤더를 기반으로 매핑할 수 있다. 요청 HTTP 헤더 내 Content-Type 속성과 일치해야 한다. 쉽게 생각해서 해당 컨트롤러가 소비할 수 있는 미디어 타입이라고 생각하면 된다!

 

produces 속성도 유사하다. 요청 HTTP 헤더 내 Accept 속성과 일치해야 한다. 컨트롤러가 만들어낼 수 있는 미디어 타입이다.

위에서 설명한 2가지 속성과 일치하지 않으면 다음과 같은 에러가 발생한다. (단어가 직관적이라 조금만 생각해보면 이해하기 쉽다)

2. HTTP 요청 - 헤더 조회

@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) {
}

1. HttpServletRequest

2. HttpServletResponse

3. HttpMethod : HTTP 메소드 조회

4. Locale : Locale 정보 조회

5. @RequestHeader MultiValueMap<String, String> : 요청 헤더를 MultiValueMap으로 조회

6. @RequestHeader("host") : 특정 요청 헤더를 조회

7. @CookieValue(value = "myCookie") : 특정 쿠키 조회

 

* MultiValueMap이란 하나의 키에 여러 개의 값을 받을 수 있는 Map을 의미한다. HTTP 헤더 또는 쿼리 파라미터에서 주로 사용

3. HTTP 요청 - 쿼리 파라미터, HTML Form

@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");
}

여기선 단순히 HttpServletRequest을 통해 요청 파라미터를 조회했다.

 

@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(@RequestParam("username") String memberName, @RequestParam("age") int memberAge) {
}

@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(@RequestParam String username, @RequestParam int age) {
}

@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
}

이번엔 @RequestParam 어노테이션을 활용했다.

 

먼저 @RequestParam("username")을 통해 username이라는 이름을 가지는 요청 파라미터를 조회할 수 있다. 내부적으로는 request.getParameter("username")이 동작하게 된다. 만약 요청 파라미터명이 변수 이름과 같게 작성한다면 @RequestParam 처럼 이름값을 생략할 수 있다. 또한 위처럼 요청 파라미터가 String, int, Integer와 같은 단순 타입이면 @RequestParam 어노테이션 마저 생략할 수 있다. (이외의 타입은 @ModelAttribute가 동작) (또한 생략하게 되면 required=false 자동 적용) 하지만 가독성이 떨어질 수 있기 때문에 웬만하면 작성하는 게 좋다.

 

@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(@RequestParam(required = true) String username, @RequestParam(required = false) Integer age) {
}

@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(@RequestParam(required = true, defaultValue = "guest") String username, @RequestParam(required = false, defaultValue = "-1") int age) {
}

@RequestParam 어노테이션에 required, defaultValue 속성을 적용할 수도 있다. (말 그대로의 기능..)

 

required 속성의 기본 값은 true이다. 만약 해당 파라미터를 빠트리면 400 예외가 발생한다. 주의해야할 점은 ?username= 처럼 이름은 존재하는데 값이 비어있으면 빈문자가 들어가게 되어 예외가 발생하지 않는다.

또한 기본 자료형에는 null을 입력할 수 없다. 따라서 required=false을 적용하여 값이 넘어오지 않는다면 500 예외가 발생한다. 이때는 defaultValue을 설정하거나, Integer와 같이 null을 입력 받을 수 있는 자료형을 바꿔주어야 한다.

 

defaultValue는 값이 넘어오지 않았을 때 적용될 기본 값이다. 빈 문자가 넘어온 경우에도 해당 기본 값이 적용된다.

 

@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";
}

@ResponseBody
@RequestMapping("/request-param-multiMap")
public String requestParamMultiValueMap(@RequestParam MultiValueMap<String, Object> paramMap) {

  log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
  return "ok";
}

이번에는 요청 파라미터를 Map으로 조회했다. 사용법은 동일하다.

 

이때 넘어오는 파라미터 값이 1개가 확실하다면 단순 Map을 사용해도 되지만, 그렇지 않은 경우에는 MultiValueMap을 활용한다. 예를 들어 ?age=20,30,40 또는 ?age=20&age=30&age=40 처럼 파라미터가 넘어왔을 때 age=[20,30,40]와 같이 조회가 가능하다.

4. HTTP 요청 - @ModelAttribute

@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 어노테이션은 이러한 기능을 자동으로 제공한다. 이때 @ModelAttribute도 생략할 수 있다.

 

스프링 MVC가 @ModelAttribute을 만나면 아래와 같이 동작한다.

1. HelloData 객체를 생성한다. (=helloData)

2. 요청 파라미터명으로 해당 객체의 프로퍼티를 찾아서 setter을 호출한다. (요청 파라미터명이 객체 프로퍼티명과 다르면 안된다)

5. HTTP 요청 메세지 - 단순 텍스트

HTTP 요청 메세지 바디에 직접 데이터가 담겨서 넘어오는 경우엔 @RequestParam, @ModelAttribute를 사용할 수 없다. (요청 파라미터로 넘어오는 경우와 다르기 때문에 구분할 수 있어야 한다!) 따라서 이런 경우에도 데이터를 읽을 수 있어야 한다.

@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");
}

먼저 InputStream을 통해서 HTTP 메세지 바디를 직접 읽을 수 있다.

스프링 MVC는 InputStream(Reader), OutputStream(Writer)와 같은 파라미터를 지원하기도 한다.

 

@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");
}

스프링 MVC는 HttpEntity 파라미터를 제공한다. 이를 통해 HTTP 헤더와 바디 정보를 편리하게 조회할 수 있다.

내부에서는 HttpMessageConverter(StringHttpMessageConverter)가 사용된다. 이건 아래에서 설명한다. 간단하게 설명하자면 스프링 MVC가 HTTP 메세지 바디를 읽어서 문자나 객체로 변환하여 전달해주는 것이다.

 

당연히 응답에서도 사용할 수 있다. 이때는 헤더 정보를 포함할 수 있고, 메세지 바디 정보를 직접 반환한다.

 

1. RequestEntity : HTTP method, 요청 URL 정보 추가

2. ResponseEntity : HTTP 상태 코드 설정 가능

 

HttpEntity를 상속 받은 객체도 존재한다. 이를 통해 요청과 응답에 더욱 편하게 대처할 수 있다!

 

@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {

  log.info("messageBody={}", messageBody);
  return "ok";
}

@RequestBody 어노테이션을 통해 메세지 바디 정보를 손쉽게 조회할 수 있다.

(헤더 정보가 필요하다면 HttpEntity 또는 @RequestHeader을 사용하면 된다)

6. HTTP 요청 메세지 - JSON

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");
}

InputStream을 통해 메세지 바디를 읽은 뒤, ObjectMapper를 사용하여 문자로 이뤄진 JSON 데이터를 자바 객체로 변환했다.

 

private ObjectMapper objectMapper = new ObjectMapper();

@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를 통해 메세지 바디를 꺼내서 ObjectMapper를 통해 자바 객체로 변환하였다.

지금까지 방법은 메세지 바디를 꺼내서 일일이 자바 객체로 변환해주어야 한다는 번거로움이 있다. 귀찮다...

 

@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {

  log.info("username={}, age={}", data.getUsername(), data.getAge());
  return "ok";
}

@RequestBody 파라미터를 객체로 지정함으로써 해결할 수 있다! HttpMessageConverter(MappingJackson2HttpMessageConverter)메세지 바디의 내용을 우리가 원하는 객체로 변환해준다. 이때 @RequestBody는 생략하면 안된다. 만약 생략하게 되면 객체 타입이기 때문에 @ModelAttribute가 자동으로 적용되어 버린다. @ModelAttribute는 메세지 바디가 아니라 요청 파라미터를 처리하기 때문에 원하는 결과가 나오지 않는다.

 

혹시나 HTTP 요청의 Content-Type이 application/json이 아니면 HTTP 메세지 컨버터가 동작하지 않으니 주의해야 한다!

 

@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<HelloData>를 사용해도 똑같다.

 

@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) throws IOException {

  log.info("username={}, age={}", data.getUsername(), data.getAge());
  return data;
}

또한 @ResponseBody를 사용하여 해당 객체를 HTTP 메세지 바디에 직접 넣어줄 수도 있다. (HttpEntity를 사용해도 같다)

7. HTTP 응답 - 정적 리소스, 뷰 템플릿

스프링에서 응답 데이터를 만드는 방법은 크게 3가지이다.

 

1) 정적 리소스

 

스프링 부트는 아래와 같은 디렉토리에 있는 정적 리소스를 제공한다. 정적 리소스는 해당 파일을 변경 없이 그대로 제공하는 것이다.

- /static

- /public

- /resources

- /META-INF/resources

 

2) 뷰 템플릿

 

스프링 부트는 기본 뷰 템플릿 경로를 제공한다. (src/main/resources/templates)

 

@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!!");
}

컨트롤러는 위 방법대로 뷰 템플릿을 호출할 수 있다.

 

1. ModelAndView 반환

2. String 반환 : 반환되는 논리 뷰 이름을 가지고 뷰 리졸버를 실행하여 렌더링 한다.

3. void 반환 : 요청 URL을 참고하여 논리 뷰 이름으로 사용한다. (웬만하면 피하자.. 명시성이 너무 떨어진다)

8. HTTP 응답 - HTTP API, 메세지 바디에 직접 입력

@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
	response.getWriter().write("ok");
}

HttpServletResponse 객체를 통해 메세지 바디에 직접 데이터를 입력했다.

 

@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
  return new ResponseEntity<>("ok", HttpStatus.OK);
}

ResponseEntity를 사용했다. "ok" 문자열을 메세지 바디에 입력하고, HTTP 응답 코드도 설정할 수 있다.

 

@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
  return "ok";
}

@ResponseBody 어노테이션을 통해 반환되는 문자열이 직접 메세지 바디에 입력된다.

 

@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 객체를 통해 HelloData 객체를 메세지 바디에 입력하고, HTTP 응답 코드를 설정했다.

 

@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 어노테이션을 통해 반환된 HelloData 객체를 직접 메세지 바디에 입력했다.

 

또한 @ResponseBody를 통해 메세지 바디에 데이터를 담고 응답을 할 경우, HTTP 상태 코드를 설정하기 어렵다. 따라서 @ResponseStatus() 어노테이션을 통해 응답 코드를 설정해줄 수 있다. 물론 동적으로 상황에 따라 상태 코드를 달리할 수는 없다. 이때는 그냥 ResponseEntity 객체를 사용하면 된다.