Spring Handler Method Argument 2 - Request Parameter(query or form data)

jiho·2021년 6월 14일
0

Spring

목록 보기
8/13
post-custom-banner

요청 매개변수(Request Paramter)를 스프링 Handler Method가 처리하는 방식을 정리해보려고 합니다.

Http Query Paramter로 넘어올 수도 있고 본문(body)를 통해서 넘어올 수도 있습니다. 스프링에서는 두 가지 모두 요청 매개변수로 취급해서 동일하게 처리됩니다.

@RequestParam

@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의 이름을 다르게 지정할 수도 있지만 코드를 좀더 짧게 유지하기 위해서 일치하는 것이 좋습니다.

Map 형태로 Parameter를 처리

또 다른 형태로

@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

@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 정리

@ModelAttribute는 ReqeustParameter를 모델에 할당할 수 있게 해주며 각 데이터를 알맞게 바인딩 해줍니다. 그리고 특별한 설정이 없을 경우, 바인딩 실패시 400 BadReqeust를 돌려주게 되며 BindingResult Argument를 Handler Argument로 받게되면 내부적으로 Binding Error를 처리할 수 있습니다.

추가로 @Valid@Validated를 사용해서 각 필드의 검증을 할 수 있습니다.

Spring MVC에서 Binding Error를 처리방식

코드로 작성하려면 thymeleaf의 코드가 필요하니 간단하고 전형적인 form의 에러를 처리하는 시나리오만 작성해두겠습니다.

  1. GET /events/form : Form 화면 보여줍니다.
  2. 사용자가 Form의 field를 입력합니다. (비정상적인 입력 limit의 값을 음수로 줍니다.)
  3. POST /events 형태로 field를 등록하는 POST 요청을 합니다.
  4. Spring @ModelAttribute@Valid로 핸들러가 form field값들을 검증하고 바인딩됩니다.
  5. 검증에는 실패할 경우, 실패 이유인 Error가 BindingResult에 담기며 BindingResult는 기본적으로 Model에 담기고 다시 Error 이유를 가진 GET /events/form 의 화면으로 돌아갑니다.
  6. 검증에 성공시 다른 Event List 화면으로 Redirect 됩니다.
profile
Scratch, Under the hood, Initial version analysis
post-custom-banner

0개의 댓글