김영한 선생님의 Spring MVC 2편의 검증 부분을 학습하고 있던 중,
문득 BindingResult
의 구동 방식에 대해서 궁금해졌다.
스프링에서 Binding이란, 클라이언트가 요청한 값이 객체에 알맞게(자바 프로퍼티 규약) binding되는 것을 의미하는 듯 하다.
예를 들자면,
타임리프에서 POST 메서드로 받아온 데이터 값들이, 객체에 @ModelAttribute
에 의해, 그리고 자바 프로퍼티 규약에 의해, 객체의 인스턴스 변수에 바인딩된다거나.. 하는 것들이 있을 것이다. (역시 말로 표현하는 것은 너무 어렵다.)
생명공학도의 입장에서, Binding이란 너무나 친숙한 단어이기 때문에, Binding 자체에 대한 이해는 쉽게 할 수 있었던 것 같다.
다만, 많은 개발자들이 시간을 쏟는 예외 처리와 검증을 숙지하기 위해서는,
BindingResult의 메커니즘은 조금 더 깊게 생각해볼 필요가 있는 것 같다.
왜 이런 규칙이 생겼을까?
답은 구동 방식에 있는 듯 했다.
클라이언트가 POST 요청으로 data를 서버로 보냄 (타임리프가 데이터를 처리)
@ModelAttribute
가 타임리프에서 가져온 data를 객체에 Binding
Binding
결과를 말 그대로 바인딩 결과인 BindingResult
에 담아둔다.
위와 같은 구동 방식이 맞는지(내 가설이 맞는지) 확인하고자 디버깅을 돌려보았다.
내 웹페이지에는 총 3개의 값을 입력해야 하며, 각각 다음의 값을 입력해주었다.
디버깅 결과, 바인딩되는 객체에 다음과 같이 값이 담겼다.
@ModelAttribute Item item
// 타입 에러로 인해, 바인딩이 안되므로 값이 넘어가지 않는다.
// item: "Item(id=null, itemName=, price=null, quantity=null)"
BindingResult bindingResult
// 대충 매우 길고 무서운 말이 적혀있음.
price
와 quantity
에 대한 typeMismatch
라고 알려주고있었다. (bindingFailure가 아닌 오류는 굳이 안알려주는듯 했다.)price
와 quantity
의 rejectedValue
가 [q]임을 명시해주고 있었다.디버깅을 통해, @ModelAttribute
에 선 바인딩 후, BindingResult
가 구동됨을 확인할 수 있었다.
위의 의문점을 알아보기위해, 마저 디버깅을 돌려봤다.
@ModelAttribute Item item
// item: "Item(id=null, itemName=, price=null, quantity=null)"
// 검증 로직
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(),
false, null, null, "상품 이름은 필수 입니다."));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(),
false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(),
false, null, null, "수량은 최대 9,999 까지 허용합니다."));
}
위 객체는 코드 구성 상, itemName부터 검증에 들어간다. StringUtils.hasText()로 검증했다.
값이 없으므로, 첫 번째 검증 로직에 걸렸다. 신기한 점은, 원래 2개의 error(typeMismatchError 2개)만 명시했던 BindingResult가 3개의 에러를 명시하게 되었다는 것이다.
그 다음은 똑같다. price
에 대한 사용자 지정 에러가 추가되고,
quantity
에 대한 사용자 지정 에러가 추가되어,
총 5개의 에러가 BindingResult에 담기게 된다.
그리고 검증에 실패했으므로, 입력 폼으로 다시 돌아간다.
BindingResult
에 담긴 값을 보여준다.
그리고 스텝 오버를 누르려고했는데 손이 미끄러지는 바람에(?) 스텝 인투를 눌러서 영한쌤이 그려준 그림 (디스패처 서블릿 -> 핸들러 조회 -> 핸들러 어댑터 조회 -> 핸들러 어댑터 -> 핸들러 호출 -> 뷰 리졸버 -> ,,,)까지 탐방하고 왔다. 사실 그림보다 한 10배는 복잡했던 것 같다. 한번쯤 해볼만 한 것 같다.
아무튼 우리가 알고있는 루트로 뷰를 전달하고, 그 뷰에서는 아래와 같은 결과를 반환한다.
Type Mismatch로 인한 바인딩 에러의 rejectValue도, 우리가 만들어준 검증 로직의 에러와 똑같이 관리되는 것 같다. 다만, 다른점은 바인딩 에러는 생성 후, BindingResult에 자동으로 담긴다는 것이고, 우리가 지정한 에러는 검증 후 담긴다는 것이다.
즉, 우리가 만든 에러는 rejectedValue를 지정해주지 않으면 작성했던 값이 날아가지만, TypeMismath로 인한 바인딩 에러의 rejectedValue는 자동으로 값이 지정되어 작성했던 값이 절대 날아가지 않는다.
바인딩 에러
TypeMismatch field error 발생 -> rejectedValue 등, FieldError 관련 값이 자동으로 담긴 FieldError객체 생성 및 BindingResult에 담아줌 -> 자동으로 잘못 입력한 값을 다시 전송해줌 -> 에러 메시지 출력
개발자가 만든 에러
검증 로직 -> 여러 정보가 담긴 FieldError 객체 생성 및 BindingResult에 담아줌 -> rejectedValue가 있으면 잘못 입력한 값을 다시 전송해주고, 없으면 값이 날아감 -> 에러 메시지 출력
클라이언트가 POST 요청으로 data를 서버로 보냄 (타임리프가 데이터를 처리)
@ModelAttribute
가 타임리프에서 가져온 data를 객체에 Binding
Binding결과, field 에러인 type mismatch error가 발생하면, 스프링이 자동으로, 에러가 발생한 필드(price, quantity
)의 rejectValue에 q를 담아 FieldError 객체를 생성한다. 그리고 BindingResult
에 담아준다. (이 때, 객체에는 아무런 값도 담기지 않는다! null
이 담겨있음! 그렇기 때문에, 검증 로직으로 인한 에러 메시지도 출력된다.)
입력 폼으로 이동되고, 위 사진과 같은 에러 메시지를 출력한다.
클라이언트가 POST 요청으로 data를 서버로 보냄 (타임리프가 데이터를 처리)
@ModelAttribute
가 타임리프에서 가져온 data를 객체에 Binding
개발자가 직접 만든 검증 로직을 수행함.
클라이언트가 잘못 적은 데이터가 개발자가 직접 만든 검증 로직의 rejectedValue
에 담기고, FieldError
객체를 생성한 후, BindingResult
에 담아준다.
검증에 실패하였으므로, 입력 폼으로 이동되며 rejectedValue
에 담겨있던 값을 출력한다.
선대 개발자들이 하사해주신 @Slf4j
의 log
를 사용해보자. 아주 쉽게 무엇이 담겼는지 확인할 수 있다.
--log.info(bindingResult = {}, bindingResult)
--
bindingResult = org.springframework.validation.BeanPropertyBindingResult: 5 errors
--TypeMismatch로 인한 BindingError--
Field error in object 'item' on field 'price': rejected value [q];
Field error in object 'item' on field 'quantity': rejected value [q];
--개발자가 만든 검증 로직에 의한 FieldError--
Field error in object 'item' on field 'itemName': rejected value [];
Field error in object 'item' on field 'price': rejected value [null];
Field error in object 'item' on field 'quantity': rejected value [null];
각 에러 별로, 할당된 RejectedValue를 확인해보자.
저 RejectedValue가 입력 칸에 출력되는 것이다.
주의할 점은, TypeMismatch로 인한 바인딩 에러가 더 먼저 처리되기 때문에, 바인딩 에러의 rejectedValue
가 입력 칸에 출력되고 그 뒤의 rejectedValue는 무시된다.
이 누추한 곳에 방문객이 계실지 모르겠지만,
혹여나 들어오셔서 잘못된 정보가 눈에 띄실 경우,
귀중한 시간을 조금만 내주셔서 지적 해주시면 감사드리겠습니다.
좋은 글 감사합니다. 덕분에 많이 배웠습니다~!