Spring - Validation(검증)

김승호·2024년 12월 17일
0

1. Validation 사용 - BindingResult

1) Spring에서 제공하는 BindingResult를 사용해서 Error을 등록하고 사용할 수 있다.

2) 또한 타임리프와 연동해서 사용이 가능하다.

참고

1) 에러를 담당할 객체 다음에 매개변수로 BindingResult가 와야 된다.

public String addItemV3(
            @ModelAttribute Item item,
            BindingResult bindingResult){

2) 자동으로 Model에 등록 된다.

3) message , 국제화 기능을 사용할 수 있다.

4) 데이터 바인딩 에러 발생의 경우

데이터 형변환의 경우에는 Controller에 접근하지 못하고 Error가 발생함으로 Spring에서 직접 BindingResult에 messageCode를 만든 FieldError을 담아준다. (아래 설명)

1-2. 사용법

1. 간편화 등록 - reject(글로벌에러) , rejectValue(필드에러)

fieldError , ObjectError 등록을 간편화한 구조이다.
1. rejectvalue
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
1) field : 오류 코드
2) errorCode : 오류 필드명
3) errorArgs : 오류 메시지에서 {0} 을 치환하기 위한 값
4) defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지

//field
bindingResult.rejectValue("price","range", new Object[]{1000,1000000} , null);
//클로벌 (errorCode만 오류 코드가 빠진다.)
bindingResult.reject("totalPriceMin" , new Object[]{10000,resultPrice} , null);

reject() , rejectValue를 사용 시 messageCodesResolver()가 작동되어서 message 코드를 등록해준다. (아래 설명)

2. bindingResult.addError()

1) 매개변수 new FieldError(필드에러) , new ObjectError(글로벌 에러)를 통해 필드 or 글로벌 에러를 등록하고 사용이 가능하다
2) 에러가 발생한 곳에 해당 함수를 사용해서 사용할 수 있다.

2개의 오버로딩 제공

  1. public FieldError(String objectName, String field, String defaultMessage);
    1-1) objectName : 객체명
    1-2) field : 오류가 발생한 필드명
    1-3) defaultMessage : 오류 메세지
bindingResult.addError(new FieldError("item","quantity","수량은 최대 9,999 까지 허용합니다"));
  1. public FieldError(String objectName, String field, @Nullable Object rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable
    Object[] arguments, @Nullable String defaultMessage)
    1) objectName : 오류가 발생한 객체 이름
    2) field : 오류 필드
    3) rejectedValue : 사용자가 입력한 값(거절된 값)
    4) bindingFailure : : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
    5) codes : 메시지 코드 - message 기능 사용
    6) argument : 메시지에서 사용하는 인자
    7) defaultMessage : 기본 오류 메시지
///case - 1 (message 사용 안함)
bindingResult.addError(new FieldError("item","quantity", item.getQuantity(), false , null , null ,"수량은 최대 9,999 까지 허용합니다"));
/// case - 2 (message 사용)
bindingResult.addError(new FieldError("item","itemName", item.getItemName(), false , new String[]{"required.item.itemName"} , null ,null));

1-3. 분리

1. @Validate를 사용한다.

1) InitBinder에 해당하는 검증 객체를 등록 후

    @InitBinder
    public void init(WebDataBinder dataBinder){
        dataBinder.addValidators(itemValidator);
    }

2) 해당하는 객체 앞에 @Validated를 설정한다.

public String addItemV6(@Validated @ModelAttribute Item item, BindingResult 
bindingResult, RedirectAttributes redirectAttributes)

글로벌 설정

}
 @SpringBootApplication
 public class ItemServiceApplication implements WebMvcConfigurer {
 public static void main(String[] args) 
 {
 	SpringApplication.run(ItemServiceApplication.class, args);
 }
@Override
 public Validator getValidator() 
 {
 	return new ItemValidator();
 }

해당처럼 설정하면 모든 컨트롤러에 적용된다.

2. implements Validator를 할당받은 객체를 직접 선언

3. Validator 설명

 public interface Validator {
 	boolean supports(Class<?> clazz);
 	void validate(Object target, Errors errors);
 }

1) supports() {} : 해당 검증기를 지원하는 여부 확인(뒤에서 설명)
2) validate(Object target, Errors errors) :
: 검증 대상 객체와 BindingResult
ex) 구현 예시 코드

@Override
    public boolean supports(Class<?> clazz) {
        return Item.class.isAssignableFrom(clazz);
        //넘어서는 클래스가 Item의 자식이거나 Item 자체냐?
    }
    //Errors BindingResult의 부모클래스
    @Override
    public void validate(Object target, Errors errors) {
        Item item = (Item) target;
        //Null , 공백 , 길이 체크
        if(!StringUtils.hasText(item.getItemName())){
            //errors.addError(new FieldError("item","itemName", item.getItemName(), false , new String[]{"required.item.itemName"} , null ,null));
            errors.rejectValue("itemName","required" ); // Code 구현 : errorCode.가르키는Object.field
        }
	}

