등의 요구사항등이 존재할 수 있다.
폼 입력시에 이러한 요구사항에 대한 오류들이 빈번하게 발생하게 된다면 유저들은 금방 사이트를 떠나게 될 것 이기에 입력에 대한 에러가 무엇이 발생했는지 친절하게 알려주어야 한다.
일반적으로 클라이언트 단에서 JS를 이용한 검증을 한다. 하지만 이에 대한 단점으로는
따라서 클라이언트와 서버의 검증 둘을 적절하게 섞어서 사용해야 한다.
오늘 알아 볼 것은 스프링에서 사용되는 검증 기술에 대한 것이다.
@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";
}
// 성공로직..
// ..
}
입력을 넘기는 폼이 커질수록 컨트롤러에서의 검증 구현 코드가 길어질 수 밖에 없다.
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}";
}
}
@Validated
는 WebDataBinder
에 등록한 검증기를 찾는다.
만약 여러 검증기가 등록되었을 경우 아까전에 만들어 둔 supports(Class<?>)
에서 어떤 검증기를 찾을 지 파악하고나서 validate()를 호출한다.
그런데..
위와같이 Validator를 매번 만드는건 귀찮고 작업적으로 오버헤드가 발생 할 수 있는 부분이다.
ex. 필드 하나만을 검증해야하는데 validator를 만들거나 컨트롤러에 validation 코드를 추가하는 상황
이럴 때 유용하게 사용될 수 있는 것이 바로 BeanValidator라는 기술이다.
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가 떠버릴 것이다.
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;
//...
}