Spring의 Bean Validation
Spring

Spring의 Bean Validation

반응형

1.Bean Validation 소개

 

Spring의 Validation이란

Spring_Boot_Study/4.spring-MVC2/message at master · NamHyeop/Spring_Boot_Study GitHub - NamHyeop/Spring_Boot_Study: Spring 공부를 하며 기록한 자료들입니다. Spring 공부를 하며 기록한 자료들입니다. C..

hyeophyeop.tistory.com

  • 이런 복잡한 로직을 Spring에서는 Bean Validation을 통해 간단하게 검증을 할 수 있다.
  • 아래의 복잡한 이전 코드와 Spring Bean Validation을 사용한 코드를 비교해보자
  • 비교해본다면 Spring Bean Validation을 사용하고 싶어질 것이다.
  • 이 글에서는 Bean Validation은 무엇이고 어떻게 동작하며 사용시 주의점에 대해 설명한다.

복잡한 이전 Validation 검증 코드


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);
            }
        }
    }
}

Bean Validation을 사용해서 간단하게 검증 수행

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;
      //...
}

검증 어노테이션의 종류

@NotBlank

  • 빈값 + 공백만 있는 경우를 허용하지 않는다.

@NotNull

  • null을 허용하지 않는다.

@Range(min={숫자}, max = 100000)

  • 숫자 범위 이내의 값이어야 한다.

@Max(9999)

  • 매개변수만큼의 숫자까지만 허용한다.

💡 필요한 어노테이션이 없다면? 아래 사이트를 참조

Hibernate Validator 7.0.4.Final - Jakarta Bean Validation Reference Implementation: Reference Guide

[Hibernate Validator 6.2.3.Final - Jakarta Bean Validation Reference Implementation: Reference Guide

Validating data is a common task that occurs throughout all application layers, from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone. To avoid duplication of th

docs.jboss.org](https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-gettingstarted-createmodel%5D(https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-gettingstarted-createmodel))

빈 테스트 방법

package hello.itemservice.validation;

import hello.itemservice.domain.item.Item;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapProperties;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

public class BeanValidationTest {

    @Test
    void beanValidation(){
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Item item = new Item();
        item.setItemName(" "); //공백인 경우
        item.setPrice(0);
        item.setQuantity(10000);

        //오류가 있을경우 violations에 담기게 된다.
        Set<ConstraintViolation<Item>> violations = validator.validate(item);
        for(ConstraintViolation<Item> violation : violations){
            System.out.println("violation = " + violation);
            System.out.println("violation = " + violation.getMessage());
        }
    }
}
  • 먼저 Validation.buildDefaultValidatorFactory() 메소드를 사용하여 Validator를 가져올 수 있는 팩토리를 생성한다.
  • 이후 Validator를 가져온다.
  • Validator에 테스트할 객체를 매개변수로 넘겨준다.
  • 결과적으로 오류값이 있을 경우 Set 매개변수에 담기게 된다.

💡 Spring은 어떻게 Bean Validator를 사용하는가?

  • Spring은 spring-boot-starter-validation 라이브러리를 넣으면 자동으로 Bean Validator를 스프링에 등록한다.
  • 이후 LocalValidatorFactoryBean을 글로벌 Validator로 등록한다.
  • 글로벌 설정이므로 어노테이션 기반 검증을 사용할 수 있는것이다.
  • 검증 오류가 발생할 경우 이전 Validation 설명와 똑같이 BindingResult에 오류를 담아서 반환한다.

검증 순서

  1. @ModelAttribute로 넘어온 각각의 필드에 타입 변환을 시도한다.
    1. 성공할 경우 다음 절차 수행
    2. 실패할 경우 typeMismatch로 FieldError를 추가한다.
  2. Bean Validator를 적용한다.

Bean Validation의 에러코드는 어떻게 처리하나요?

  • Bean Validation의 오류코드를 보면 이전 Validation 설명때 했던 오류 코드와 유사한 것을 확인할 수 있다.
  • 이러한 이유는 Bean Validation의 오류코드 또한 MessageCodeResolver를 통해 메시지 코드가 생성되기 때문이다.
  • 생성순서는 아래와 같다.
    • 어노테이션.객체.필드명,
    • 어노테이션.필드명
    • 어노테이션.자료형타입
    • 어노테이션
  • 이다.
  • 생성되는 규칙을 활용해 Bean Validation의 오류코드도 자유롭게 등록할 수 있다.
@NotBlank
NotBlank.item.itemName
NotBlank.itemName 
NotBlank.java.lang.String 
NotBlank
  • 실제 메시지 등록은 아래와 같이 등록한다.
#Bean Validation 추가 
NotBlank={0} 공백X 
Range={0}, {2} ~ {1} 허용 
Max={0}, 최대 {1}
  • 0은 필드명을 의미하고 1과 2는 각각 어노테이션마다 다르지만 순차적으로 값을 가져오는 공통점이 있다.

Bean Validation의 Object Error는 어떻게 처리하나요?

  1. 아래와 같이 @SrciptAssert()를 사용할 수 있는데 권장하지 않는다.
@Data
  @ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >=
  10000")
  public class Item {
//...
}
  • 왜냐하면 실제 제약이 너무나도 많고 복잡하다. 또한 해당 객체의 범위를 넘어서는 경우들도 존재하는데 그런 경우 대응이 어렵다.
  • 그렇기 때문에 아래처럼 그냥 자바코드로 작성하는것이 필자의 추천방식이다.
