Spring - @RequestParam, @ModelAttribute의 차이점?

2019. 9. 10. 15:22Web/Spring

 

이번 포스팅은 @RequestParam과 @ModelAttribute의 차이점에 대해 다루어볼 것이다. 이번에 다루어볼 내용은 특정 유저관련 컨트롤러 코드를 작성하여 살펴볼 것이다.

 

작업환경은 MacOS+intelliJ로 구성하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
 
    @GetMapping("/insert")
    public Object insertUser(@ModelAttribute("findUser") User user, BindingResult bindingResult){
 
        if(bindingResult.hasErrors()){
            log.info("binding error");
            return "invalidParam";
        }
 
        if(user.getId() == 1){
            user.setLastName("yoon");
            user.setAge(28);
            user.setLevel(Level.BASIC);
 
            return user;
        }
 
        return null;
    }
 
    @GetMapping("/insert2")
    public Object insertUser(@RequestParam int id, @RequestParam String firstName, @RequestParam String lastName){
        return "RequestParamBinding";
    }
 
}
 
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
class User{
 
    private int id;
    private String firstName;
    private String lastName;
    private int age;
    private Level level;
 
}
cs

 

위와 같은 코드가 있다. 세부적인 로직은 제외하고 파라미터를 바인딩하는 부분을 집중해보자. 우선 @RequestParam과 @ModelAttribute의 파라미터 바인딩에는 큰 차이가 있다. 바로 객체로 받는냐, 혹은 쿼리스트링을 하나하나 바인딩 받느냐의 차이이다. 이것은 코드의 가독성에 아주 큰 차이가 있다. 두번째 차이점은 @ModelAttribute는 사용자가 직접 View에 뿌려질 Model객체를 직접 set하지 않아도 된다는 점이다. @ModelAttribute는 스프링이 직접 ModelMap에 객체를 넣어주기 때문이다. 이러한 바인딩 차이점 이외에 한가지 아주 큰 차이점이 있다.

 

매핑을 보면 @ModelAttribute에는 BindingResult라는 인자가 들어가있는 것을 볼 수 있다. 하지만 @RequestParam은 없다. 과연 무슨 말일까?

 

@ModelAttribute는 사실 파라미터를 검증할 수 있는 기능을 제공한다. 이 말은 파라미터 바인딩에 예외가 발생하여도 혹은 파라미터 바인딩은 문제없이 됬으나 로직처리에 적합하지 않은 파라미터가 들어오는 등 이러한 파라미터를 검증할 수 있는 기능이 있다. 만약 파라미터 바인딩에 예외가 발생하였을 때, @ModelAttribute와 @RequestParam의 차이점을 보자. 전자는 바인딩 예외를 BindingResult에 담아준다. 즉, 4xx 예외가 발생하지 않는 것이다. 하지만 @RequestParam 같은 경우 바인딩 예외가 발생하는 순간 TypeMisMatch 예외를 발생시켜 사용자에게 4xx status code를 전달한다. 

 

그러면 개발자는 파라미터를 검증하는 로직을 컨트롤러 코드와 따로 분리하여 바인딩된 파라미터의 값의 유효성을 검사할 수 있게 해준다. 만약 @RequestParam이라면 직접 비즈니스 로직내에서 파라미터를 검증하는 코드가 들어가야 할 것이다.

 

바인딩 예외 처리의 차이점을 간단한 테스트 코드로 테스트 해보았다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserControllerTest {
 
    private static final String FINDBYID_MODELATTRIBUTE_REQUEST_URI = "/user/find";
    private static final String FINDBYID_REQUESTPARAM_REQUEST_URI = "/user/find2";
    private User validParamForFindByUserId ;
 
    private MockMvc mockMvc;
 
    @Before
    public void init(){
        this.mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
    }
 
    @Before
    public void initParam(){
        validParamForFindByUserId = new User(1,"yeoseong","yoon",28,Level.valueOf(1));
    }
 
    @Test
    public void paramVaildationWhenInvalidInputParamWithModelAttribute() throws Exception {
        MvcResult createResult = mockMvc.perform(get(FINDBYID_MODELATTRIBUTE_REQUEST_URI+"?id=abc"))
                                        .andDo(print())
                                        .andExpect(content().string("invalidParam"))
                                        .andReturn()
                                        ;
    }
 
    @Test
    public void paramValidationWhenInvalidInputParamWithRequestParam() throws Exception {
        MvcResult createResult = mockMvc.perform(get(FINDBYID_REQUESTPARAM_REQUEST_URI+"?id=abc"))
                                        .andDo(print())
                                        .andExpect(status().is4xxClientError())
                                        .andReturn()
                                        ;
    }
 
}
cs

 

위 테스트를 돌려보면 우리가 기대하는 바인딩 예외처리 테스트가 무사히 통과할 것이다.