Bean Validation

JP·2022년 1월 8일
0

spring

목록 보기
3/5

Validation, 검증

  • 값의 입력에 있어 해당 값이 유효한지에 대한 검증을 하는 로직
    - 타입검증
    - 숫자가 들어가야한다거나, 대문자만 들어가야한다는 등의 로직
    • 필드 검증
      • 필수값인지, 공백이 허용되는지, 몇부터 몇 이상의 숫자가 들어가야하는지
        • 최소, 최대값은 얼마인지 등..
    • 특정 필드가 아닌 복합적인 룰 검증
      • 가격 x 수량이 10000원 이상이어야만 무료배송이 가능하다는 등의 요구사항 검증

등의 요구사항등이 존재할 수 있다.

폼 입력시에 이러한 요구사항에 대한 오류들이 빈번하게 발생하게 된다면 유저들은 금방 사이트를 떠나게 될 것 이기에 입력에 대한 에러가 무엇이 발생했는지 친절하게 알려주어야 한다.

클라이언트의 검증

일반적으로 클라이언트 단에서 JS를 이용한 검증을 한다. 하지만 이에 대한 단점으로는

  • 클라이언트 검증은 조작할 수 있으므로 보안에 취약하다(ex: POSTMAN)

서버에서의 검증

  • 반면 서버만으로 필드값들의 검증을 시행하면 유연한 대처가 안되기에 고객 사용성이 부족해지고 이는 고객의 이탈로 이어질 수 있다.

따라서 클라이언트와 서버의 검증 둘을 적절하게 섞어서 사용해야 한다.

오늘 알아 볼 것은 스프링에서 사용되는 검증 기술에 대한 것이다.

BindingResult

  • BindingResult는 Errors를 상속받은 인터페이스로 검증 오류를 보관한다.
  • BindingResult가 있으면 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다
    - BindingResult가 없으면 -> 에러 발생시 바로 오류 페이지로 이동
    • BindingResult가 있으면 -> 오류정보를 BindingResult에 담아서 컨트롤러를 호출
      - ex. 타입을 잘 못 입력해도 컨트롤러 호출함
      • 그러면 FieldError를 생성해서 바로 BindingResult에 넣어준다.
 @PostMapping("/add")
public String save(@ModelAttribute SearchForm searchForm, BindingResult bindingResult){
    if(!StringUtils.hasText(searchForm.getName())){
            errors.rejectValue("name", "required");
    }
    if (bindingResult.hasErrors()) {
        log.info("errors={} ", bindingResult);
        return "search";
    }
	// 성공로직..
    // ..
}
  • BindingResult는 @ModelAttribute 뒤에 와야 함. 순서 중요!
  • BindingResult는 Model에 안담아도 뷰에 자동으로 넘어간다

Validator

입력을 넘기는 폼이 커질수록 컨트롤러에서의 검증 구현 코드가 길어질 수 밖에 없다.

if(!StringUtils.hasText(searchForm.getName())){
    errors.rejectValue("name", "required");
}
if(.....){
    errors.rejectValue("...", "....");
}
if(.....){
    errors.rejectValue("...", "....");
}
if(.....){
    errors.rejectValue("...", "....");
}
if(.....){
    errors.rejectValue("...", "....");
}

이 로직을 아래와 같이 바꾸어 보았다.
스프링에서 제공해주는 Validator를 사용하였고
사용의 이유는 검증 로직의 분리와 재사용에 있겠다.

import org.springframework.stereotype.Component;
import org.springframework.validation.Validator;

@Component
public class SearchFormValidator implements Validator{
    
}

Validator 인터페이스는 두 가지 메서드를 구현해야한다.
1. supports(Class<?>)
2. validate(Object,Errors)

supports(Class<?>) : 해당 검증기를 지원하는 여부 확인를 확인한다
@Validated 어노테이션은 WebDataBinder에 등록한 검증기를 찾는데, 이 때 여러 검증기 중 어떤 검증기를 지원하는지를 찾는데 그 때 필요한 메서드가 supports(Class<?>)이다.

validate(Object,Errors) : 검증 대상과 Errors(Errors 는 BindingResult의 부모)

따라서 다음과 같이 사용된다

@Component
public class SearchFormValidator implements Validator{

    @Override
    public boolean supports(Class<?> clazz) {
        return SearchForm.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        SearchForm searchForm = (SearchForm) target;

        if(!StringUtils.hasText(searchForm.getName())){
            errors.rejectValue("name", "required");
        }
    }
    
    
}
@Autowired
SearchFormValidator searchFormValidator

