Spring boot "BindingResult"에 관한 고찰

김설영·2022년 6월 20일
0

MyDiscussions

목록 보기
2/4
post-thumbnail

김영한 선생님의 Spring MVC 2편의 검증 부분을 학습하고 있던 중,

문득 BindingResult의 구동 방식에 대해서 궁금해졌다.

스프링에서 Binding이란, 클라이언트가 요청한 값이 객체에 알맞게(자바 프로퍼티 규약) binding되는 것을 의미하는 듯 하다.
예를 들자면,
타임리프에서 POST 메서드로 받아온 데이터 값들이, 객체에 @ModelAttribute에 의해, 그리고 자바 프로퍼티 규약에 의해, 객체의 인스턴스 변수에 바인딩된다거나.. 하는 것들이 있을 것이다. (역시 말로 표현하는 것은 너무 어렵다.)

생명공학도의 입장에서, Binding이란 너무나 친숙한 단어이기 때문에, Binding 자체에 대한 이해는 쉽게 할 수 있었던 것 같다.

다만, 많은 개발자들이 시간을 쏟는 예외 처리와 검증을 숙지하기 위해서는,
BindingResult의 메커니즘은 조금 더 깊게 생각해볼 필요가 있는 것 같다.


BindingResult는 반드시 ModelAttribute 다음에 와야 한다.

왜 이런 규칙이 생겼을까?

답은 구동 방식에 있는 듯 했다.

  1. 클라이언트가 POST 요청으로 data를 서버로 보냄 (타임리프가 데이터를 처리)

  2. @ModelAttribute가 타임리프에서 가져온 data를 객체에 Binding

  3. Binding결과를 말 그대로 바인딩 결과인 BindingResult에 담아둔다.

위와 같은 구동 방식이 맞는지(내 가설이 맞는지) 확인하고자 디버깅을 돌려보았다.

내 웹페이지에는 총 3개의 값을 입력해야 하며, 각각 다음의 값을 입력해주었다.

  1. String itemName : 입력 안함.
  2. Integer price : 'q'
  3. Integer quantity : 'q'

디버깅 결과, 바인딩되는 객체에 다음과 같이 값이 담겼다.

@ModelAttribute Item item
// 타입 에러로 인해, 바인딩이 안되므로 값이 넘어가지 않는다.
// item: "Item(id=null, itemName=, price=null, quantity=null)"
BindingResult bindingResult
// 대충 매우 길고 무서운 말이 적혀있음. 
  • 대충 길고 무서운 말에는, 2개의 에러가 item의 price, quantity field에 있다는 말이 적혀있었다. 또한,
    • 이 에러는 pricequantity에 대한 typeMismatch라고 알려주고있었다. (bindingFailure가 아닌 오류는 굳이 안알려주는듯 했다.)
    • 그리고, 중요한 점은, pricequantityrejectedValue가 [q]임을 명시해주고 있었다.

디버깅을 통해, @ModelAttribute에 선 바인딩 후, BindingResult가 구동됨을 확인할 수 있었다.


그렇다면, 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 까지 허용합니다."));
        }
  1. 위 객체는 코드 구성 상, itemName부터 검증에 들어간다. StringUtils.hasText()로 검증했다.
    값이 없으므로, 첫 번째 검증 로직에 걸렸다. 신기한 점은, 원래 2개의 error(typeMismatchError 2개)만 명시했던 BindingResult가 3개의 에러를 명시하게 되었다는 것이다.

  2. 그 다음은 똑같다. price에 대한 사용자 지정 에러가 추가되고,

  3. quantity에 대한 사용자 지정 에러가 추가되어,

  4. 총 5개의 에러가 BindingResult에 담기게 된다.

  5. 그리고 검증에 실패했으므로, 입력 폼으로 다시 돌아간다.

  6. BindingResult에 담긴 값을 보여준다.

