빙응의 공부 블로그

[Spring]스프링 MVC 1편 - 스프링 MVC 기본 기능(요청) 본문

Spring/인프런_개념

[Spring]스프링 MVC 1편 - 스프링 MVC 기본 기능(요청)

빙응이 2024. 1. 14. 15:44

📝요청 매핑 

스프링 컨트롤러는 다양한 방법으로 요청을 매핑할 수 있다.

 

기본 요청 
 /**
 * 기본 요청
 * 둘다 허용 /hello-basic, /hello-basic/
 * HTTP 메서드 모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE
 */
 @RequestMapping("/hello-basic")
 public String helloBasic() {
    log.info("helloBasic");
    return "ok";
 }

 

HTTP 메서드 매핑 
/**
 * method 특정 HTTP 메서드 요청만 허용
 * GET, HEAD, POST, PUT, PATCH, DELETE
 */
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
  log.info("mappingGetV1");
  return "ok";
}

@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
  log.info("mapping-get-v2");
  return "ok";
}

 

PathVariable(경로 변수) 매핑

들어오는 url 경로를 이용해서 파라미터를 받을 수 있다.

/**
 * PathVariable 사용
 * 변수명이 같으면 생략 가능
 * @PathVariable("userId") String userId -> @PathVariable String 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, consume, Accept

타입들은 헤더로 검사가 가능하지만 추가 로직이 필요하여 따로 다르게 해야한다. 

/**
 * 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";
}

/**
 * Accept 헤더 기반 Media Type
 * produces = "text/html"
 * produces = "!text/html"
 * produces = "text/*"
 * produces = "*\/*"
 */
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
  log.info("mappingProduces");
  return "ok";
}

 

 

📝요청 매핑 - API 예시

회원 관리를 HTTP API로 만든다 생각하고 매핑을 어떻게 하는지 알아보자.

데이터 전달은 안한다.

  • 회원 목록 조회 : GET        /users
  • 회원 등록 :         POST      /users
  • 회원 조회 :         GET         /users/{userId}
  • 회원 수정 :         PATCH    /users/{userId}
  • 회원 삭제 :         DELETE  /users/{userId}
@RestController
@Slf4j
@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;
  }
}

 

 

📝HTTP 요청 - 기본, 헤더 조회 

애노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원한다.

이번 시간에는 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
  ) {
    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";
  }
}

위 코드의 파라미터들은 다음과 같다.

  • HttpServletRequest
  • HttpServlertResponse
  • HttpMethod : HTTP 메소드를 조회한다.
  • Locale : Locale 정보를 조회(ko,US 등)
  • @RequestHeader MultiValueMap headerMap : 모든 HTTP 헤더를 조회
  • RequestHeader("host") String host : 특정 HTTP 헤더를 조회
  • @CookieValue(value = "myCookie", required = false) String cookie : 특정 쿠키를 조회
MultiValueMap
MAP과 유사한데, 하나의 키에 여러 값을 받을 수 있다. 
HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다. 

 

🚩참고

@Controller 사용 가능 파라미터 목록

Method Arguments :: Spring Framework

 

Method Arguments :: Spring Framework

JDK 8’s java.util.Optional is supported as a method argument in combination with annotations that have a required attribute (for example, @RequestParam, @RequestHeader, and others) and is equivalent to required=false.

docs.spring.io

@Controller 사용 가능 응답 값 목록 

Return Values :: Spring Framework

 

Return Values :: Spring Framework

A single value type, e.g. Mono, is comparable to returning DeferredResult. A multi-value type, e.g. Flux, may be treated as a stream depending on the requested media type, e.g. "text/event-stream", "application/json+stream", or otherwise is collected to a

docs.spring.io

 

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

서블릿에서 학습했던 HTTP 요청 데이터를 조회하는 방법을 생각해보자 

  • 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

 

서블릿 방식으로 조회하기 

HttpServletRequest의 request.getParameter()을 통해서 두가지 요청 모두 조회할 수 있다. 

GET, 쿼리 파라미터 전송
예시
http://localhost:8080/request-param?username=hello&age=20

POST, HTML Form 전송
예시
POST /request-param
content-type: application/x-www-form-urlencoded

username=hello&age=20
  /**
   * 반환 타입이 없으면서 이렇게 응답에 값을 직접 집어넣으면, view 조회X
   */
  @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");
  }

 

애노테이션 방식으로 조회하기 

