작성
·
582
2
안녕하세요
/converter/edit 에서 제출 버튼을 클릭해서
@PostMapping("/converter/edit")
public String converterEdit(@ModelAttribute Form form, Model model) {
IpPort ipPort = form.getIpPort();
model.addAttribute("ipPort", ipPort);
return "converter-view";
}
위 부분이 실행되기 전에 첫줄(IpPort ipPort = form.getIpPort();)에 break 걸고보면
SpringToIpPortConverter : convert source = 127.0.0.1:8080
위 로그가 연속으로 2개 찍힙니다.
하나는 "127.0.0.1:8080" 의 값이 ModelAttribute에 의해 Form 클래스의 IpPort로 컨버전 되어서 찍혔다고 보면
나머지 하나는 왜 찍혔는지 이해가 안 갑니다.
강의 12:38 에도 보면 같은 로그가 마지막에 2개 찍혀 있네요
답변 2
4
한참 지났지만 후에 공부하실 분들을 위해 남깁니다.
위에 두 분의 답을 보고 저도 궁금해서 찾아보다 아래의 블로그를 발견했습니다.
https://hyeon9mak.github.io/model-attribute-without-setter/
매개변수가 없는 기본 생성자가 있으면 setter를 통해 바인딩을 먼저 시도하고
그후 매개변수가 있는 생성자를 통해 한번 더 바인딩을 시도한다는 내용입니다.

위와 같이 @Data 대신 @Getter 하나만을 사용하면 기본 생성자가 없어서 setter를 통한 바인딩을 시도하지 않아 로그가 한번만 찍히는 것을 확인했습니다.
0
안녕하세요. Jay님
스프링 MVC 내부에서 확인이 필요해서 이런 과정을 거치는 것으로 추정되는데요.
관련해서 자세히 아시는 분 있으면 답변 부탁드려요.
감사합니다.
안녕하세요. 저도 같은 내용이 궁금해서 질문했었는데요.
https://www.inflearn.com/questions/727358/stringtoipconverter-가-2번-호출되는-이유
디버깅해보면서 분석한 내용 올려봅니다. 정확하지 않은 내용이므로 영한님과 서포터즈분들의 검토 부탁드리겠습니다.
@ModelAttribute로 선언한 매개변수에 값이 들어가는 과정은 1.객체만들기, 2.바인딩 으로 이루어집니다. 2.바인딩에 의해 컨버터가 동작할 것이고, 호출횟수는 한 번이라 예상했습니다. 하지만 실제로는 두 번 호출되는 것을 확인했습니다. 디버깅을 해본 결과, 1.객체만들기 과정에서도 컨버터가 동작하는 것을 확인할 수 있었습니다.
1.객체만들기 과정을 따라가다보면 ModelAttributeMethodProcessor.constructAttribute() 메서드를 확인할 수 있습니다. 단순히 빈 깡통 객체를 만드는 줄 알았지만, 데이터를 집어넣는 동작까지 합니다. 자세한 코드는 아래를 참조하시면 됩니다.
protected Object constructAttribute(Constructor<?> ctor, String attributeName, MethodParameter parameter,
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
//생성자로부터 멤버변수가 무엇이 있는지 알아냅니다.
String[] paramNames = BeanUtils.getParameterNames(ctor);
for (int i = 0; i < paramNames.length; i++) {
//Request에 멤버변수명으로 값을 조회합니다.
//"ipPort"로 조회한 결과 -> value:"127.0.0.1"
Object value = webRequest.getParameterValues(paramName);
//컨버팅이 필요한 멤버변수가 있다면 컨버팅을 수행한 결과를 저장합니다.
//"127.0.0.1"을 ipPort로 컨버팅
args[i] = binder.convertIfNecessary(value, paramType, methodParam);
}
// 최종적으로 컨버팅한 결과를 바탕으로 객체를 생성합니다.
return BeanUtils.instantiateClass(ctor, args);
}
아래 코드처럼 binding = false 옵션을 주면, 1.객체만들기만 수행하면서 컨버팅 작업이 한번만 일어나는 것을 확인할 수 있습니다.
@PostMapping("/converter/edit")
public String converterEdit(@ModelAttribute(binding = false) Form form, Model model) {
IpPort ipPort = form.getIpPort();
model.addAttribute("ipPort", ipPort);
log.info("post request end");
return "converter-view";
}화면에 찍히는 결과도 동일합니다.

전체적인 코드의 흐름을 봤을 때, 1.객체만들기 과정에서 바인딩 작업을 먼저 해보고, 바인딩 에러가 발생하지 않았을 경우에 2.바인딩을 하는데요. 왜 번거롭게 바인딩 작업을 두 번 거치는지 잘 모르겠습니다. 아시는 분 있으면 답변 부탁드립니다.
@호춘
저 또한 간단하게 테스트를 해봤습니다.
Form 클래스는 @Data를 통해 모든 필드 접근법을 생성하고 있는데,
여기서 @Data가 아니라 직접 @Getter, @Setter, @NoArgsConstructor, @ToString(@Value, @RequiredArgsConstructor 제외)를 추가하게되면 POST시 한개의 StringToIpPortConverter의 로그가 찍히는것을 알 수 있습니다.
@Data의 사용을 지양하라는 말을 들었는데 이와도 관련된 상황이 아닐까 하여 의견 덧붙입니다
콘솔에 로그 찍어서 여러 케이스에 대해 확인해 본 결과, 더 정돈된 결론이 나왔다고 생각해서 댓글에 공유합니다.
기본적으로는 {파라미터가 없는 기본 생성자로 바인딩할 객체 생성 -> setter로 바인딩}을 원칙으로 합니다.
하지만 기본 생성자가 없더라도 단 하나의 생성자만 존재하는 경우에는 스프링이 유연하게 허용해줍니다.
이때 생성자를 호출하려면 IpPort가 필수적이기 때문에 어쩔 수 없이 컨버터가 1회 호출되고, 원칙적으로 setter 바인딩을 위해 setter를 호출하기 때문에 여기서 컨버터가 또 1회 호출됩니다.
즉, 해당 예제에서 컨버터가 1회만 호출되도록 하려면 다음 두 가지 전략 중 하나를 선택할 수 있습니다.
파라미터가 없는 빈 생성자를 추가하고, setter를 유지
생성자는 그대로 유지하고, setter를 제거(또는 @ModelAttribute(binding=false) 옵션 설정)
아래 제 블로그에서 검증을 위한 여러가지 케이스에서 로깅과 함께 설명해뒀으니, 더 자세한 확인을 원하시는 분들은 참고하셔도 좋겠습니다.
https://hyoyoonnam.github.io/posts/@ModelAttribute%EC%97%90-%EB%8C%80%ED%95%9C-%EC%83%9D%EC%84%B1%EC%9E%90,-setter-%EB%B0%94%EC%9D%B8%EB%94%A9/