-
[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 : bbhttpRequest.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 기본 객체 처리

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); }- getter, setter, 생성자 모두 없을 때 -> 실패
- getter 있을 때 -> 성공
- setter 있을때 -> 성공
- 생성자만(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인지 확인
- @JsonCreator는
- 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를 제외하고 미리 사용법을 정리하면 다음과 같다
- http://localhost:8080/board/1
- @PathVariable사용
- http://localhost:8080/board?id=1&name=kang
- @RequestParam, @ModelAttribute 사용
- http://localhost:8080/board
- POST에서 @RequestBody 사용
//body { "id":1, "name":"kang", }
출처
- https://medium.com/webeveloper/modelattribute-%EC%99%80-%EC%BB%A4%EB%A7%A8%EB%93%9C-%EA%B0%9D%EC%B2%B4-command-object-42c14f268324
- https://tecoble.techcourse.co.kr/post/2021-05-11-requestbody-modelattribute/
- https://jojoldu.tistory.com/407
- https://doing7.tistory.com/10
- https://velog.io/@conatuseus/RequestBody%EC%97%90-%EA%B8%B0%EB%B3%B8-%EC%83%9D%EC%84%B1%EC%9E%90%EB%8A%94-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80
'Backend > 공부,개념' 카테고리의 다른 글
[Spring] Spring Data (0) 2023.02.27 [Spring Boot] Validation 유효성 검증 (0) 2022.02.17 [Spring Boot] Dto와 Entity (0) 2022.02.12 [Spring Security] WebSecurityConfig 정적 파일 설정 (0) 2021.12.20 [Spring Boot] @configuration과 WebMvcConfigurer의 addCorsMapping (0) 2021.08.26