요청 매개변수(Request Paramter)를 스프링 Handler Method가 처리하는 방식을 정리해보려고 합니다.
Http Query Paramter로 넘어올 수도 있고 본문(body)를 통해서 넘어올 수도 있습니다. 스프링에서는 두 가지 모두 요청 매개변수로 취급해서 동일하게 처리됩니다.
@Controller
public class EventController {
@GetMapping("/events/{id}")
@ResponseBody
public Event getEvents(
@PathVariable Integer id,
@RequestParam(required=false, defaultValue="None") String name) {
Event event = new Event();
event.setId(id);
return event;
}
}
@RequestParam
를 통해 parameter를 받아서 처리할 수 있습니다. required
속성으로 필수 유무, defaultValue
속성으로 parameter를 받지 못할 경우 해당 속성의 값으로 설정됩니다.
@RequestParam(value="name") String nameValue
으로 argument의 이름을 다르게 지정할 수도 있지만 코드를 좀더 짧게 유지하기 위해서 일치하는 것이 좋습니다.
또 다른 형태로
@Controller
public class EventController {
@GetMapping("/events/{id}")
@ResponseBody
public Event getEvents(
@PathVariable Integer id,
@RequestParam Map<String, String> params) {
Event event = new Event();
event.setId(params.get("name"));
return event;
}
}
위와 같이 Map 타입으로 @RequestParam
를 처리할 경우, Map형태로 처리할 수 있습니다.
핸들러가 아래와 같이 정의된다면
@GetMapping("/event/{id}")
@ResponseBody
public Event getEvent(@PathVariable Integer id,
@RequestParam String name,
@RequestParam Integer limit) {
Event event = new Event();
event.setId(id);
event.setName(name);
event.setLimit(limit);
return event;
}
요청 매개변수를 MockMvc를 사용해서 테스트에 활용해보겠습니다.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class EventControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void getEventTest() throws Exception {
mockMvc.perform(get("/events/1")
.param("name", "A")
.param("limit", "10"))
.andDo(print())
.andExpect(status().isOk());
}
}
위와 같이 param
를 사용해서 parameter를 넘겨주는 테스트를 진행할 수 있습니다.
@ModelAttribute
는 Method Argument에 적용할 경우, Model의 attribute에 접근할 수 있습니다.
해당 어노테이션을 사용하면 직접 해야하는 각각의 query paramters와 form field들을 파싱하고 변환하는 작업을 대신 할 수 있다. 즉, 데이터 바인딩 작업을 스프링이 대신해줍니다.
아래와 같은 코드 대신
@GetMapping("/events")
@ResponseBody
public Event getEvent(@RequestParam String name,
@RequestParam Integer limit) {
Event event = new Event();
event.setName(name);
event.setLimit(limit);
return event;
}
@ModelAttribute
를 사용해서
@PostMapping("/event")
@ResponseBody
public Event getEvent(@ModelAttribute Event event) {
return event;
}
간단히 query paramter나 form fields를 접근할 수 있습니다.
하지만 Binding 도중 잘못된 값(Integer변환할 수 없는 문자열이 들어올 경우)가 있습니다.
그러한 경우 @ModelAttibute
는 각 field값을 검증하는 역할을 할 수도 있습니다. 이상한 값이 들어올 경우(특별한 설정없이), 400(Bad Reqeust)를 응답하게 됩니다. 하지만 만약 잘못된 형식을 직접 처리하고 싶을 경우가 있습니다.
@ModelAttribute
를 이용시 BindingResult
를 이용해서 필드 값 검증 후 처리@ModelAtrribute가 적용된 Argument 바로 옆에 BindingResult
를 Argument로 받을 경우, Exception없이 Binding Error를 담아서 넘겨주게됩니다.
@PostMapping("/event")
@ResponseBody
public Event getEvent(@ModelAttribute Event event, BindingResult bindingResult) {
if(bindingResult.hasErrors())
{
bindingResult.getAllErrors().forEach(error -> {
System.out.println("===================");
System.out.println(error);
});
}
return event;
}
BindingResult 속에 담긴 에러를 콘솔로 찍는 코드입니다.
위와 같이 @ModelAttribute
는 바인딩에 실패하는 경우만 Error를 발생하고 있습니다. Field 값의 범위를 좀더 사용자 정의로 한정하고 싶을 경우, @Valid
나 @Validated
어노테이션을 활용할 수 있습니다.
@Valid
, @Validated
를 사용해서 Model 검증@PostMapping("/event")
@ResponseBody
public Event getEvent(@ModelAttribute Event event, BindingResult bindingResult) {
if(bindingResult.hasErrors())
{
bindingResult.getAllErrors().forEach(error -> {
System.out.println("===================");
System.out.println(error);
});
}
return event;
}
Event의 Limit 값의 범위를 제한하고 싶을 경우, Event Class에 @Min
설정과 @Valid
를 Handler Argument에 적용하면 됩니다.
public class Event {
...
@Min(0)
private Long limit;
...
}
@Min(0)
이라는 제약을 나타내는 Annotation을 적용 후, 아래의 요청을 처리할 경우.
mockMvc.perform(post("/event")
.param("limit", "-30"))
.andDo(print())
.andExpect(status().isOk());
아래와 같은 Binding Error를 콘솔에 찍히는 것을 확인할 수 있습니다.
===================
Field error in object 'event' on field 'limit': rejected value [-30];
여기서 @Valid
와 @Validated
의 차이점은 @Validated
같은 경우 Group을 지정해서 검증을 진행할 수 있습니다.
@ModelAttribute
는 ReqeustParameter를 모델에 할당할 수 있게 해주며 각 데이터를 알맞게 바인딩 해줍니다. 그리고 특별한 설정이 없을 경우, 바인딩 실패시 400 BadReqeust를 돌려주게 되며 BindingResult Argument를 Handler Argument로 받게되면 내부적으로 Binding Error를 처리할 수 있습니다.
추가로 @Valid
와 @Validated
를 사용해서 각 필드의 검증을 할 수 있습니다.
코드로 작성하려면 thymeleaf의 코드가 필요하니 간단하고 전형적인 form의 에러를 처리하는 시나리오만 작성해두겠습니다.
GET /events/form
: Form 화면 보여줍니다.POST /events
형태로 field를 등록하는 POST 요청을 합니다.@ModelAttribute
와 @Valid
로 핸들러가 form field값들을 검증하고 바인딩됩니다.GET /events/form
의 화면으로 돌아갑니다.