티스토리 뷰

카테고리 없음

@Valid, @BindingResult

✨✨✨✨✨✨✨ 2022. 5. 22. 19:45
반응형

springMvc2_validation

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - Validation

4장 Validation

  • V1

    • errors?.containsKey('itemName')

    • <div th:if="${errors?.containsKey('globalError')}">
          <p class="field-error" th:text="${errors['globalError']}">오류 메시지</p>
      </div>
  • V2

    • Java

      • BindingResult

      • addError

        • FieldError
        • ObjectError
      • bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수 입니다."));
      • bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
      • bindingResult.hasErrors()

  • Thymeleaf

    • #fields 로 bindingResult가 제공하는 검증 오류에 접근할 수 있다

    • ${#fields.hasGlobalErrors()}

    • <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">글로벌 오류</p>
    • th:errorclass="field-error"

    • error인 경우 class추가 **th:field="*{itemName}"**의 이름과 맞춰 BindingResul
      new Field("item", "itemName", "MSG") 있는경우 오류를 표시한다.

    • <input type="text" id="itemName" th:field="*{itemName}"
        th:errorclass="field-error"
          class="form-control" placeholder="이름을 입력하세요">
    • th:error="*{itemName}"

      • <div class="field-error" th:errors="*{itemName}"> 오류메시지 </div>
  • BindingResult

    • BindingResult가 없으면

      • 400오류가 발생하면서 컨트롤러가 호출되지않고 오류페이지로 이동한다.
    • 있으면

      • 오류정보(Field)를 BindingResult에 담아서 컨트롤러를 정상 호출한다.

      • <div class="field-error" th:errors="*{itemName}"> 오류메시지 </div>
      • image-20220409230824820
    • 주의)

      1. BindingResult는 검증할 대상 다음에 와야한다

        public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult
      2. BindingResult는 Model에 자동으로 포함된다.

그런데 위 그림을 보면 Validator 검증 후 BindingResult에 의해 아래 메시지는 출력되지만, 사용자 입력 값는 유지안된다.

  • 가격은 1,000원 ~ 1,000,000 까지 허용합니다.

  • public FieldError(String objectName, String field, String defaultMessage);
  • public FieldError(String objectName, String field
    , @Nullable Object rejectedValue, boolean bindingFailure, @Nullable String[] codes
        , @Nullable Object[] arguments, @Nullable String defaultMessage)
    • new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~
      1,000,000 까지 허용합니다.")
      
      // 만약 codes를 null로 하면 defaultMessage가 선택된다.
      /*
      Field error in object 'item' on field 'itemName': rejected value []; 
      codes []; arguments []; default message [상품 이름은 필수 입니다.]
      */
    • bindingResult.addError(
        new FieldError("item", "itemName", item.getItemName()
                       , false, new String[]{"required.item.itemName"}, null,null)
      );
      
      // required.item.itemName로 code가 정해져있다.
      
      /*
      Field error in object 'item' on field 'itemName': rejected value []; 
      codes [required.item.itemName]; 
      arguments []; default message [null]
      */
    • th:field="*{price} 타임리프의 th:field 는 매우 똑똑하게 동작하는데,
      정상 상황에는 모델 객체의 값(Item item)을 사용하지만, 오류가 발생하면 FieldError 에서 보관한 값을 사용해서 값을 출력한다.

/* 
	errorCode 를 원래는 "required.item.itemName"인데
	item 어떤 객체인지, 어떤 필드인지 알면
required.객체명.필드명 으로 붙여서 따라간다.
*/
/*
	여기서 재미있는 점은 "required.item.itemName"뿐만 아니라, 의 메시지 모두 우선순위로 사용된다.
	- required.item.itemName
	- required.itemName
	- required.java.lang.String
	- required
*/
bindingResult.rejectValue("itemName", "required");
/*
Field error in object 'item' on field 'itemName': 
rejected value []; codes 
[
  required.item.itemName,
  required.itemName,
  required.java.lang.String,
  required
]; arguments []; default message [null]
*/
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);

