ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 요청 매핑
    Spring/MVC1-스프링MVC 기본기능편 2023. 6. 17. 07:51
    728x90

    개요

    클라이언트에서 서버로 데이터가 전달되는 방법은 크게 3가지로 나눌 수 있다.

    1. GET - 쿼리 파라미터
      예시 : search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=0&ie=utf8&query=축구
      메세지 바디 없이, URL에 데이터를 포함해서 전달하는 방식
      검색, 필터, 페이징기능 에서 많이 사용하는 방식

    2. POST - HTML Form 데이터
      쿼리 파라미터와 비슷하지만, 이는 메세지 바디에 쿼리파라미터 형식으로 데이터를 전달한다
      content-type : application/x-www-form-urlencoded
      회원가입 처럼 post방식의 form태그에 정보를 직접 드러내지 않고 보낼 때 사용한다

    3. HTTP message body 에 데이터를 직접 담아보내기
      HTTP API에서 주로 사용
      데이터 형식은 Json, XML, TEXT 등이 있지만, JSON이 가장 많이 사용된다.
      POST, PUT PATCH 방식으로 사용된다

     

     

     

    GET - 쿼리 파라미터 와 POST - HTML Form 데이터

    이들 둘은 URL에 데이터를 담아오느냐, message body에 데이터를 담아오느냐에 차이가 존재한다.

    하지만, 실질적으로 Servlet이 이 둘을 parsing하는 과정은 동일하다.

     

    쿼리파라미터
    http://localhost:8080/request-param?username=hello&age=20

    message body
    POST /request-param ...
    content-type: application/x-www-form-urlencoded
    username=hello&age=2

    이런 이유로 이 둘은 서버에서 데이터를 받는 방식이 비슷하므로 한번에 알아보자

    또한, 데이터가 어떻게 들어오는지를 확인하는 차원에서 접근할 것이므로 @ResponseBody를 사용하여 message body에 "OK"를 찍어주고 들어온 데이터는 log로 확인해보자.

     

     

    1. servlet 방식

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

    가장 근본은 HttpServletRequest에서 넘어온 데이터를 꺼내는 방법이다.

     

     

    1. @RequestParam & @ModelAttribute

    @ResponseBody // @RestController와 같은 효과
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") String memberName,
            @RequestParam("age") int memberAge  // 알아서 parsing 됨(binding이라고함)
    
    ) {
        log.info("username={}, age={}", memberName, memberAge);
        return "Ok"; // 이렇게 되면, @Controller라면 view를 찾는다. 그러나 @ResponseBody 이므로 단순 텍스트를 반환
    }

    해당방법은 @RequestParam 어노테이션을 활용하여 데이터를 받아오는 방법이다. 이 과정에서는 spring이 적절한 자료형으로 binding하여 값을 받아오게된다.

    여기서 name="username" option의 값은 쿼리 파라미터의 key-value의 key값을 받아오는 것이다. 만약 post-form데이터 형태였다면 input태그의 name 속성값을 받아온다는 것이다.

     

    만약, input태그의 name 속성값과 쿼리파라미터의 key 값과 동일한 이름의 변수를 받아온다면?
    @RequestParam에서의 name 속성값 생략 가능!
    @ResponseBody
    @RequestMapping("/request-param-v3")
    public String requestParamV3(
            @RequestParam String username,
            @RequestParam int age
    ) {
        log.info("username={}, age={}", username, age);
        return "Ok";
    }

    여기서 더 나아가 @RequestParam의 경우는 어노테이션까지 생략이 가능하다. 그렇게 되면 @RequestParam의 속성 중,

    데이터가 없거나 빈값이 넘어왔을 때도 괜찮은 required 옵션이 false가 된다. 하지만,  너무 과도한 생략으로 인해 명확성이 다소 떨어질 수 있으므로 어노테이션의 생략은 피하는 방향이 좋다.

     

    이런 쿼리파라미터를 전달 받을 때 발생할 수 있는 문제는, 데이터의 null이 담길 때이다.

    만약 데이터의 null값이 넘어왔을 때 이를 primitive 타입으로 받게 된다면, 에러가 발생하게 된다.

    이를 해결하는 방법은 크게 2가지이다.

    1. Wrapper 클래스를 활용하여 데이터를 받는다,.
    2. @RequestParam의 default 옵션을 활용하여 null 값이나 빈 값에 대해서 대체할 값을 세팅한다.

     

    이렇게 @RequestParam은 데이터를 받아오는 과정에서 개발자의 편의를 위해 Binding을 해주게 되는데,

    데이터의 타입을 유추하여 개별적으로 받아오는 과정도 가능하지만, 넘어온 데이터의 key-value쌍이라는 특징을 이용해

    객체나 자료구조의 형태로 받아오는 것도 가능하다.

     

    ◎ requestParam - Map

    @ResponseBody
        @RequestMapping("/request-param-map")
        public String requestParamMap(@RequestParam MultiValueMap<String, Object> paramMap) {
    
            List<Object> username = paramMap.get("username");
    
            log.info("username={}, age={}", username, paramMap.get("age"));
            return "Ok";
    
            // http://localhost:8080/request-param-map?username=hong&age=30&username=kim&age=34
            // username=[hong, kim], age=[30, 34]
        }
    MultiValueMap은 동일한 key에 대해서 다양한 value값들을 리스트의 형태로 담아 보관할 수 있는 자료구조이다.

    위에서 처럼 Client로 부터 넘어온 데이터가 key-value 쌍이라는 특징을 이용해 Map의형태로 받아왔다면,

    다른 방법은 자바의 객체를 이용해 데이터를 받는것이다. 이러한 객체를 우리는 대개 DTO(Data Tranfer Object), Command 객체 라 부른다.

     

     

    ◎ @ModelAttribute - Command 자바 객체

    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        log.info("helloData={}", helloData); // @Data 에서 toString()해주기 때문에
        return "ok";
    
        /*
        스프링MVC는 @ModelAttribute 가 있으면 다음을 실행한다.
        HelloData 객체를 생성한다.
        요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾는다.
        그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.
        예) 파라미터 이름이 username 이면 setUsername() 메서드를 찾아서 호출하면서
            값을 입력한다
        */
    }

    여기서 자바객체에 값이 넘어오는 과정에서 중요하게 작용하는 것은 setter이다. 즉, 객체의 member 변수가 넘어온 데이터의 key 또는 input 태그의 name 속성값과 일치해야하며, setter가 준비되어 있어야 가능하다.

    ModelAttribute의  name="" 옵션은 아무것도 지정하지 않게되면, Command 객체의 클래스 이름(앞글자 소문자)를 따라가게된다.

    @RequestParam처럼 @ModelAttribute도 생략이 가능하지만, 동일하게 명시성을 위해 남겨주는 방식을 받아드리기로 했다.

     

     

     

     

     

     

     

    HTTP message body 에 데이터를 직접 담아보내기

    우리가 클라이언트로부터 데이터를 받는 방식은 message body를 통해서도 가능하다.

    이 경우에는 위에서 쿼리파라미터와 form 데이터처럼 parsing의 과정이 이전과는 다르기 때문에 @RequestParam / @ModelAttribute는 사용할 수 없다.

    그럼 어떻게 받아오는지 순차적으로 알아보자.

     

    message body - String 값 받아오기

    1. servlet 방식

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

    가장 전통의 방식이다. request 안에서 inputStream을 통해 messageBody의 내용을 받아오는 방식이다.

    여기서 조금 진화한 방법은 애초에 쿼리파라미터에서 진화한 방법처럼 애초에 inputStream 자체를 인자로 받아오는 방법이다.

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

     

    이제 이후에 소개하는 방식부터는 body의 내용 뿐만 아니라 요청 header의 내용까지 받아올 수 있는 방법이다. 거기에 더해 단순 String 타입의 message body를 받아오는 것이 아닌, command 객체의 형태로 json형식을 받아올 수 있는 방법이다.

     

    2. servlet 방식 - HttpEntity (RequestEntity / ResponseEntity)

    @PostMapping("/request-body-string-v3")
    @ResponseStatus(HttpStatus.CREATED)
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
    
        String messageBody = httpEntity.getBody();
        log.info("messageBody={}", messageBody);
        return new HttpEntity<>("ok");
    }

    httpEntity는 내가 데이터를 받아올 타입을 지정하여 받을 수 있다.

    이후 객체.getBody() 메소드를 통해 원하는 형태로 Parsing이 가능하다.
    이는 단연 String 타입 뿐만 아니라 이후에 다룰 Json데이터도 받아 올수 있다. 이 부분은 조금 뒤에 자세히 알아보기로...

    RequestEntity / ResponseEntity 는 httpEntity 클래스를 상속받아 만들어진 클래스로 조금 더 부가적인 기능 수행이 가능하다. 바로 바디에 담아서 보내줄 때, status까지 보내줄 수 있다는 것이다. httpEntity도 이 부분을 해결하기 위해 어노테이션으로 해당기능을 제공하기도 한다. (위에서 확인)

    @PostMapping("/request-body-string-v3")
    public ResponseEntity<String> requestBodyStringV3(RequestEntity<String> httpEntity) throws IOException {
    
        String messageBody = httpEntity.getBody();
        HttpHeaders headers = httpEntity.getHeaders();
        log.info("messageBody={}", messageBody);
        log.info("headers={}", headers);
    
    
        return new ResponseEntity<>("ok", HttpStatus.CREATED);
    }

    여기서 한단계 진화한 버전이 최종 버전인데, 역시나 어노테이션을 지원하는 방법이다.

     

     

    3.  @RequestBody

    @ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {
        log.info("messageBody={}", messageBody);
        return "Ok";
    }

    @RequestBody는 메세지 바디의 내용을 일거엇 문자나 객체로 변환해주는데 이과정에서 HttpMessageConverter가

    동작하게 된다. 그중에서도 String 타입으로 전환시켜주는 메세지 컨버터는 StringHttpMessageConverter이다.

     

    코드는 깔끔해졌지만 여기서는 단점이 존재한다. 바로 요청헤더의 정보를 받아올 수 없다는 것이다.

    만약 요청헤더의 정보가 필요하다면?

    HttpEntity<>를 사용하여 받아오자.

    HttpHeaders headers = httpEntity.getHeaders();
    log.info("messageBody={}", messageBody);

    헤더의 정보를 조회하는 방법은 다양하게 존재하지만, @RequestHeader 라는 어노테이션 기반으로 받아올 수도 있다.

     

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

    @RequestHeader 를 사용하면, 특정헤더의 key를 받아 조회도 가능하고,

    (@RequestHeader("host") String host,)

     

    요청헤더 내의 모든 값들의 key-value쌍을 모두 가지고 오는 방법으로 조회가 가능하다,.(@RequestHeader MultiValueMap<String, String> headerMap,)

     

    다음은 우리가 message body에 데이터를 받는 데이터의 형태 중 가장 많이 이용하는 json 형태의 데이터를 받은 방법을 알아보자.

     

     

     

     

     message body - JSON 데이터 받아오기

     

    1. servlet 방식

    json데이터를 바다오기 위해서는 이전에 servlet 버전에서 본 것처럼 String 값을 받아오는 inputStream을 사용하는 방법이 있다. 하지만 json의 데이터는 java의 객체형태로 전환하는 과정이 필요한데 보통의 스프링은 Jackson 라이브러리가 도와주지만 초기에는 ObjectMapper가 json을 java 객체로 전환해주는 기능을 제공했다.

    ObjectMapper objectMapper = new ObjectMapper(); // 이건 Jackson 라이브러리를 사용하지 않을때,Servlet에서 사용하는 방식
    
    @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());
    
        /*reponse.getWriter().write() 로 json을 그대로 보내주기 위해서 사용*/
        String s = objectMapper.writeValueAsString(helloData);
        response.getWriter().write(s);
    json -> java 객체 : objectMapper.readValue(inputStream, 변환에 필요한 encoding타입)
    java 객체 -> json : objectMapper.writeValueAsString(객체)

     

     

    1. @RequestBody - converter 사용

    우리는 이전에 String값을 @RequestBody 어노테이션을 받아올 때 StringHttpMessageConverter가 동작한다고 했다.

    그렇다면, json 형태의 데이터를 자바의 객체로 한번에 Converting해주는 객체도 있지 않을까?

    있다 ㅋㅋㅋㅋㅋㅋ 역시 스프링

    아까 스프링에서 객체로 converting해주는 라이브러리를 제공하는 게 Jackson이라고 했는데 이 이름을 따서,

    MappingJackson2HttpMessageConverter가 있다.

    이 converter의 특징은 양방향으로 converting을 해준다는 것이다. 즉, 객체타입으로 converting하는 것도 가능하고,

    json의 형태로 데이터를 내려줄 때, java 객체를 -> json의 형태로 변환도 알아서 해준다.

    @ResponseBody
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData helloData) throws IOException {
                                        /*json -> HelloData 객체*/
        log.info("messageBody={}", helloData);
        log.info("username={}. age={}", helloData.getUsername(), helloData.getAge());
        return helloData;  /*HelloData 객체 -> json*/
    }

    (진짜 어디까지 편의기능을 제공할런지...ㅋㅋㅋㅋ)

     

    물론 이전에 잠깐 언급한 것처럼 HttpEntity도 해당 기능을 제공한다.

        @PostMapping("/request-body-json-v4-1")
        public HttpEntity<HelloData> requestBodyJsonV41(HttpEntity<HelloData> httpEntity) throws IOException {
            HelloData data = httpEntity.getBody();
            log.info("messageBody={}", data);
            log.info("username={}. age={}", data.getUsername(), data.getAge());
            return new HttpEntity<>(data);
            /* 이렇게 처리하는 것도 기억하자! */
        }

     

     

    여기까지 요청 데이터를 Spring이 데이터의 형식에 따라, 어떤 방식으로 binding 혹은 converting하는지 알아보았다.

    'Spring > MVC1-스프링MVC 기본기능편' 카테고리의 다른 글

    로깅  (1) 2023.06.14
Designed by Tistory.