2. Validation 사용 - BeanValidation

매우 일반적인 로직 검증에는 Spring에서 제공하는 BeanValidation을 적용할 수 있다.
예시 코드

public class Item {
 private Long id;
    @NotBlank private String itemName;
    @NotNull
    @Range(min = 1000, max = 1000000) private Integer price;
    @NotNull
    @Max(9999)private Integer quantity;
 }

1. 동작원리

LocalValidatorFactoryBean을 글로벌 Validator로 등록한다. 이 Validator는 보고 검증을 수행한다. 이렇게 글로벌 Validator가 적용되어 있기 때문에 @Validated , @Valid , @NotNull만 적용하면 된다.검증 오류가 발생하면, FieldError , ObjectError같은 애노테이션을 생성해서 BindingResult 에 담아준다.

참고사항

1) 직접 글로벌 Validator을 등록하면 LocalValidatorFactoryBean을 등록하지 않기 때문에 주의하자
2) 검증 시 @Validated , @Valid 둘 다 사용 가능하지만, @Validated에는 group 추가 기능이 있다
3) 글로벌 오류 일 경우에는 BeanValidation보다는 자바코드로 직접 사용을 권장한다.

2-1 검증 순서 - @ModelAttribute

1.@ModelAttribute 각각의 필드에 타입 변환 시도
1-1) 성공하면 다음으로
1-2) 실패 시 typeMismatch로 FieldError() 추가
2. Validator 적용

2-2 검증 순서 - @RequestBody

  1. @RequestBody를 사용하는 경우, BindingResult를 지원하지 않는다.
    why? @RequestBody에서 객체 바인딩과 검증 실패가 발생하면, Spring은 바로 예외를 던지기 때문이다.
  2. @RequestBody와 @Valid를 사용할 때 검증 실패 시
    1) Spring이 객체 매핑 후 Bean Validation을 수행합니다.
    2) Bean Validation 실패 시 MethodArgumentNotValidException이 발생합니다.
    3) 컨트롤러 메서드로 진입하지 않고 예외 처리기로 전달됩니다.

3. BeanValidation에 message 사용

ex) item 객체에 itemName에서 에러 발생 시
@NotBlank
1. NotBlank.item.itemName
2. NotBlank.itemName
3. NotBlank.java.lang.String
4. NotBlank

4. 에러 메세지 적용 순서

  1. MessageSource에서 적용된 에러 찾기
  2. 속성에 있는 message 값 사용
  3. 라이브러리 제공 기본값 사용

5. 하나의 객체에 BeanValidation을 다르게 적용하는 방법

1. 도메인을 요구 사항에 따라서 분리하고 검증 Bean을 적용 시킨다..

BeanValidation , 직접 등록 동시 사용 시 주의사항

BeanValidation으로 등록한 검증과 직접 등록한 검증이 중복 동작 될 수 있음으로 주의하자!

3. 개인적 정리

  1. Global Error의 경우 = 직접 자바코드로 new ObjectError or reject를 구현
  2. Field Error의 경우
    2-1) BeanValidation을 사용한다.
    2-2) 안될 경우 직접 자바코드로 등록한다.

타임리프 적용

1. ObjectError()에러 확인

  1. th:if="${#fields.hasGlobalErrors()}“ : GlobalError 확인
  2. th:each를 통한 모든 글로벌 에러 추출
  3. #fields로 `BindingResult가 제공하는 검증 오류에 접근할 수 있다.
 <div th:if="${#fields.hasGlobalErrors()}">
    <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">전체 오류 메시지</p>
 </div>

2. FieldError()에러 확인

  1. th:errors="*{itemName}“
    해당 에러는 th:if , th:text를 생략해준 문장
 <input type="text" id="itemName" th:field="*{itemName}"
 th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
 <div class="field-error" th:errors="*{itemName}">상품명 오류</div>

개념설명 : messageCodesResolver()

매개변수로 메세지를 등록하는 코드이며 주로 reject(), rejectvalue() 내부에서 해당 함수가 사용되어 messageCode를 등록한다.

void MessageCodesFields(){
        String[] name = messageCodesResolver.resolveMessageCodes("required", "item" , "itemName", String.class);
        Assertions.assertThat(name).containsExactly("required.item.itemName","required.itemName","required.java.lang.String","required");
    }
  1. 필드의 경우
    1.: errorcode + "." + object name + "." + field = required.item.itemName
    2.: errorcode + "." + field
    3.: errorcode + "." + field type
    4.: errorcode
  2. 글로벌의 경우
    1.: code + "." + object name
    2.: code

개념설명 : 데이터 바인딩 에러

  1. typeMismatch.item(객체).price(필드명)
  2. typeMismatch.price(필드명)
  3. typeMismatch.java.lang.Integer(필드 타입)
  4. typeMismatch
    를 BindingResult에 담아서 반환해준다.

참고

스프링MVC2편

profile
백준 : https://www.acmicpc.net/user/tmdghdhkdw (골드 - 2)

0개의 댓글