Spring에서 요청 파라미터를 바인딩하는 방법은 다양하다.
필자는 여러 개의 파일과 데이터 그리고 list 형식의 dto 데이터를 받고자 하는 경우가 발생해 @ModelAttribute 어노테이션을 활용했었는데 레퍼런스를 따라쓰기 바빴던 것 같다.
해당 어노테이션이 어떤 역할이고 어떻게 동작하는지를 살펴보면서 알고 사용해보자!
@ModelAttribute는 클라이언트로부터 일반 HTTP 요청 파라미터나 multipart/form-data 형태의 파라미터를 받아 객체로 사용하고 싶을 때 이용된다.@ModelAttribute는 "가장 적절한" 생성자를 찾아 객체를 생성 및 초기화한다.Annotation that binds a method parameter or method return value to a named model attribute, exposed to a web view.
Supported for controller classes with @RequestMapping methods.
@RequestMapping 메소드가 붙여진 Controller 클래스에 지원된다.WARNING: Data binding can lead to security issues by exposing parts of the object graph that are not meant to be accessed or modified by external clients. Therefore the design and use of data binding should be considered carefully with regard to security.
Data Binding은 외부 클라이언트가 접근(get) 또는 수정(set)하면 안되는 객체의 일부분을 노출시킴으로써 보안 이슈를 일으킬 수 있다. 그러므로 Data Binding을 설계하고 사용할 때, 보안과 관련해 신중하게 고려되어야 한다.
parameter, method 레벨로 두 가지의 방식을 지원하고 있다.
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Reflective
public @interface ModelAttribute {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean binding() default true;
}
@Controller
@RequestMapping
public class TestController {
@GetMapping("/test")
public String printTest(@ModelAttribute("printRequest) PrintReq req,
ModelMap model) {
return req.getMessage();
}
@ModelAttribute
public void setAttributes(Model model) {
model.addAttribute("message", "hello");
}
}
Model에 추가하고 싶을 때 method 레벨에서 @ModelAttibute를 추가해준다.@ModelAttibute가 붙은 메소드를 먼저 호출한다. @ModelAttibute는 여러 곳에 있는 단순 데이터 타입을 복합 타입의 객체로 받아오거나 해당 객체를 새로 만들 때 사용할 수 있다.BindException이 발생하여 400 에러를 반환한다.BindingResult 파라미터를 추가하면 된다. @GetMapping("/test")
public String printTest(@ModelAttribute PrintReq req, BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
bindingResult.getAllErrors().forEach(e -> {
System.out.println(e.toString());
});
}
return req.getMessage();
}@ModelAttribute는 Controller에서 endpoint 요청 파라미터에 이용된다.@GetMapping("/test")
public String printTest(@ModelAttribute PrintReq req) {
return req.getMessage();
}
@ModelAttribute를 사용할 때는 객체의 각 필드에 접근해 데이터를 바인딩 할 수 있는 생성자나 setter가 필요하다.@Getter
@Setter // or @AllArgsConstructor
public class PrintReq {
private String message;
}
/test?message=hello 형태의 쿼리 스트링이 되어 GET 요청PrintReq{message='hello'}로 데이터가 바이딩된다@Test
void printTest() throws Exception {
mockMvc.perform(get("/test")
.param("message", "hello")
.andExpect(status().isOk())
.andExpect(content().string("hello"));
}
@ModelAttribute가 붙여진 메소드의 반환값을 처리한다.ModelAttributeMethodProcessor#resolveArgument 메소드 안에서 진행된다.👀 ModelAttributeMethodProcessor 클래스를 뜯어보며 동작 원리를 파악해보자.
createAttribute 안에서 내부적으로 리플렉션(ReflectionUtils)을 사용하여 적절한 생성자를 찾아서 바인딩할 객체를 생성한다.
BeanUtils.getResolvableConstructor()가 실행되는데 이는 대상이 되는 클래스의 사용 가능한 생성자를 찾는 메소드이다.constructAttribute 안에서 생성자 생성하기
resolveArgument 메소드에서 파라미터 바인딩을 시도한다.
ReflectionUtils)을 사용하므로 setter만 만들어서 사용해도 되고, 적절한 생성자(매개 변수가 가장 적은 생성자)를 기준으로 매개변수의 이름을 파라미터 이름과 동일하게 하여 사용해도 된다.@ModelAttribute를 생략하여도 Spring MVC는 ModelAttributeMethodProcessor를 우선적으로 호출해 생성자를 선택하는 과정을 가진다고 한다.ref
덕분에 좋은 정보 잘 보고 갑니다.
감사합니다.