그리고 스텝 오버를 누르려고했는데 손이 미끄러지는 바람에(?) 스텝 인투를 눌러서 영한쌤이 그려준 그림 (디스패처 서블릿 -> 핸들러 조회 -> 핸들러 어댑터 조회 -> 핸들러 어댑터 -> 핸들러 호출 -> 뷰 리졸버 -> ,,,)까지 탐방하고 왔다. 사실 그림보다 한 10배는 복잡했던 것 같다. 한번쯤 해볼만 한 것 같다.

아무튼 우리가 알고있는 루트로 뷰를 전달하고, 그 뷰에서는 아래와 같은 결과를 반환한다.

Type Mismatch로 인한 바인딩 에러의 rejectValue도, 우리가 만들어준 검증 로직의 에러와 똑같이 관리되는 것 같다. 다만, 다른점은 바인딩 에러는 생성 후, BindingResult에 자동으로 담긴다는 것이고, 우리가 지정한 에러는 검증 후 담긴다는 것이다.

즉, 우리가 만든 에러는 rejectedValue를 지정해주지 않으면 작성했던 값이 날아가지만, TypeMismath로 인한 바인딩 에러의 rejectedValue는 자동으로 값이 지정되어 작성했던 값이 절대 날아가지 않는다.

바인딩 에러
TypeMismatch field error 발생 -> rejectedValue 등, FieldError 관련 값이 자동으로 담긴 FieldError객체 생성 및 BindingResult에 담아줌 -> 자동으로 잘못 입력한 값을 다시 전송해줌 -> 에러 메시지 출력

개발자가 만든 에러
검증 로직 -> 여러 정보가 담긴 FieldError 객체 생성 및 BindingResult에 담아줌 -> rejectedValue가 있으면 잘못 입력한 값을 다시 전송해주고, 없으면 값이 날아감 -> 에러 메시지 출력


정리하자면...

바인딩 오류(Type Mismatch...)의 경우

  1. 클라이언트가 POST 요청으로 data를 서버로 보냄 (타임리프가 데이터를 처리)

  2. @ModelAttribute가 타임리프에서 가져온 data를 객체에 Binding

  3. Binding결과, field 에러인 type mismatch error가 발생하면, 스프링이 자동으로, 에러가 발생한 필드(price, quantity)의 rejectValue에 q를 담아 FieldError 객체를 생성한다. 그리고 BindingResult에 담아준다. (이 때, 객체에는 아무런 값도 담기지 않는다! null이 담겨있음! 그렇기 때문에, 검증 로직으로 인한 에러 메시지도 출력된다.)

  4. 입력 폼으로 이동되고, 위 사진과 같은 에러 메시지를 출력한다.

개발자가 만든 오류의 경우

  1. 클라이언트가 POST 요청으로 data를 서버로 보냄 (타임리프가 데이터를 처리)

  2. @ModelAttribute가 타임리프에서 가져온 data를 객체에 Binding

  3. 개발자가 직접 만든 검증 로직을 수행함.

  4. 클라이언트가 잘못 적은 데이터가 개발자가 직접 만든 검증 로직의 rejectedValue에 담기고, FieldError객체를 생성한 후, BindingResult에 담아준다.

  5. 검증에 실패하였으므로, 입력 폼으로 이동되며 rejectedValue에 담겨있던 값을 출력한다.


더 쉽게 다가갈 수 있는 방법..

선대 개발자들이 하사해주신 @Slf4jlog를 사용해보자. 아주 쉽게 무엇이 담겼는지 확인할 수 있다.

--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는 무시된다.


이 누추한 곳에 방문객이 계실지 모르겠지만,
혹여나 들어오셔서 잘못된 정보가 눈에 띄실 경우,
귀중한 시간을 조금만 내주셔서 지적 해주시면 감사드리겠습니다.

profile
블로그 이동하였습니당! -> https://kimsy8979.tistory.com/

1개의 댓글

comment-user-thumbnail
2022년 10월 13일

좋은 글 감사합니다. 덕분에 많이 배웠습니다~!

답글 달기