Spring MVC(2) Request Handler : 요청 데이터를 읽는 방법

오잉·2023년 4월 18일
0

SPRING

목록 보기
9/15

클라이언트에서 서버로 요청 데이터를 전달하는 방법

HTTP 요청 메시지를 통해 클라이언트에서 서버로 데이터를 전달하는 방법에는 크게 3가지가 있다.

1. GET - 쿼리 파라미터

  • /url?username=hello&age=20
  • 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달
  • 예) 검색, 필터, 페이징등에서 많이 사용하는 방식

2. POST - HTML Form

  • content-type: application/x-www-form-urlencoded
  • 메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&age=20
  • 예) 회원 가입, 상품 주문, HTML Form 사용

3. HTTP message body에 데이터를 직접 담아서 요청

  • HTTP API에서 주로 사용, JSON, XML, TEXT
  • 데이터 형식은 주로 JSON 사용
  • POST, PUT, PATCH

이제부터 각각의 방법에 대해 하나씩 알아보자


1. 요청 파라미터 조회

  • 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

쿼리 파라미터 전송 방식과 HTML Form 전송 방식은
클라이언트가 보내는 방식이 다를 뿐
서버 입장에서는 username=hello&age=20 처럼 서로 같은 형식을 갖고 있다

즉, 서버 입장에서는 같은 형식이기 때문에,
구분 할 필요없이 같은 방법으로 조회할 수 있다.

이런 형식을 조회하는 방법을 요청 파라미터(request parameter) 조회라 한다.

1) HttpServletRequest가 제공하는 방법

@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);
}
  • request.getParameter("파라미터명")

2) @RequestParam

/** @RequestParam : 파라미터 이름으로 바인딩 **/
@RequestMapping("/request-param-v2")
public void requestParamV2(
             @RequestParam("username") String memberName,
             @RequestParam("age") int memberAge) {
    log.info("username={}, age={}", memberName, memberAge);
}

// HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능
@RequestMapping("/request-param-v3")
public void requestParamV3(
          	@RequestParam String username,
          	@RequestParam int age) {
    log.info("username={}, age={}", username, age);
}

// String , int , Integer 등의 단순 타입이면 @RequestParam 도 생략 가능
@RequestMapping("/request-param-v4")
public void requestParamV4(String username, int age) {
    log.info("username={}, age={}", username, age);
}

// 파라미터 필수 여부를 required 옵션으로 지정할 수 있다 (기본값 : true)
@RequestMapping("/request-param-required")
public void requestParamRequired(
            @RequestParam(required = true) String username,
            @RequestParam(required = false) Integer age) {
    log.info("username={}, age={}", username, age);
}

// 파라미터에 값이 없는 경우 defaultValue 를 사용하면 기본 값을 적용할 수 있다.
// 이미 기본 값이 있기 때문에 required 는 의미가 없다.
@RequestMapping("/request-param-default")
public void requestParamDefault(
          	@RequestParam(defaultValue = "guest") String username,
          	@RequestParam(defaultValue = "-1") int age) {
     log.info("username={}, age={}", username, age);
}
  • @RequestParam의 name(value) 속성이 파라미터 이름으로 사용됨
    ex. @RequestParam("username") String memberName -> request.getParameter("username")
  • @RequestParam 애노테이션을 생략하면 스프링 MVC는 내부에서 required=false 를 적용한다.
  • 근데 애노테이션 너무 생략하는 것도 과하다 -> 이왕이면 @RequestParam 붙여서 명확하게 하자
@ResponseBody
@RequestMapping("/request-param-map")
public void requestParamMap(@RequestParam Map<String, Object> paramMap) {
      log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
}
  • 파라미터를 Map 또는 MultiValueMap으로 조회할 수 있다.
  • @RequestParam Map
    -> Map(key=value)
  • @RequestParam MultiValueMap
    -> MultiValueMap(key=[value1, value2, ...] ex) (key=userIds, value=[id1, id2])
  • 파라미터의 값이 1개가 확실하다면 Map 을 사용해도 되지만, 그렇지 않다면 MultiValueMap 을 사용하자.

3) @ModelAttribute

실제 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다. 보통 다음과 같이 코드를 작성할 것이다.

@RequestMapping("/request-param")
public void requestParam(
          	@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;
}

스프링MVC는 @ModelAttribute 가 있으면 다음을 실행한다.
1. HelloData 객체를 생성한다.
2. 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다.
3. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.
예) 파라미터 이름이 username 이면 setUsername() 메서드를 찾아서 호출하면서 값을 입력한다.

/** @ModelAttribute : 요청 파라미터를 이용해 필요한 객체 만드는 과정 자동화 **/
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
      log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
}

// @ModelAttribute 는 생략할 수 있다.
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
}

참고) @ModelAttribute 는 생략할 수 있다. 그런데 @RequestParam 도 생략할 수 있으니 혼란이 발생할 수 있다.
=> 스프링은 해당 생략시 다음과 같은 규칙을 적용한다.
String , int , Integer 같은 단순 타입 = @RequestParam
나머지 = @ModelAttribute


