Spring의 Validation이란
Spring

Spring의 Validation이란

Spring_Boot_Study/4.spring-MVC2/message at master · NamHyeop/Spring_Boot_Study

 

GitHub - NamHyeop/Spring_Boot_Study: Spring 공부를 하며 기록한 자료들입니다.

Spring 공부를 하며 기록한 자료들입니다. Contribute to NamHyeop/Spring_Boot_Study development by creating an account on GitHub.

github.com

1.검증이 필요한 이유

  • 사용자가 잘못된 입력값을 넣었을 경우 정상적인 요청인지 검증을 해야한다. 만약 검증을 안할 경우 아래와 같은 상황이 발생할 수 있다.
    • 만약 이름 입력란에 공백을 넣고 입력했는데 이름이 입력되지 않았습니다가 출력이 안된다면?
    • 검증을 하지 않고 Http 요청을 Server에 요청했는데 Server가 사용자에게 404 에러를 출력한다면?
      • 고객 : 나 회원가입 안해!
      • 결국 검증 기능을 수행하지 않을 경우 유저 이탈로 이끌게 된다.
  • 검증은 크게 클라이언트와 서버 두 영역에서 가능하다.
  • 클라이언트 검증은 빠르나(ex:자바스크립트) 조작할 수 있고 보안에 취약하다.
    • Postman으로 냅다 그냥 Json으로 Server에 공격한다면?
  • 그러나 서버로만 검증을 수행하면 고객 속도 적인 측면과 자원 사용 부분에서 효율성이 떨어진다.
  • 그렇기 때문에 두 방법(클라이언트 검증, 서버 검증)을 적절히 섞어서 사용해야한다.

검증 처리 목표와 예제

  • 예시를 통해서 검증로직에 대한 이해를 돕도록 해보겠다.
  • 아래 그림처럼 사용자가 Get 요청을 통해서 상품 등록폼 을 요청한다.
  • 그러나 가격이나 수량으로 인해 상품저장이 실패할 경우 사용자가 입력한 Data양식에 맞게 아래 그림처럼 상품 등록 폼에 데이터를 추가하여 반환해줘야한다.
  • 그렇지 않으면 상품에 대한 정보를 사용자가 다시 추가해줘야 하고 이는 사용자의 불편함을 야기하여 서비스 이탈을 하게 되는 계기가 된다.
  • 즉 Server 개발자 사용자가 입력한 양식 오류를 Server에서 다시 입력하도록 Client에게 정보를 주어야 하며 입력한 양식이 정상적인 데이터인지를 검증해야 하는 의무가 있다.
  • 검증 도중 발생하는 오류로 인해 사용자 정보를 유지시키는 방법과 검증 로직을 작성하는 방법을 예제 코드를 보면서 설명하겠다.

예제 Controller

//경로
package hello.itemservice.web.validation;

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

@PostMapping("/add")
    public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

        if(bindingResult.hasErrors()){
            log.info("errors = {}", bindingResult);
            return "validation/v2/addForm";
        }

        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v2/items/{itemId}";
    }
  • 예제 코드에서 중점적으로 볼것은 @Validated, BindingResult, @InitBinder 아래 코드이다.
    • WebDataBinder는 해당 Controller 영역에서 도착할 때마다 검증로직을 자동으로 적용해주는 정도로만 이해하자
  • @Validate 어노테이션이 사용된것을 볼 수 있다.
  • @Validate 어노테이션을 통해서 Spring에 등록된 검증 로직에 접근하게 된다.
  • 즉 Controller에 도달한 이후 검증로직을 거치고 나서 본래의 Controller로직을 수행하는것이다.
  • BindingResult는 밑에서 설명하겠다.

Validate 검증 로직

//경로
package hello.itemservice.web.validation;

import hello.itemservice.domain.item.Item;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

@Slf4j
@Component
public class ItemValidator implements Validator {

    //supports는 검증기의 유형이 맞는지를 확인하는 함수다. 이 값이 true가 나와야만 정상 동작함.
    //검증기의 종류가 여러개가 될 수 있으므로 존재하는 함수다.
    @Override
    public boolean supports(Class<?> clazz) {
        return Item.class.isAssignableFrom(clazz);
        //item == clazz (검증 대상 클래스와 비교 가능)
        //item == subItm (검증 대상의 자식 클래스 또한 비교 가능)
    }

