Spring MVC : HTTP 요청 데이터 받기

    HTTP 요청 데이터, 과연 어떤 것들이 올까?

    @Controller + @RequestMapping으로 만들어지는 @RequestMappingController는 다양한 파라미터를 지원해준다. 이런 파라미터들은 Adapater 단에 있는 ArgsResolver 및 HttpMessageConverter를 통해서 실제로 Handler에 공급되기도 한다. 여기서는 HTTP 요청 데이터들이 어떤 것이 오는지를 확인해본다.

    @RequestMapping("/headers")
    public String headers(HttpServletRequest request,
                          HttpServletResponse response,
                          HttpMethod httpMethod,
                          Locale locale,
                          @RequestHeader MultiValueMap<String, String> headerMap,
                          @RequestHeader(name = "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";
    }

    • 위의 코드를 실행하면 위의 콘솔 화면의 값을 얻을 수 있다. @RequestMapping 기반의 컨트롤러는 위에서 볼 수 있듯이 굉장히 다양한 파라메터를 제공하는 것을 볼 수 있다. 
    • Headers는 MultiValueMap으로 받는다. MultiValueMap은 Value에 List가 들어가있고, 거기에 동일 해쉬 값으로 Value가 추가되는 구조다. 이것은 동일한 이름의 헤더에 여러 가지의 값이 들어갈 수 있기 때문이다.(locale 같은거)
    • @Controller의 사용 가능한 파라미터 목록
    • @Controller의 사용 가능한 응답 목록

     

    HTTP 요청 데이터 전송


    HTTP요청을 통해 서버가 클라이언트에 데이터를 전달하는 방법은 세 가지다.

    1. GET + 쿼리 파라미터
      • /url?username=hello&age=20
    2. POST + HTML Form
      • content-type: application/x-www-form-urlencoded
      • 메세지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age=20
    3. HTTP Message Body에 데이터를 직접 담아서 요청
      • HTTP APIT에서 주로 사용
      • JSON, XML, TEXT 형식으로 옴
      • POST, PUT, PATCH 형식

    1,2번은 쿼리 파라미터 형식으로 전달되는 방식에서 SPRING에서는 요청 파라미터로 값을 받을 수 있고, 3번은 ResponseBody와 같은 방식으로 받을 수 있다. 아래에서 좀 더 세분화해서 알아보려고 한다. 

     

     

    HTTP 요청 파라미터 조회 → 쿼리 파라미터, HTML FORM

    GET + 쿼리 파라미터, POST + HTML Form 모두 쿼리 파라미터 형식으로 데이터가 전달된다. 따라서 Request.getParameter()로 "key"로 조회하면 필요한 Value들을 다 받을 수 있었다. 

     

    요청 파라미터를 받는 방법은 크게 세 가지가 있다.

    1. request.getParamter()로 직접 파라메터 조회
    2. @ReqeustParam으로 매개변수를 받기
    3. @ModelAttribute으로 매개변수를 받기

    컨트롤러에 매개변수를 넣어줄 때, 이미 Default로 @RequestParam과 @ModelAttribute가 들어간다. 만약에 어떤 어노테이션도 매개변수에 명시해주지 않는다면 다음과 같이 동작한다

    • 자바의 기본형(String, Int) → @RequestParam으로 동작
    • 사용자 선언 객체 → @ModelAttribute로 동작

    위의 것을 고려하면서 아래의 내용을 살펴본다.

     

     

    요청 파라미터 받기1 (getParamter()로 받기)

    @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");
    }
    • 처음 MVC를 공부했던 것처럼 HttpServletRequest request 객체를 가져와서 getParameter()로 필요한 객체를 꺼내는 형태다.
    • Get 형식으로 /request-param-v1?username=userA&age=20 형식으로 보내면 서버에서 쿼리 파라미터를 기가막히게 분리해서 보여준다.
    • POST 형식으로 x-www-form-urlencoded로 보내도 기가 막히게 쿼리 파라미터를 분리해서 보여준다.

     

    요청 파라미터 받기2 (@RequestParam의 이름 명시해서 받기)

    @ResponseBody
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") String username,
            @RequestParam("age") int age) {
        log.info("username = {}, age = {}", username, age);
    
        return "ok";
    }
    • @RequestParam은 옵션으로 name을 가질 수 있다. 
    • @RequestParam의 name 옵션을 선택해주면, 이름과 동일한 key를 찾아 그 Value를 자동으로 넣어준다.
    • @RestController = @ResponseBody + @Controller다. 이렇게 설정해주고 Return Type을 String으로 해주면 값을 직접 Body에 넣는다.

     

    요청 파라미터 받기3 (@RequestParam의 이름 지우기)

    @ResponseBody
    @RequestMapping("/request-param-v3")
    public String requestParamV3(
            @RequestParam String username,
            @RequestParam int age) {
        log.info("username = {}, age = {}", username, age);
    
        return "ok";
    }
    • @PathVariable과 동일하게 매개변수의 이름과 @RequestParam으로 받고자 하는 변수의 key가 같다면 자동으로 바인딩 된다.

     

    요청 파라미터 받기4 (@RequestParam 생략하기)

    @ResponseBody
    @RequestMapping("/request-param-v4")
    public String requestParamV4(
            String username,
            int age) {
        log.info("username = {}, age = {}", username, age);
    
        return "ok";
    }
    
    • 매개변수 앞에 어노테이션을 생략하면, 기본형인 경우 @RequestParam이 Default로 붙는다.
    • 즉, 위의 식은 생략되었으나 @RequestParam String username으로 이해하면 된다.

     

    요청 파라미터 받기5 (필수 파라미터 여부 설정하기)

    @ResponseBody
    @RequestMapping("/request-param-v5")
    public String requestParamV5(
            @RequestParam(required = false) String username,
            @RequestParam(required = true) int age) {
        log.info("username = {}, age = {}", username, age);
    
        return "ok";
    }
    • @RequestParam의 옵션으로 required를 설정할 수 있다.
    • required = false로 하면, 이 값은 반드시 들어오지 않아도 된다.
      • required = false로 하면, 쿼리 값이 들어오지 않아도 에러가 발생하지 않는다. 단, null 값이 들어온다.
      • 이 때, null값이 들어갈 수 없는 변수 타입이라면, 5xx 에러가 발생한다. 

     

    참고 : null과 ""는 다르다.

    ""는 빈 문자열이다. 따라서 "?username="으로 보내도 응답은 정상적으로 된다. status code 200 OK 응답이 나온다.

     

    요청 파라미터 받기6(Default 값 설정하기)

    @ResponseBody
    @RequestMapping("/request-param-default")
    public String requestParamDefault(
            @RequestParam(required = false, defaultValue = "guest") String username,
            @RequestParam(required = false, defaultValue = "1" ) int age){
        log.info("username = {}, age = {}", username, age);
        return "ok";
    }
    • required = false로 설정하면, 값이 들어오지 않아도 요청은 잘 실행이 된다.
    • 이 때 null이 들어가게 되면, NPE라는 무서운 역병이 터질 수 있기 때문에 Null값을 없애주는 마음으로 default 값을 설정할 수 있다.
    • default value를 설정해두면, 값이 들어오지 않으면 그 값으로 셋팅되어 변수를 사용할 수 있게 된다.

     

    요청 파라미터 받기7(Map으로 모든 파라미터 받기)

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

     

    • @RequestParam으로 Map 형식으로 요청 파라미터를 받을 수 있다.
    • 이 때, Value 값은 Object Type으로 받는다. 왜냐하면 실제로 어떤 값이 paramMap으로 들어올지 모르기 때문에 모든 것을 포괄할 수 있는 Object Type으로 받는다고 한다.
    • 값을 불러올 때는 Map.get(key)로 value를 불러온다.

     

    @ModelAttribute를 활용한 요청 파라미터 받기

    @ModelAttribute는 파라미터를 객체 형태로 바인딩 하는 기능을 지원한다.

    1. HelloData 객체가 생성된다.

    2. HelloData 객체의 프로퍼티를 조회한다. 해당 프로퍼티의 Setter를 호출해서 프로퍼티의 이름과 동일한 요청 파라미터의 값을 바인딩한다.

    3. HelloData 객체를 반환한다.

     

    @ModelAttribute를 활용한 요청 파라미터 받기 1

    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData) {
        log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }
    • @ModelAttribute로 HelloData 객체를 생성한다.
    • 객체를 생성하면, 스프링은 HelloData의 Properties의 이름으로 찾아서 HTTP 요청 파라미터의 이름과 같은 값을 바인딩해서 객체에 담아준다.
    • 필드에서는 HelloData에 이미 값이 담겨져 있기 때문에 해당 값을 사용할 수 있다.

     

    @ModelAttribute를 활용한 요청 파라미터 받기2(@ModelAttribute 생략하기)

    @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2(HelloData helloData) {
        log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }
    • @RequestParam, @ModelAttribute가 생략되면 변수의 타입에 따라 둘 중에 하나가 자동으로 실행된다.
    • HelloData는 유저가 선언한 타입으로 @ModelAttribute가 실행되게 되어서, 자동으로 바인딩이 이루어진다. 

     

    HTTP 요청 메세지 받기

    HTTP 요청 메세지는 Body에 데이터가 포함되어 넘어오는 경우를 이야기 한다. 이 때 전달되는 데이터를 요청 메세지라고 하고, 요청 메세지는 요청 파라메터처럼 받을 수 없다. 요청 파라미터는 쿼리 파라미터 형식으로 넘어오는 것이었고, 요청 메세지는 메세지 바디를 통해 데이터가 직접 넣어오는 것이다.

    따라서 요청 파라미터에서 사용하던 것처럼 .getParameter(), @RequestParam, @ModelAttribute를 사용해서 값을 받을 수는 없다. 

    • 1. InputStream으로 받기
      • 바이트코드이기 때문에 CopyToSpring 필요
    • 2. HttpEntity로 받기
      • HttpMessageConverter가 Content-Type을 보고 Body를 필요한 형태로 Binding 해준다.
      • Body의 값이 그대로 나오고, getBody로 Body를 가져올 수 있다.
    • 3. @RequestBody로 받기 → 헤더 정보가 필요하면 @RequestHeaders, HttpEntity로 접근.
      • String이면 Body 정보가 그대로, 객체 타입을 넣으면 Body 정보가 객체의 properties에 Binding된다.

    HTTP 요청 메세지를 받을 때는, 주로 HTTP 요청 메세지의 Content-Type을 보고 대응되게 된다.

     

    응답하기

    • 1. response.getWriter().write()로 하기
    • 2. HttpEntity로 돌려주기
      • HttpEntity<>로 선언하면, Body가 그대로 Binding 된다.
    • 3. @ResponseBody로 하기 
      • String을 넣어주면 String 값이 Body로 들어간다
      • 객체로 넣어주면, properties에 대해서 JSON 형태로 들어간다.

     

     

    HTTP 요청 메세지 받기 - 단순 텍스트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("message Body = {}", messageBody);
        response.getWriter().write("ok");
    
    }
    • 요청 메세지 바디는 request.getInputStream()으로 받을 수 있다.
    • Stream은 항상 바이트 코드이기 때문에 문자로 인코딩을 해야한다. 
      • 인코딩을 위해 StreamUtils.copyToString(inputstream, 문자 인코딩 타입)으로 문자열로 변경해준다.
      • 그리고 실행한다.

     

    HTTP 요청 메세지 받기 - 단순 텍스트2(InputStream으로 받기)

    @PostMapping("/request-body-string-v2")
    public void requestBodyStringV2(InputStream inputStream, Writer responseWirter) throws IOException {
    
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
    
        log.info("message Body = {}", messageBody);
        responseWirter.write("ok");
    }
    • ArgsResolver가 지원하는 InputStream과 Writer를 매개변수로 받는다.
    • InputStream = request.getInputStream(), Writer = response.getWriter()의 축약으로 이해하면 된다.

    정리하면 Reader = InputStream, Writer = OutputStream으로 정리할 수 있다.

     

    HTTP 요청 메세지 받기 - 단순 텍스트3(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 응답 / 요청 메세지 전달을 위해 제공되는 객체다.
      • 매개변수에 HttpEntity 타입으로 받게 되면, HTTP 메세지가 스펙화 된 것이 그대로 HTTP Entity에 들어와서 전달된다. 
      • <T>에 String을 넣어주면, 스프링은 Http 스펙에 있는 값들을 String으로 바꿔서 넣어준다.
      • getBody(), getHeaders() 메서드를 사용해서 필요한 정보를 보여줄 수 있다.
    • HttpEntity를 돌려주면 메세지 바디에 직접 적힌다. 이 때 헤더 정보를 포함할 수 있다.
    • HttpEntity로 값을 돌려주면, View를 거치지 않는다.

     

    HTTP 요청 메세지 받기 - 단순 텍스트4(RequestEntity, ResponseEntity로 처리하기)

        @PostMapping("/request-body-string-v4")
        public HttpEntity<String> requestBodyStringV4(RequestEntity<String> httpEntity) {
            String messageBody = httpEntity.getBody();
            URI url = httpEntity.getUrl();
            log.info("messageBody = {}, URI = {}", messageBody, url);
    
            return new ResponseEntity<String>("ok", HttpStatus.CREATED );
        }
    • HttpEntity를 상속받은 RequestEntity, ResponseEntity를 HttpEntity와 동일하게 사용해서 처리할 수 있따.
    • RequestEntity는 HttpMethod, url 정보가 추가된다.
    • ResponseEntityt는 HTTP 상태 코드 설정이 가능하다. 

    HTTP 요청 메세지 받기 - 단순 텍스트5(@RequestBody, @ResponseBody)

    @ResponseBody
    @PostMapping("/request-body-string-v5")
    public String requestBodyStringV5(@RequestBody String messageBody) {
        log.info("messageBody = {}", messageBody);
        return "ok";
    }
    
    • @RequestBody를 사용하면, 해당 매개변수에 Body의 내용이 HttpEntity에 바인딩 되었던 것처럼 바인딩 되어 들어온다.
      • @RequestBody를 사용하게 되면 Header 정보가 필요하면 @RequestHeaders를 사용하면 된다. 혹은 HttpEntity를 같이 넘겨주면 된다. 
    • @ResponseBody를 하게 되면 HttpEntity에 Body 값을 담아 바로 Body에 꽂아 넣은 것처럼 반응한다. 즉, View를 거치지 않는다.
      • @RestController = @Controller + @ResponseBody다.

     

    HTTP 요청 메세지 받기 - JSON

    JSON은 먼저 key : value 형식으로 값이 들어온다. 따라서, 자바에서는 JSON 값을 쉽게 객체로 바인딩해서 보내줄 수 있도록 한 가지 도구가 필요하다. 그것이 바로 ObjectMapper이다. 컨트롤러에 먼저 ObjectMapper를 선언해주고 필요할 때 마다 맵핑해서 사용하도록 한다. 

    private ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.readValue(body,  객체)

     

    HTTP 요청 메세지 받기 - JSON V1

    @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");
        
    }
    • 먼저 request에서 Body값을 불러와야한다. Request에서 Body값은 getInputStream()을 통해서 할 수 있다.
    • InputStream은 바이트코드이기 때문에 copyToString을 통해 String으로 처리해준다.
    • String은 현재 JSON 방식의 데이터만 있기 때문에 이것을 ObjectMapper를 통해서 객체에 바인딩해준 후 사용한다.

     

    HTTP 요청 메세지 받기 - JSON V2 (@RequestBody 사용)

    //@RequestBody 적용
    //HttpMessageConverter 사용 → StringHttpMessageConvert 적용
    
    //@ResponseBody 적용
    //메세지 바디에 정보를 직접 반환 (view를 거치지 않음)
    //HttpMessageConverter 사용 → StringHttpMessageConverter
    
    
    @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를 사용하면, Body에 있는 내용이 그대로 String으로 변경되어 저장된다.
    • String은 현재 JSON 타입이기 때문에 ObjectMapper를 통해서 객체에 맵핑이 필요하다.
    • 객체에 맵핑 후, 객체 프로퍼티 접근법으로 처리한다.

     

     

     

    HTTP 요청 메세지 받기 - JSON V3 (권장방법)

    
    // @RequestBody 적용
    // - HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용(content-type : application/json)
    
    
    @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV1(@RequestBody HelloData helloData) throws IOException {
        log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }
    • @RequestBody는 일반적으로 HttpBody가 그대로 읽혀서 String 타입으로 저장된다.
    • 이 때, String 대신 객체로 부를 경우, Body에 있는 JSON 형태의 값을 자동으로 파싱해서 객체와 바인딩한다. 그리고 바인딩 된 객체를 제공해준다. 
      • @RequestBody를 해두고 타입을 선언하면, Http Message Converter가 메세지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변경해준다. 
      • 메세지 바디는 text, JSON 모두 가능하다. 
      • HTTP Converter는 Http 요청 Headers를 확인한 다음, HTTP 메세지 바디의 상태를 읽고, 필요한 형태로 바인딩 해준다.
      • 이 경우, HTTP Message Converter는 objectMapper.readValue(messageBody, HelloData.class)를 실행해준다.

     

    HTTP 요청 메세지 받기 - JSON V4 (HttpEntity로 받기)

    @ResponseBody
    @PostMapping("/request-body-json-v4")
    public String requestBodyJsonV4(HttpEntity<HelloData> helloData) throws IOException {
    
        HelloData body = helloData.getBody();
        log.info("username={}, age={}", body.getUsername(), body.getAge());
        return "ok";
    }

    HttpEntity<HelloData> helloData로 값을 받는다.

    이 때, helloData를 출력해보면 이미 Body의 값은 필드 형식으로 바인딩이 되어있다.

    helloData.getBody()를 통해서 Body의 값을 HelloData 객체에 바인딩 시켜준다.

    그리고 나머지는 properties 접근법으로 처리한다. 

     

    HTTP 요청 메세지 받기 - JSON V5 (객체 돌려주기)

    
    
    // @ResponseBody 적용
    // - 메세지 바디 정보를 직접 반환(view 조회 X)
    // - HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용(Accept : application/json)
    
    
    @ResponseBody
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(HttpEntity<HelloData> httpEntity) throws IOException {
        HelloData helloData = httpEntity.getBody();
        log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
        return helloData;
    }

    • 앞의 코드와 동일하지만, Return Type이 객체다. 
    • @ResponseBody를 사용하면 return 값을 Body에 직접 넣어준다.
      • String으로 반환되면 문자값이 들어간다.
      • 객체로 반환되면 객체값이 들어간다. 주로, JSON 타입으로 바뀐다.
      • new HttpEntity<HelloData>()로도 가능함.

     

    @ResponseBody는 생략이 불가능하다.

    @ResponseBody를 생략하게 되면, 매개변수에는 자동적으로 @RequestParam, @ModelAttribute가 자동으로 들어가게 된다. 기본적으로 위에서는 HelloData라는 객체를 대상으로 처리를 했었는데 HelloData에서 @ResponseBody를 빼게 되면 HelloData에 대해서는 @ModelAttribute가 자동적으로 적용되게 된다.

    HelloData에 @ModelAttribute가 붙게 되면, 이것은 요청 파라미터를 처리하는 식으로 동작한다. 따라서, 메세지 바디에 있는 값을 처리할 수 없게 된다.

    댓글

    Designed by JB FACTORY