//V3에서는 @Validate를 사용해서 좀더 검증 과정을 간편화 하는 작업을진행한다.
    //@PostMapping("/add")
    //검증 로직을 빼버려도 @Validated를 추가하면 Validated가 Item을 검색한다. dependencies에 Validation 추가문을 넣어주었다는 전제하에서이다.
    public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
        //별도의 Object검증로직 추가
                if(item.getPrice() != null && item.getQuantity() != null){
            int resultPrice = item.getPrice() * item.getQuantity();
            if(resultPrice < 10000){
                bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
            }
        }
        if(bindingResult.hasErrors()){
            log.info("errors = {}", bindingResult);
            return "validation/v3/addForm";
        }

        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v3/items/{itemId}";
    }

그렇다면 Bean Validation이 검증 방식에서 최고냐? 한계점은 없냐?

  • 아래처럼 한 가지의 요구사항으로 인해 Side Effect 효과가 날 수 있는 경우 문제가 된다.

  • 등록시 기존 요구사항
    • 타입 검증
    • 가격, 수량에 문자가 들어가면 검증 오류 처리
  • 필드 검증
    • 상품명: 필수, 공백X
    • 가격: 1000원 이상, 1백만원 이하
    • 수량: 최대 9999
  • 특정 필드의 범위를 넘어서는 검증
    • 가격 * 수량의 합은 10,000원 이상
  • 수정시 요구사항
    • 등록시에는 quantity 수량을 최대 9999까지 등록할 수 있지만 수정시에는 수량을 무제한으로 변경할 수있다.(기존 요구사항과 충돌)
    • 등록시에는 id 에 값이 없어도 되지만, 수정시에는 id 값이 필수이다.(기존 요구사항과 충돌)
package hello.itemservice.domain.item;
@Data
public class Item {
            @NotNull //수정 요구사항 추가 private Long id;
      @NotBlank
      private String itemName;
      @NotNull
      @Range(min = 1000, max = 1000000)
            private Integer price;
      @NotNull
            //@Max(9999) //수정 요구사항 추가 
            private Integer quantity;
            //...
}

  • 이런식으로 Bean Validation 충돌로 인해 한계점이 존재한다.

Bean Validated Groups 기능을 사용하면 될 거 같은데?

  • 실무에서는 addForm 형식과 editForm 형식을 분리해서 관리한다.
  • 회원가입 할 때 넣는 데이터와 회원수정할 때 넣는 데이터가 다르기 때문이다.
  • Bean Groups 기능을 사용하면 코드가 복잡해지므로 권장하지 않는다.

그러면 어떻게 하라고? → 전송 객체를 분리해

  • 실무에서는 전송 객체를 분리한다.
  • 폼에 전달하는 데이터와 도메인 객체가 다르기 때문이다.
    • 상품을 등록할 때와 수정할 때의 입력 양식이 같은가? →X
    • 회원가입과 수정의 입력 양식이 같은가? → X
  • 그렇기 때문에 별도의 DTD를 만들어서 관리하는 방법을 추천한다.

그러면 Bean Validation에서 HTTP Message Converter는 어떻게 동작하나요?

  • Spring에 대해서 깊은 탐구를 하게 된다면 JSON, XML등 입력 양식에 맞게 HTTP Message Converter가 동작하는것을 알 수 있다.
  • HTTP Message Converter는 어떻게 Bean Validation에서 동작하는지 설명하겠다.
  • API 요청은 3가지로 나누어지듯이HTTP Message Converter의 동작 방식도 아래의 3가지로 나누어지며 @ModelAttribute와 @RequestBody 두 가지 요청으로 나누어진다.
    1. 성공
      1. 말 그대로 성공이다. 알아볼 필요가 없다.
    2. 실패
      1. JSON을 객체로 생성하는게 실패한 경우이다.(@ModelAttribute는 하나라도 실패해도 검증 수행함)
      2. HTTP메시지 컨버터는 JSOIN 객체로 생성하는게 실패할 경우 컨트롤러 자체가 호출되지 않고 그 전에 예외가 발생한다.
      3. 당연히 Validator도 실행되지 않는다.
    3. 검증 오류 요청
      1. JSON을 객체로 생성한 경우 Validator까지는 진행한다.
      2. @ModelAttribute는 하나라도 실패해도 검증을 수행한다.
      3. 이후 해당 오류 유형에 맞는 ObjectError와 FieldError를 반환한다.
  • 간단하게 정리하면 @ModelAttribue는 필드 단위로 적용되기 때문에 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 돼서 Validator를 사용하여 검증된다.
  • 그러나 문제는 JSON 형태의 @RequestBody 타입이 오는 경우다. 이 경우에는 JSON 데이터를 HttpMessageConverter가 하나의 필드라도 오류가 있을 경우 변환을 진행하지 못한다. 이로 인해 컨트롤러도 호출되지 않고 Validator도 적용 될 수없다.
    • 즉 별도의 예외 처리가 필요하다.

REFERENCES

0.https://www.inflearn.com/course/스프링-mvc-2/dashboard

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

1.https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-gettingstarted-createmodel

 

Hibernate Validator 6.2.3.Final - Jakarta Bean Validation Reference Implementation: Reference Guide

Validating data is a common task that occurs throughout all application layers, from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone. To avoid duplication of th

docs.jboss.org

 

반응형

'Spring' 카테고리의 다른 글

Spring의 필터와 인터셉터  (0) 2022.07.01
Spring의 쿠키,세션  (0) 2022.06.29
Spring 메시지, 국제화  (0) 2022.06.27
Spring의 Validation이란  (0) 2022.06.27
Spring MVC 구조 및 구현  (0) 2022.04.16