이렇게하면 범용적인 메시지를 하나하나 지정할 필요없게되기때문에
중요하지않은 메시지를 관리하기 편해진다.

정리)

  1. rejectValue()를 호출

  2. MessageCodeResolver를 사용해서 messageCode를 생성

  3. new FieldError()를 생성하면서 메시지 코드들을 보관

    • @Override
      public void rejectValue(@Nullable String field, String errorCode
                              , @Nullable Object[] errorArgs, @Nullable String defaultMessage) {
      	///// 생략 //// 
        String fixedField = fixedField(field);
        Object newVal = getActualFieldValue(fixedField);
        
        // resolveMessageCodes를 통해 code 생성
        FieldError fe = new FieldError(getObjectName(), fixedField, newVal, false,
                        resolveMessageCodes(errorCode, field), errorArgs, defaultMessage);
        addError(fe);
      }
      
      
      // resulverMessageCodes
      @Override
      public String[] resolveMessageCodes(String errorCode, @Nullable String field) {
        return getMessageCodesResolver().resolveMessageCodes(
          errorCode, getObjectName(), fixedField(field), getFieldType(field));
      }
  4. th:errors 에서 메시지코드들로 메시지를 순서대로 찾고 노출

이제는 숫자입력폼에 문자입력 시 뱉는 에러를 문구를 만들어보자

image-20220410112818873

다음과 같이 숫자부분에 String을 입력하면 다음과 같은 오류를 뱉어내고,
bindingResult를 찍어보면 아래와 같이 표시된다.

log.info("bindingResult {} ", bindingResult);

// Spring이 자동으로 제공해주는 오류코드
타입을 자동으로 체크해준다.

Field error in object 'item' on field 'price': rejected value [A]; **codes **[
- typeMismatch.item.price,
- typeMismatch.price,
- typeMismatch.java.lang.Integer,
- typeMismatch
];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.price,price];
arguments []; default message [price]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.lang.Integer' for property 'price'; nested exception is java.lang.NumberFormatException: For input string: "A"]

// 개발자가 지정한 오류코드

Field error in object 'item' on field 'price': rejected value [null]; codes [range.item.price,range.price,range.java.lang.Integer,range]; arguments [1000,1000000]; default message [null]

위 내용을 보면 자동으로 타입매치에 대한 오류코드를 만들어 주었기에 properties에 아래와같이 추가한다.

#추가
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.

image-20220410113329049


image-20220416121759322




  • 생각해보면 null, 금액range등으로 이를 어노테이션을 통해 체크할 생각을 함

  • Build.gradle 아래 내용을 정의함으로써
    어노테이션에 대한 인터페이스와 구현체가 정의된 라이브러리를 다운받는다.

    implementation 'org.springframework.boot:spring-boot-starter-validation'
      
    // Jakarta Bean Validation
    // jakarta.validation-api : Bean Validation 인터페이스 
    // hibernate-validator 구현체

위 라이브러리를 추가함으로써 @Validated를 선언하고
Validation을 체크해주는 @NotNull, @NotBlank등을 사용할 수 있다.

image-20220416190116827




@ModelAttribute뿐만아니라 @RequestBody으로도 Validated를 체크할 수 있다.
차이점은 아래와 같다.

image-20220416191541417

@ModelAttribute vs @RequestBody
HTTP 요청 파리미터를 처리하는 @ModelAttribute 는 각각의 필드 단위로 세밀하게 적용된다. 
그래서 특정 필드에 타입이 맞지 않는 오류 발생해도 나머지 필드는 정상 처리할 수 있었다. 
HttpMessageConverter@ModelAttribute 와 다르게 각각의 필드 단위로 적용되는 것이 아니라, 전체 객체 단위로 적용된다.
따라서 메시지 컨버터의 작동이 성공해서 Item 객체를 만들어야 @Valid , @Validated  적용된다.
  
@ModelAttribute 는 필드 단위로 정교하게 바인딩이 적용된다. 
  특정 필드 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다.
  
@RequestBodyHttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 (price에 문자열을 넣는다 라던지)
  이후 단계 자체 진행되지 않고 예외 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다.
반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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 31
글 보관함