(Spring)@ModelAttribute
@ModelAttribute
글을 작성하게 된 이유
- N 사 모 카페에서 질문 글을 읽어 보던 중, 내가 생각했던 해결방법과 작성자께서 해결하신 해결 방법이 달라 헷갈렸던 내용을 정리하면서,
@ModelAttribute
에 대해 정확하게 알고자 작성하게 되었다. - 질문 내용으로는 Form 형식으로 데이터를 전송하는 중에 Dto 를 사용해서 전송된 값을 Controller 에서 받고 있는데 그 값이 null 로 들어온다는 내용이였다.
헷갈렸던 내용
1
2
3
4
5
6
7
8
@Controller
public class Test {
@RequestMapping(value = "/create", method = RequestMethod.POST)
public String hospicreate(HosSaveRequestDto dto) {
...
return "redirect:/";
}
}
- HosSaveRequestDto 를 통해서 Form 에서 전달된 데이터를 받고있는데, 여기서 dto 내부의 값이 null 로 받아지는 문제가 발생하고 있었다.
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
@Getter
@NoArgsConstructor
public class HosSaveRequestDto {
private Long id;
private String name;
private String address;
@Builder
public HosSaveRequestDto(Long id, String name, String address) {
this.id = id;
this.name = name;
this.address = address;
}
public Hospital toEntity() {
return Hospital.builder()
.id(id)
.name(name)
.address(address)
.build();
}
@Override
public String toString() {
return
"name::" + this.getName()
+ "address::" + this.getAddress();
}
}
- Dto 내부의 모습
- 내가 생각했던 해결 방법으로는, @ModelAttribute 어노테이션이 빠져서 이런 문제가 발생하는 것이라 생각했었다.
- 작성자께서 해결하신 방법은 Dto 에 @Setter 어노테이션을 달아 모든 필드값에 대한 Setter 를 만들어 해결 하셨다.
- 처음 해결하셨다는 글을 보고 매우 당황했다. 내가 알기로는 Setter 가 필요가 없을텐데 왜? Setter 를 만들어 줌으로 해결이 되었을까?
- 내가 잘못알고 있던 내용이 많은것 같아 공부하고 정리하기로 했다.
사용방법
1
2
3
4
5
6
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelAttribute {
...
}
- @ModelAttribute 는 Parameter 와 Method 두 가지 방식을 지원한다.
Method
1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
public class Test {
@RequestMapping(value = "/create", method = RequestMethod.POST)
public String hospicreate(@ModelAttribute HosSaveRequestDto dto) {
...
return "redirect:/";
}
@ModelAttribute
public void modelAttributeMethodTest() {
System.out.println("method attribute");
}
}
- Controller 에서 Method 에 명시하여 사용하면 된다.
- Method 에 @ModelAttribute 를 명시하여 사용할 경우 hospicreate Method 전에 @ModelAttribute 가 명시되어 있는 Method 가 먼저 실행되게 된다.
Parameter
1
2
3
4
5
6
7
8
@Controller
public class Test {
@RequestMapping(value = "/create", method = RequestMethod.POST)
public String hospicreate(@ModelAttribute HosSaveRequestDto dto) {
...
return "redirect:/";
}
}
- Controller 에서 파라미터 부분에 명시하여 사용하면 된다.
동작원리
순서
- 생성자를 찾아서 인스턴스를 생성한다.
- public 으로 선언되어 있는 생성자를 찾는다.
- 만약 생성자가 없다면, public 이 아닌 생성자중 인자가 제일 적은 생성자를 선택
- 찾은 생성자가 1개라면, 해당 생성자를 선택
- 찾은 생성자가 여러 개라면, 인자수가 제일 적은 생성자를 선택
- 선택된 생성자를 사용해 인스턴스를 만들 때 생성자의 인자의 이름 중 클라이언트가 요청한 파라미터의 이름과 같은 것이 있다면 해당 인자에 요청한 파라미터의 값을 넣어서 생성
- public 으로 선언되어 있는 생성자를 찾는다.
- 클라이언트가 요청한 파라미터 기준으로 setter 메서드를 찾아서 setter 메서드를 찾아서 실행
상세
- @ModelAttribute 의 경우 ModelAttributeMethodProcessor 클래스 내부에서 대게 진행되며, constructAttribute 라는 이름의 메서드 내부에서 생성자를 찾는 동작이 이뤄진다.
1 2 3 4 5 6 7 8 9 10 11 12 13
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { protected Object constructAttribute( Constructor<?> ctor, String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception { if (ctor.getParameterCount() == 0) { // A single default constructor -> clearly a standard JavaBeans arrangement. return BeanUtils.instantiateClass(ctor); } ... } }
- 내부 로직이 매우 복잡 하지만, 하나씩 살펴본다면 위의 Dto(HosSaveRequestDto) 처럼 인자가 0개의 기본 생성자만 가지고 있는 경우라면, 여기서 보이는 if 문에 의해서 BeanUtils.instantiateClass(ctor) 로 넘어가게 된다.
- 여기서 setter 가 있는 경우와 없는 경우로 나뉘게 되는데.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Getter
@NoArgsConstructor
public class HosSaveRequestDto {
private Long id;
private String name;
private String address;
@Builder
public HosSaveRequestDto(Long id, String name, String address) {
this.id = id;
this.name = name;
this.address = address;
}
...
public void setName(String name) {
this.name = name;
}
}
- 이 처럼 setter 가 있을 경우 setter 를 이용해서 값을 넣어주게 된다.
- 만약 setter 가 없고 인자가 제일 적은 생성자가 기본 생성자일 경우 어떠한 값도 바인딩 시키지 못해 값을 넣어주지 못하게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Getter
public class HosSaveRequestDto {
private Long id;
private String name;
private String address;
@Builder
public HosSaveRequestDto(Long id, String name, String address) {
this.id = id;
this.name = name;
this.address = address;
}
...
}
- 위의 경우 처럼 3개의 인자를 갖는 생성자가 유일한 생성자의 경우 생성자를 통해 값이 바인딩 되기 때문에 문제 없이 값이 잘 받아지게 된다.
생략 가능
1
2
3
4
5
6
7
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}
}
- SimpleValueType 이 아니면서, Argument Resolver에 등록되어 있지 않는 경우 @ModelAttribute 가 사용된다.
- 즉, Parameter 의 Type 복합 타입일 경우 생략하여도 @ModelAttribute 가 동작하기 때문에 생략이 가능하다.
- 단, 코드의 가독성이나 추후 유지보수를 생각한다면 생략하는것 보다 명시해주는것이 좋다.
정리
- 위에 질문에 대해 해결하는 방법으로는 두 가지가 있다는걸 알게되었다.
- 첫번째로는 setter 를 사용하는 방법
- Dto 라 크게 문제되지 않을것 같지만, 그래도 무분별한 setter 의 사용은 지양하는 편이 좋다.
- 두번째로는 생성자 를 사용하는 방법
- 단, form 에서 넘겨주는 name 의 값과 생성자에 인자값의 이름과 동일하게 맞춰줘야 하며 2개 이상의 생성자가 있는 경우 주의해서 사용해줘야 한다.
- 위에 질문에서 가장 큰 문제점은 생성자를 2개 사용하는데 그 중 하나로 기본생성자를 사용하고 있다는 문제인것 같다. 만약 기본생성자를 절대적으로 사용해야만 한다면 바인딩 해줘야하는 필드값에 대해 setter 를 만들어주어야 할것 같다.
This post is licensed under CC BY 4.0 by the author.