2.HTTP 메시지 바디 조회

요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam , @ModelAttribute 를 사용할 수 없다. (물론 HTML Form 형식으로 전달되는 경우는 요청 파라미터로 인정된다.)

HttpEntity , @RequestBody 를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다.
HTTP 메시지 컨버터는 문자 뿐만 아니라 JSON도 객체로 변환해준다.

단순 텍스트

1) InputStream

@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(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
  • OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력

2) HttpEntity

@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 header, body 정보를 편리하게 조회
    • 메시지 바디 정보를 직접 조회
    • 요청 파라미터를 조회하는 기능과 관계 없음 @RequestParam X, @ModelAttribute X
  • HttpEntity는 응답에도 사용 가능
    • 메시지 바디 정보 직접 반환
    • 헤더 정보 포함 가능
    • view 조회X

HttpEntity 를 상속받은 다음 객체들도 같은 기능을 제공한다.

  • RequestEntity
    • HttpMethod, url 정보가 추가, 요청에서 사용
  • ResponseEntity
    • HTTP 상태 코드 설정 가능, 응답에서 사용
    • return new ResponseEntity("Hello World", responseHeaders, HttpStatus.CREATED)

3) @RequestBody

@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
        log.info("messageBody={}", messageBody);
        return "ok";
}
  • @RequestBody :HTTP body 정보를 편리하게 조회
    • 참고) 헤더 정보가 필요하다면 HttpEntity 를 사용하거나 @RequestHeader 를 사용
    • 요청 파라미터를 조회하는 기능과 관계 없음 @RequestParam X, @ModelAttribute X
  • @ResponseBody
    • 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있다
    • view를 사용 X

JSON

⚠️ 주의
HTTP 요청시에 content-type이 application/json인지 꼭! 확인해야 한다.
그래야 JSON을 처리할 수 있는 HTTP 메시지 컨버터가 실행된다.

1) InputStream

private ObjectMapper objectMapper = new ObjectMapper();
 
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
          ServletInputStream inputStream = request.getInputStream(); // HttpServletRequest를 사용해서 직접 HTTP 메시지 바디에서 데이터를 읽어온다
          String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); // 문자로 변환한다
          log.info("messageBody={}", messageBody);
          HelloData data = objectMapper.readValue(messageBody, HelloData.class); // 문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper를 이용해 자바 객체로 변환
          log.info("username={}, age={}", data.getUsername(), data.getAge());
          response.getWriter().write("ok");
}

2) HttpEntity

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

2) @RequestBody

// 문자로 변환하고 다시 json으로 변환
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
      	HelloData data = objectMapper.readValue(messageBody, HelloData.class);
      	log.info("username={}, age={}", data.getUsername(), data.getAge());
      	return "ok";
}
// @ModelAttribute처럼 한번에 객체로 변환
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
}
  • @RequestBody 객체 파라미터
    • @RequestBody HelloData data
    • @RequestBody 에 직접 만든 객체를 지정할 수 있다.
  • @RequestBody는 생략 불가능
    • @ModelAttribute , @RequestParam 과 같은 애노테이션을 생략시 다음과 같은 규칙을 적용한다.
      • String , int , Integer 같은 단순 타입 = @RequestParam
      • 나머지 = @ModelAttribute (argument resolver 로 지정해둔 타입 외)
    • 따라서 HelloData에 @RequestBody 를 생략하면 @ModelAttribute 가 적용되어버린다.
    • 따라서 생략하면 HTTP 메시지 바디가 아니라 요청 파라미터를 처리하게 된다.
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
      log.info("username={}, age={}", data.getUsername(), data.getAge());
      return data;
}
  • @ResponseBody
    • 응답의 경우에도 @ResponseBody 를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있다.
    • 물론 이 경우에도 HttpEntity 를 사용해도 된다.

@RequestBody 요청 : JSON 요청 -> HTTP 메시지 컨버터 ->객체
@ResponseBody 응답 : 객체 -> HTTP 메시지 컨버터 -> JSON 응답


정리

요청 파라미터 vs HTTP 메시지 바디

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

+) HTTP 헤더 정보를 조회하는 방법

@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)
                 		  ){
        return "ok";
    }
}
  • HttpServletRequest
  • HttpServletResponse
  • HttpMethod : HTTP 메서드를 조회한다. org.springframework.http.HttpMethod
  • Locale : Locale 정보를 조회한다.
  • @RequestHeader MultiValueMap<String, String> headerMap
    : 모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.
  • @RequestHeader("host") String host : 특정 HTTP 헤더를 조회한다.
    (필수 값 여부: required / 기본 값 속성: defaultValue)
  • @CookieValue(value = "myCookie", required = false) String cookie : 특정 쿠키를 조회한다.
    (필수 값 여부: required 기본 값: defaultValue)

참고 : MultiValueMap은 하나의 key에 여러 value를 가질 수 있는 map


인프런 김영한님의 스프링 MVC 1편의 [섹션6 - Spring MVC 기본기능]
스프링 공식문서의 Method Arguments

profile
오잉이라네 오잉이라네 오잉이라네 ~

0개의 댓글