@PostMapping("/save")
public String save(@ModelAttribute SearchForm searchForm, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

	searchFormValidator.validate(searchForm, bindingResult);
    //검증에 실패하면 다시 입력 폼으로
    if (bindingResult.hasErrors()) {
        return "search";
    }
    //성공 로직
    //..
    //..
    return "redirect:/items/{id}";
}

위와 같이
validator를 컨트롤러 로직에 넣어서 사용해도 되지만 다음과 같은 방법으로도 사용할 수 있다.

@PostMapping("/save")
public String save(@Validated @ModelAttribute SearchForm searchForm, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
    //검증에 실패하면 다시 입력 폼으로
    if (bindingResult.hasErrors()) {
        return "search";
    }
    //성공 로직
    //..
    //..
    return "redirect:/items/{id}";
}

여기서 @Validated는 검증기를 실행하라는 어노테이션이다.
그런데 이 어노테이션을 사용하기 위해 컨트롤러에 아래의 코드를 넣어주는 사전 작업이 필요하다.

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

컨트롤러 부분 전체 코드

@Controller
@RequiredArgsConstructor
public class ValidateTest{
	private final SearchFormValidator searchFormValidator;
    
    //이 컨트롤러가 호출 될 때 웹바인더가 만들어지고 Validator를 add해준다.
    @InitBinder
    public void init(WebDataBinder dataBinder){
    	dataBinder.addValidators(searchFormValidator);
    }
    
    @PostMapping("/save")
  public String save(@Validated @ModelAttribute SearchForm searchForm, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
      //검증에 실패하면 다시 입력 폼으로
      if (bindingResult.hasErrors()) {
          return "search";
      }
      //성공 로직
      //..
      //..
      return "redirect:/items/{id}";
  }
}

@ValidatedWebDataBinder에 등록한 검증기를 찾는다.
만약 여러 검증기가 등록되었을 경우 아까전에 만들어 둔 supports(Class<?>)에서 어떤 검증기를 찾을 지 파악하고나서 validate()를 호출한다.

그런데..

위와같이 Validator를 매번 만드는건 귀찮고 작업적으로 오버헤드가 발생 할 수 있는 부분이다.
ex. 필드 하나만을 검증해야하는데 validator를 만들거나 컨트롤러에 validation 코드를 추가하는 상황

BeanValidation

이럴 때 유용하게 사용될 수 있는 것이 바로 BeanValidator라는 기술이다.

  • jakarta api는 interface 제공
  • hibernate-validator는 구현체

BeanValidation은 사용하려면 의존 관계를 추가해줘야 한다.

implementation 'org.springframework.boot:spring-boot-starter-validation'

검증에 사용 될 객체에다가 어노테이션을 이용한 검증을 처리한다

public class Car{
	@NotNull
	private String manufacturer;
	
	@NotNull
	@Size(min =2, max=14)
	private String licensePlate;

	@Min(2)
	private int seatCount;

	//...
}

@BeanValidation은 데이터 바인딩에 성공한 필드에 한해서만 검증작업이 이뤄진다.
예를 들어
private int num; 이라는 필드에 qqq 라는 스트링값을 받으면 검증작업 이전에
typeMismatch가 떠버릴 것이다.

하나의 입력폼으로 여러가지의 폼등록을 해야 할 때 사용하는 기술?

  • groups
    - group에 해당하는 interface를 만들고
    - @Validated(SaveCheck.class) 와 같이 쓴다.
    - form으로 넘길 객체에는 다음과 같이 써줘야 한다
public class Car{
      @NotNull(groups = UpdateCheck.class)
      private String manufacturer;

      @NotNull
      @Size(min =2, max=14)
      private String licensePlate;

      @Min(2)
      @NotNull(groups = {SaveCheck.class, UpdateCheck.class})
      private int seatCount;

          //...
      }
  • 왠만하면 각각의 폼을 전달하기 위한 별도의 객체를 사용하는 것이 제일 좋을 것 같다.(dto)
  • json으로 넘어오는 값에 대해서도 검증이 가능하다(@RequestBody)
    - HTTP 메세지 컨버터가 json을 객체로 못바꾸면 컨트롤러 자체를 호출 못한다.
    - json parsing error 등의 객체가 바뀌기 이전에 생기는 에러에 대해서는 잡지 못한다.
profile
to Infinity and b

0개의 댓글