스프링이 제공하는 @RequestParam을 사용하면 정말 편하게 사용할 수 있다. 

  @ResponseBody
  @RequestMapping("/request-param-v2")
  public String requestParamV2(
      @RequestParam("username") String memberName,
      @RequestParam("age") int memberAge) {
    log.info("username={}, age={}", memberName, memberAge);
    return "ok";
  }
  /**
   * @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 사용
   * String, int 등의 단순 타입이면 @RequestParam 도 생략 가능
   */
  @ResponseBody
  @RequestMapping("/request-param-v4")
  public String requestParamV4(String username, int age) {
    log.info("username={}, age={}", username, age);
    return "ok";
  }

이렇게 점점 생략이 가능하다.

  • 그러나 내 생각은 너무 생략하는 것은 가독성을 해친다 생각한다. 
파라미터 필수 여부로 조회하기 

아래 코드처럼 파라미터가 없을 시 오류가 나올 수 있게 할 수 있다.

  /**
   * @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 = true, defaultValue = "guest") String username

 

파라미터를 Map으로 조회하기 
/**
 * @RequestParam Map, MultiValueMap
 * Map(key=value)
 * MultiValueMap(key=[value1, value2, ...]) ex) (key=userIds, value=[id1, id2])
 */
@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

실제 개발을 하면서 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다. 

스프링은 이 과정을 완전히 자동화 해주는 @ModelAttribute 기능을 제공한다. 

/**
 * @ModelAttribute 사용
 * 참고: model.addAttribute(helloData) 코드도 함께 자동 적용됨, 뒤에 model을 설명할 때 자세히
 설명
 */
  @ResponseBody
  @RequestMapping("/model-attribute-v1")
  public String modelAttributeV1(@ModelAttribute HelloData helloData) {
    log.info("username={}, age={}", helloData.getUsername(),
        helloData.getAge());
    return "ok";
  }

스프링 MVC는 다음과 같은 실행을 한다.

  • HelloData 객체를 생성
  • 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾아 Setter을 호출해서 입력한다. 

 

 

📝HTTP 요청 - HTTP message body 

우리는 바로 직전에 HTTP Form과 쿼리 파라미터를 보았다 남은 HTTP message body 데이터를 알아보자 

  • HTTP message body에 데이터를 직접 담아서 요청  
    • HTTP API에서 주로 사용, JSON, XML, TEXT
    • POST, PUT, PATCH

요청 파라미터와 다르게, 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");
  }

inpustStream을 통해서 받을 수 있다. 

 

Input, Output 스트림으로 조회
/**
 * InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
 * OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
 */
@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");
}

스프링 컨트롤러는 input과 output 모두 지원하기 때문에 HttpServlet을 생략할 수 있다. 

 

HttpMessageConverter으로 조회 
  /**
   * HttpEntity: HTTP header, body 정보를 편리하게 조회
   * - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
   * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
   *
   * 응답에서도 HttpEntity 사용 가능
   * - 메시지 바디 정보 직접 반환(view 조회X)
   * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
   */
  @PostMapping("/request-body-string-v3")
  public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
    String messageBody = httpEntity.getBody();
    log.info("messageBody={}", messageBody);
    return new HttpEntity<>("ok");
  }
  • HttpEntitiy를 상속받는 다음 객체들도 있다.
    • RequestEntity
      • HttpMethod, url 저옵를 추가, 요청에서 사용
    • ResponseEntity
      • HTTP 상태 코드 설정 가능, 응답에서 사용 
@RequestBody로 조회
/**
 * @RequestBody
 * - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
 * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
 *
 * @ResponseBody
 * - 메시지 바디 정보 직접 반환(view 조회X)
 * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
 */
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
  log.info("messageBody={}", messageBody);
  return "ok";
}

 

  • @RequestBody
    • HTTP 메시지 바디 정보를 편리하게 조회할 수 있다. 참고로 헤더 정보가 필요하면 HttpEntity를 사용하거나 @RequestHeader을 사용하면 된다.

 

🚩정리

  • 요청 파라미터 조회 : @RequestParam, @ModelAttribute
  • HTTP 메시지 바디를 직접 조회하는 기능 : @RequestBody

📝HTTP 요청 메시지 - JSON 

이번에는 HTTP API에서 주로 사용하는 JSON 데이터 형식을 조회해보자 

들어오는 객체는 다음과 같다.

{"username":"hello", "age":20}
  /**
   * @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
   * HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content-type:
  application/json)
   *
   * @ResponseBody 적용
   * - 메시지 바디 정보 직접 반환(view 조회X)
   * - HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용(Accept:
  application/json)
   */
  @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 응답로 바뀌어 응답으로 나가게 된다.