    //Validator를 상속받고 검증의 논리 과정에 대한 코드를 작성하면 된다.
    //errors안에 BindingResult 들어간다. BindingResult가 errors의 자식이기 때문이다.
    @Override
    public void validate(Object target, Errors errors) {
        Item item = (Item) target;

        if(!StringUtils.hasText(item.getItemName())){
            errors.rejectValue("itemName", "required");
        }
        if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
            //bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}, null));
            errors.rejectValue("price", "range.item.price", new Object[]{1000, 10000000}, null);
        }
        if(item.getQuantity() == null || item.getQuantity() >= 9999){
            //bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, new String[]{"max.item.quantity"}, new Object[]{9999},  null));
            errors.rejectValue("quantity", "max", new Object[]{9999}, null);
        }
        //특정 필드가 아닌 복합 룰 검증
        if(item.getPrice() != null && item.getQuantity() != null){
            int resultPrice = item.getPrice() * item.getQuantity();
            if(resultPrice < 10000){
                //defaultMessage값에 값을 안주고 codes, argument에만 null값을 주는 것도 가능하다.
                //bindingResult.addError(new ObjectError("item", new String[]{"totalPriceMin"}, new Object[]{10000, resultPrice}, null));
                errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
            }
        }
    }
}
  • Validator 인터페이스를 상속하고 supports와 validate를 재정의한다.
  • supports는 검증해야할 객체에 대한 인스턴스를 명시해줘야한다
  • supports는 여러개의 검증기를 등록할 경우 검증 객체 타입에 맞는 객체가 들어왔을 경우에만 검증해주기 위해 존재하는 메소드이다.
  • validate 메소드에서는 객체에 대한 검증을 진행하는 로직을 명시해주기 위해 존재한다.
    • 현재 예시기준 Item 객체에 대한 검증을 진행하는 로직을 가지고 있다.
  • Controller에서는 BindingResult를 통해 Error로직을 전달해주지만 validate에서는 Errors로 명시되어있다.
  • Errors가 BindingResult의 부모이기 때문에 인터페이스가 Errors로 설정되어있는것이다.
  • 이후 검증로직중에서 errors의 메소드에 대해서 집중적으로 분석해보겠다.

BindingResult가 뭐죠?

💡 rejectValue(), Reject()는 뭔가요?, FieldError는요? ObjectError는요?

  • BindingResult는 스프링이 제공하는 검증 오류를 보관하는 객체이다.
    • 즉. 검증 오류가 발생되면 오류코드가 BindingResult에 보관되는것이다.
    • 주의. BindingResult bindingResult 파라미터의 위치는 항상 @ModelAttribute {객체} {변수} 다음에 와야 한다.
    • BindingResult 내부에서 검증 객체에 대한 정보를 알아야지만 오류코드를 미리 담을수 있기 때문이다.
  • BindingResult에 담을 수 있는 오류 종류는 FieldError와 ObjectError로 나누어진다.
  • FieldError와 ObjectError는 Message로 관리되는 오류 코드를 조회하기 위해 존재한다
  • FieldError는 필드 자체에 대한 입력값이 틀렸을 경우를 의미한다.
    • 예를 들어 Integer 자료형 값에 문자형을 넣었을 경우가 있다.
    • FieldError의 생성자
      • 2가지가 존재한다
      • public FieldError(String objectName, String field, String defaultMessage)
        • objectName : @ModelAttribute
        • field : 오류가 발생한 필드 이름
        • defaultMessage: 오류 기본 메시지
      • public FieldError(String objectName, String field, @Nullable Object rejectedValue, boolean bindingFailure, @Nullalble String[] ocdes, @Nullable Ibject[] arguments, @Nullable String defaultMessage)
        • objectName: 오류가 발생한 객체 이름
        • field: 오류 필드
        • rejectedValue : 사용자가 입력한 값(거절된 값) → 자료형이 틀려도 틀린 자료를 보관해주는 파라미터
        • bindingFailure: 타입 오류 가은 바인딩 실패인지, 검증 실패인지 구분 값
        • codes: 메시지 코드
        • arguments: 메시지에 사용하는 인자
        • defaultMessage: 기본 오류 메시지
    • objectName.field로 오류를 해석한다
  • ObjectError는 필드 값은 정상적으로 들어왔으나 Object의 로직 자체에 문제가 있을 경우를 의미한다.
    • 예를 들어 물건의 총합이 4만원 이상인 경우
    • ObjectError의 생성자(Field와 다르게 생성자는 한 개이다.)
    • public ObjectError(String objectName, String defaultMessage)
    • objectName : @ModelAttribute의 이름
    • defaultMessage: 오류기본 메시지
    • FileldError처럼 입력자체에는 문제가 없기때문에 field 부분에 대한 정보가 없다.
    • ObjectName으로 오류를 찾는다.
  • rejectValue는 첫 번째 상품주문 로직에서 Item에 대한 정보를 입력했을때 정상적인 입력이 입력되지 않았을 경우
  • RejectValue와 Reject는BindingResult의 복잡한 코드를 단순환 시킨것이다.
  • 검증 로직 코드에서 BindingResult를 주석시킨 코드를 첨부하였다.
    • 굉장히 복잡하며 사용하기 어렵다.
  • 그러나 BindingResult의 이해를 해야만 rejcetValue(),reject() 메소드의 사용의 깊은 이해를 할 수 있기에 간단히 설명하겠다.

