ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] Controller의 Request data 받아오는 방식
    Backend/공부,개념 2022. 2. 12. 16:59
    반응형

    github에 정리한 내용 중 너무 복잡하거나 불확실한 내용 빼고 정리해서 다시 올려본다.


    SpringBoot에서 RestController 구현 시 요청을 받아오는 방법에 대해서 정리해보려고 한다.

    지금 정리하는 방식에는 form data 형식의 요청은 일단 제외하고 클라이언트가 json형태의 데이터를 보내거나, url query에 담아서 보내는 방식만을 정리하려고 한다. ( content-type: application/json )

     

    1. HTTP Method

    먼저, HTTP METHOD 중 GET, POST, DELETE에 대해서 간단히만 정리하면 다음과 같다.

    GET

    • 리소스 조회에 사용
    • query(쿼리 스트링, 파라미터)로 전달
    • body 지원 일부 안함 (Getmapping일 때 requestBody로 받으면 안 되는 이유)

    POST

    • body를 통해 전달

    DELETE

    • body에 데이터 포함 안하는걸 권장( GET과 같이 header에 데이터 포함)
      • 톰캣은 request body를 post일 때만 파싱 한다

    Get과 delete는 url과 query만을 이용해야 하고, post만 body를 사용할 수 있다는 것을 잊지 말아야 한다.

    원래 DTO에 대충 @Data를 붙여놓고 method, query, body 개념 없이 API가 받아올 데이터가 많으면 무작정 @RequestBody를 써버린 것 같은데(dto로 매핑이 바로 되니까 편하다고 생각해서) HTTP Method별로 특징을 파악하고 HttpRequest 객체가 어떻게 받아와 지는지를 알고 사용해야 할 것 같다.

    2. 데이터를 받아오는 방법

    controller에서 데이터를 받아오는 방법은 5가지가 있다.

    • HttpServletRequest
    • @PathVariable
    • @RequestParam
    • @ModelAttribute
    • @RequestBody

     

    HttpServletRequest

    @PostMapping("/servlet-request")
    public void servletReqeust(HttpServletRequest httpRequest) {
        log.info(httpRequest.getParameter("aa").toString());
        log.info(httpRequest.getParameter("bb").toString());
    }
    httpServletRequest 테스트
    @Test
    public void HttpservletRequest() throws Exception{
        mockMvc.perform(post("/servlet-request?bb=bb")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .param("aa","aa"))
                .andDo(print())
                .andReturn();
        // body null
    }
    INFO 17472 --- [    Test worker] c.d.s.httpRequest.RequestBodyController  : aa
    INFO 17472 --- [    Test worker] c.d.s.httpRequest.RequestBodyController  : bb

    httpRequest.getParameter("")로 파라미터로 보낸 값, url 등을 받아올 수 있다.

    body에 담아서 요청을 보낸 경우는 request.getInputStream과 같은 형태로 읽어올 수 있는데 컨트롤러에서 말고 다른 앞단에 대해 따로 Servlet이나 Filter를 상속받아 커스텀하게 구현을 해야 하는 것 같다. 그리고 getInputStream은 서블릿이 실행되고 딱 한 번만 읽어오기 때문에 주의해야 한다.

     

    @PathVariable

    URL을 처리할 때는 @PathVariable을 사용한다. ,. 같은 구분자를 넣으면 안 된다.

        @GetMapping("/path-variable/{id}/{name}")
        public void pathVariable(@PathVariable Long id, @PathVariable(name = "name") String username) {
            log.info(String.valueOf(id));
            log.info(username);
        }

    @GetMapping 부분 url에 {}로 받아올 변수를 적고 파라미터에 @PathVariable와 함께 같은 이름으로 적어 받아오면 된다. 만약에 {}에 넣은 값과 다른 이름의 변수로 받아오고 싶다면 @PathVariable(name="name")으로 지정해주면 된다.

     

    @RequestParam

    Spring에는 httpRequest.getParameter("")를 간편하게 받아올 수 있는 @RequestParam 어노테이션을 제공한다.

    다음과 같이 사용할 수 있다.

    @GetMapping("/request-param")
    public void requestParam(@RequestParam String name,
                             @RequestParam Long id,
                             @RequestParam(required = false, defaultValue = "default") String requireValue, // default 처리 
                             @RequestParam Map<String, Object> map // 한번에 받아오려면 이렇게 
                             ){
        log.info(name);
        log.info(String.valueOf(id));
        log.info(requireValue);
        for(Map.Entry<String, Object> entry : map.entrySet()) {
            log.info("{}:{}",entry.getKey(), String.valueOf(entry.getValue()));
        }
    }


    주의할 점은, @RequestParam을 사용해서 받아오는 변수에 값이 존재하지 않으면 400 error로 응답한다.

    Status = 400
    Error message = Required request parameter 'id' for method parameter type Long is not present

    @RequestParam(required = false, defaultValue = "default") 이렇게 required를 false로 지정하거나, default값을 지정해주면 된다.

     

    @RequestParam Map<String, Object> map

    @RequestParam Map <String, Object> map 이렇게 여러 개의 param을 map으로 가져올 수도 있다. 그런데 이렇게 사용하는 것 여러 개를 한 번에 처리하고 싶으면 뒤에 나오는 @ModelAttribute를 사용하는 게 좋을 것 같다.

    request param 테스트
      @Test
      public void requestParam() throws Exception{
          mockMvc.perform(get("/request-param")
                  .contentType(MediaType.APPLICATION_JSON_VALUE)
                  .param("id","1")
                  .param("name","jisu"))
                  .andDo(print())
                  .andReturn();
      }
    INFO 18398 --- [    Test worker] c.d.s.httpRequest.RequestBodyController  : jisu
    INFO 18398 --- [    Test worker] c.d.s.httpRequest.RequestBodyController  : 1
    INFO 18398 --- [    Test worker] c.d.s.httpRequest.RequestBodyController  : default
    INFO 18398 --- [    Test worker] c.d.s.httpRequest.RequestBodyController  : id:1
    INFO 18398 --- [    Test worker] c.d.s.httpRequest.RequestBodyController  : name:jisu

     

    @ModelAttribute

    • 클라이언트가 보내는 HTTP 파라미터들을 특정 Object에 바인딩
      • 생성자 또는 setter메서드 필요
    • Query String 및 Form 형식 데이터만 처리

    @ModelAttribute를 사용하면? name=&id= 받아올 값이 많아졌을 경우에 @RequestParam로 하나씩 받아오지 않고 미리 만들어둔 객체로 바인딩시킬 수 있다. 주의할 점은 이때 객체에 각각의 변수에 setter함수(또는 생성자)가 없다면 저장되지 않는다.

    @ModelAttribute나 @RequestParam을 생략해도 String, int들은 RequestParam으로 기타 객체들은 ModelAttribute로 자동으로 처리해주긴 하지만 무조건 생략 해버 리진 않는 게 좋을 것 같다.

     

    @Setter
    public class ModelAttributeDto {
        private String name;
        private Long id;
    }
        @GetMapping("/model-attribute")
        public ResponseEntity<ModelAttributeDto> modelAttribute(@ModelAttribute ModelAttributeDto requestDto) {   
        return ResponseEntity.ok(requestDto);    
        }
    테스트
    
        @Test
        public void modelAttribute() throws Exception{
            mockMvc.perform(get("/model-attribute")
                    .contentType(MediaType.APPLICATION_JSON_VALUE)
                    .param("id","1")
                    .param("name","jisu"))
                    .andDo(print())
                    .andExpect(jsonPath("name").value("jisu"))
                    .andExpect(jsonPath("id").value("1"))
                    .andReturn();
        }

     

    HttpMediaTypeNotAcceptableException

    주의할 점은 ModelAttribute로 받아올 DTO 객체에 setter를 생성하지 않고 그냥 사용하면 Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation] 다음과 같은 에러를 만난다.

    입력받은 파라미터 값을 DTO객체에 바인딩시키기 위해서는 추가적인 설정(setter)이 필요하다.

     

    @ModelAttirbute의 추가 기능 - Validation

    @ModelAttribute를 이용하면 @RequestParam과는 달리 검증 작업을 할 수 있다.

    RequestParam의 경우 잘못된 요청이 들어오면(값이 없는 경우) 400 Bad Reqeust응답을 하게 되는데, ModelAttribute의 경우에는 잘못된 값이 들어올 경우 BindingResult 객체에 실패 결과를 담아 컨트롤러에 전달하므로 그에 따른 추가적인 검증 처리가 가능하다.

    또는 @Valid를 사용해서 검증된 값을 BindingResult에 담을 수도 있다. 에러가 발생했는지 확인은 bindingResult.hasError()으로 한다.

    -> 이때 Excpetion이 발생하는 것이 아니라 해당 변수에 null이 담기는(바인딩이 되지 않는) 상태가 되므로 잘 처리해주어야 한다.

    [BindingResult사용 예]
        @Test
        @DisplayName("Long에 String을 넣었을 때 BindingResult 객체에 저장되는지 테스트 ")
        public void modelAttributeBindingResult() throws Exception{
            mockMvc.perform(get("/model-attribute/binding")
                    .contentType(MediaType.APPLICATION_JSON_VALUE)
                    .param("id","ㅁㄴ")
                    .param("name","jisu"))
                    .andDo(print())
                    .andReturn();
        }
        @GetMapping("/model-attribute/binding")
        public ResponseEntity<String> modelAttributeBindingResult(@ModelAttribute ModelAttributeDto requestDto, BindingResult bindingResult) {
            log.info("request : {},{}", requestDto.getId(), requestDto.getName());
            bindingResult.getFieldErrors().stream()
                    .forEach(error -> log.info("{} ,{}", error.getField(), String.valueOf(error.getRejectedValue())));
            return null;
        }

    결과

    INFO 20842 --- [    Test worker] c.d.s.httpRequest.RequestBodyController  : request : null,jisu
    INFO 20842 --- [    Test worker] c.d.s.httpRequest.RequestBodyController  : id ,ㅁㄴ
    [@Valid와 BindingResult같이 사용 예]

    DTO에 validation 지정

    @Setter
    @Getter
    public class ModelAttributeDto {
    
        private String name;
    
        @Range(min=20, message = "20이상이어야 합니다.")
        private Long id;
    
    }
    
    
    ```java
        @GetMapping("/model-attribute/valid")
        public ResponseEntity<String> modelAttributeValid(@Valid @ModelAttribute ModelAttributeDto requestDto, BindingResult bindingResult) {
            log.info("request : {},{}", requestDto.getId(), requestDto.getName());
    
            if (bindingResult.hasErrors()) {
                bindingResult.getFieldErrors().stream()
                        .forEach(error -> log.info("{} ,{}, {}", error.getField(), String.valueOf(error.getRejectedValue()), error.getDefaultMessage()));
            }
            return null;
        }
        @Test
        @DisplayName("Long에 20 이하의 값을 넣었을 떄 BindingResult 객체에 저장되는지 테스트 ")
        public void modelAttributeBindingValid() throws Exception{
            mockMvc.perform(get("/model-attribute/valid")
                    .contentType(MediaType.APPLICATION_JSON_VALUE)
                    .param("id","10")
                    .param("name","jisu"))
                    .andDo(print())
                    .andReturn();
        }
    

    결과

       2022-02-09 20:21:04.557  INFO 21074 --- [    Test worker] c.d.s.httpRequest.RequestBodyController  : request : 10,jisu
      2022-02-09 20:21:04.559  INFO 21074 --- [    Test worker] c.d.s.httpRequest.RequestBodyController  : id ,10, 20이상이어야 합니다.
    [ Get에서 데이터 바인딩 방법 - WebDataBinder 과 Setter 없이 바인딩시키기 ] - https://jojoldu.tistory.com/407

    @RequestBody

    클라이언트가 보내는 HTTP 요청 본문 ( JSON, XML 등)을 HttpMessageConverter 를 통해 타입에 맞는 자바 객체로 변환한다.

    HttpMessageConverter

    Strategy interface for converting from and to HTTP requests and responses.

    • @RequestBody를 사용하면 요청 본문 데이터가 적합한 HttpMessageConverter를 통해 파싱 되어 자바 객체로 변환된다.
    • 종류
      • StringHttpMessageConverter : 기본 문자 처리
      • MappingJackson2HttpMessageConverter 기본 객체 처리
      • HttpMessageConverter

    Jackson

    SpringBoot에서 기본적으로 제공되는 라이브러리이다. (spring-boot-starter-web)

    ObjectMapper objectMapper = new ObjectMapper();objectMapper.readValue(messagBody, DataDto.class); 

    이런 식으로 읽어옴

     

    MappingJackson2HttpMessageConverter

    body로 json값이 들어오면 Spring에 등록된 여러 converter 중 해당 컨버터를 사용해서 기본적인 객체를 처리한다.

    ObjectMapper를 통해서 Json값을 Java 객체로 역직렬화

    직렬화 - 자바의 객체를 외부 데이터(바이트 형태)로 데이터 변환하는 것

    역직렬화 - 직렬화로 저장된 파일을 다시 자바의 객체로 만드는 것

    이때 Jackson의 ObjectMapper는 Json object필드를 java object필드에 매핑할 때 getter, 메서드를 이용해 (접두사를 지우고 나머지 문자를 소문자로 변환하여 문자열 참조) 필드명을 알아내 매핑시킨다.

    @RequestBody 사용 시 DTO에 getter 메서드가 없으면 DTO가 null로 채워진다.

     

    @RequestBody가 값을 받아오는 방법 테스트

    objectMapper가 readValue
    public class RequestBodyDto {    
      String name;
      Long age;    
      Color favoriteColor;
    }
    @Test 
    @DisplayName("RequestBodyDto objectMapper 테스트")
    public void reqeustBodyObjectMapper() throws JsonProcessingException {
        String requestBody = "{\"name\": \"jisu\",\"age\": 1,\"favoriteColor\" : \"RED\"}";
        RequestBodyDto requestBodyDto = objectMapper.readValue(requestBody, RequestBodyDto.class);
    }
    1. getter, setter, 생성자 모두 없을 때 -> 실패
    2. getter 있을 때 -> 성공
    3. setter 있을때 -> 성공
    4. 생성자만(All, NO 모두 ) 있을 때 -> 실패

    -> getter 또는 Setter가 있어야 한다. 아니면 @JsonProperty(value="name")으로 지정해줘야 한다.

     

    • @WebMvcTest할 때 @ReqeustBody 부분 값 넣어 보내는 방법 json String, object mapping 2가지
    @Test
    @DisplayName("requestBody 테스트")
    public void requestBody() throws Exception {
        String requestBody = "{\"name\": \"jisu\",\"age\": 1,\"favoriteColor\" : \"RED\"}";
        mockMvc.perform(post("/request-body")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(requestBody))
                .andExpect(jsonPath("name").value("jisu"))
                .andExpect(jsonPath("age").value(1))
                .andExpect(jsonPath("favoriteColor").value("RED"))
                .andDo(print())
                .andReturn();
    }
    
    @Test
    @DisplayName("requestBody 테스트 Map")
    public void requestBodyMap() throws Exception {
        HashMap<String, Object> map = new HashMap<>();
        map.put("name","jisu");
        map.put("age",1);
        map.put("favoriteColor","RED");
        mockMvc.perform(post("/request-body")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(objectMapper.writeValueAsString(map)))
                .andExpect(jsonPath("name").value("jisu"))
                .andExpect(jsonPath("age").value(1))
                .andExpect(jsonPath("favoriteColor").value("RED"))
                .andDo(print())
                .andReturn();
    }
    • Long <-> String | Enum <-> 맞지 않는 String 같이 body에 값이 아예 잘못 입력될 경우
      • HTTP 400 Bad Reqeust
      • controller에서 처리하기 전에 Exception 처리되기 때문에 따로 처리하고 싶으면 converter를 사용해야 한다. --> converter는 param에서는 되는데 json으로 보내는 body오류는 못 잡는 것 같다.
      • Enum의 경우 Enum class에 @JsonCreator로 처리하는 방법도 있긴 한데
        • @JsonCreator는 static 으로 만들어줘야 인식한다.
        • null로 변경 / UNKNOWN이라는 ENUM을 하나 더 추가하고 에러 처리
        • throw exception -> HttpMessageNotReadableException 핸들러에서 처리 -> InvalidFormatException(또는 CustomException)의 Instace인지 확인
    • Long, String, Enum 지정한 타입에 맞게 들어왔는데 그 안에서 validation을 하는 경우 (@Valid사용)
      • Errors 객체로 받아와 컨트롤러 내부에서 throw excpetion 해주거나 다른 Default값으로 바꿔줌
      • GlobalExceptionHandler를 만들어서 MethodArgumentNotValidException에 대해서 처리
    @Test
    @DisplayName("requestBody 테스트 Map 값 잘못 들어갔을 때 ")
    public void requestBodyERROR() throws Exception {
        HashMap<String, Object> map = new HashMap<>();
        map.put("name","jisu");
        map.put("age","ㅁㅇㅁㅇ");
        map.put("favoriteColor","RD");
        mockMvc.perform(post("/request-body")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(objectMapper.writeValueAsString(map)))
                .andExpect(status().isBadRequest())
                .andDo(print())
                .andReturn();

     

    • requestBody로 지정한 Dto의 변수의 이름이 aa가 아니고 aaAA 같은 형식일 때 처리가 잘 안 되는 경우는 https://bcp0109.tistory.com/309 이 블로그 참고

     

    정리

    클라이언트가 데이터를 보내게 된다면 이런 식의 경우가 있을 것인데, HttpServletRequest를 제외하고 미리 사용법을 정리하면 다음과 같다

    1. http://localhost:8080/board/1
      1. @PathVariable사용
    2. http://localhost:8080/board?id=1&name=kang
      1. @RequestParam, @ModelAttribute 사용
    3. http://localhost:8080/board
      1. POST에서 @RequestBody 사용
      //body
      {
        "id":1,
        "name":"kang",
      }

    출처

    댓글

Designed by Tistory.