중간정리

  • BindingResult의 FieldError와 ObjectError에 대해서 살펴보았다.
  • 느꼈겠지만 매개변수도 굉장히 많고 사용하기 어렵다.
  • 그래서 rejcetValue와 rejcet를 사용하는것이다!
  • 그러나 rejectValue와 reject의 내부동작을 이해하기 위해서는 FieldError와 ObjectError를 이해해야하기 때문에 앞에서 먼저 설명하였다.
  • 지금 부터는 rejcetValue와 reject의 내부동작을 이해해보자

rejectValue(), reject()

  • FieldError의 대체자이며 BindingResult의 오류 메시지를 담는 객체인 rejectValue()에 대해서 설명하겠다.
  • rejcetValue()의 생성자
    • void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String ,defaultMessage)
    • field : 오류 필드명
    • errorCode : 오류 코드(이 오류 코드는 메시지에 등록된 코드가 아니다. 뒤에서 설명할 messageResolver를 위한 오류 코드이다.)
    • errorArgs : 오류 메시지에서 {0} 을 치환하기 위한 값
    • defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
  • 자세히 보면 필드 정보가 없음에도 ErrorMessage를 매치할 수 있다.
  • 이는 @ModelAttribute 뒤에 BindingResult가 오는 이유이기도 하다.
    • 내부에서 MessageCodeResolver가 이러한 기능을 지원해준다.
  • reject()의 생성자
  • void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
    • errorCode : 오류 코드(이 오류 코드는 메시지에 등록된 코드가 아니다. 뒤에서 설명할 messageResolver를 위한 오류 코드이다.)
    • errorArgs : 오류 메시지에서 {0} 을 치환하기 위한 값
    • defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지

오류 메시지 설정

  • 오류 메시지는 관리를 하도록 쉽게 messages와 errors로 설정하자
  • applications.properties에 아래와 같이 설정해야한다.
  • 검색 순위는 구체적인 순서로 검색이 된다.
spring.messages.basename=messages,errors
//errors.properties
#required.item.itemName=상품 이름은 필수입니다.
#range.item.price=가격은 {0} ~ {1} 까지 허용합니다. 
#max.item.quantity=수량은 최대 {0} 까지 허용합니다. 
#totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
#==ObjectError==
#Level1
totalPriceMin.item=상품의 가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
#Level2 - 생략
totalPriceMin=전체 가격은 {0}원 이상이어야 합니다. 현재 값 = {1}
#==FieldError==
#Level1
required.item.itemName=상품 이름은 필수입니다. 
range.item.price=가격은 {0} ~ {1} 까지 허용합니다. 
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
#Level2 - 생략
#Level3
required.java.lang.String = 필수 문자입니다.
required.java.lang.Integer = 필수 숫자입니다.
min.java.lang.String = {0} 이상의 문자를 입력해주세요.
min.java.lang.Integer = {0} 이상의 숫자를 입력해주세요.
range.java.lang.String = {0} ~ {1} 까지의 문자를 입력해주세요.
range.java.lang.Integer = {0} ~ {1} 까지의 숫자를 입력해주세요.
max.java.lang.String = {0} 까지의 문자를 허용합니다.
max.java.lang.Integer = {0} 까지의 숫자를 허용합니다.
#Level4
required = 필수 값 입니다.
min= {0} 이상이어야 합니다.
range= {0} ~ {1} 범위를 허용합니다.
max= {0} 까지 허용합니다.

💡 시간이 괜찮다면 ValidationUtils를 공부하시면 좀 더 단축되고 가독성 좋은 코드를 작성하실수 있습니다!

REFERENCE

'Spring' 카테고리의 다른 글

Spring의 Bean Validation  (0) 2022.06.28
Spring 메시지, 국제화  (0) 2022.06.27
Spring MVC 구조 및 구현  (0) 2022.04.16
스프링 빈 기능  (0) 2022.04.12
Spring의 종류와 장점  (0) 2022.04.12