폼 값 검증과 에러 메시지 처리는 어플리케이션 개발에서 중요하다.
폼 값 검증과 에러 메시지 처리를 위한 2가지 기능이다.
스프링 MVC에서 커맨드 객체의 값이 올바른지 검사하기 위해 두가지 인터페이스를 사용한다.
객체를 검증할 때 사용하는 Validator 인터페이스
package org.springframework.validation;
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Error erros);
}
package controller;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import spring.RegisterRequest;
public class RegisterRequestValidator implements Validator {
private static final String emailRegExp =
"^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@" +
"[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
private Pattern pattern;
public RegisterRequestValidator() {
pattern = Pattern.compile(emailRegExp);
System.out.println("RegisterRequestValidator#new(): " + this);
}
@Override
public boolean supports(Class<?> clazz) {
return RegisterRequest.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
System.out.println("RegisterRequestValidator#validate(): " + this);
RegisterRequest regReq = (RegisterRequest) target;
if (regReq.getEmail() == null || regReq.getEmail().trim().isEmpty()) {
errors.rejectValue("email", "required");
} else {
Matcher matcher = pattern.matcher(regReq.getEmail());
if (!matcher.matches()) {
errors.rejectValue("email", "bad");
}
}
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required");
ValidationUtils.rejectIfEmpty(errors, "password", "required");
ValidationUtils.rejectIfEmpty(errors, "confirmPassword", "required");
if (!regReq.getPassword().isEmpty()) {
if (!regReq.isPasswordEqualToConfirmPassword()) {
errors.rejectValue("confirmPassword", "nomatch");
}
}
}
}
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required");
if (regReq.getEmail() == null || regReq.getEmail().trim().isEmpty()) {
errors.rejectValue("email", "required");
}
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required");
위 코드에서 if조건문 내부의 코드와 ValidationUtils클래스의 rejectIfEmptyOrWhitespace메서드를 이용한 코드는 동일한 기능을 제공한다.
ValidationUtils.rejectIfEmptyOrWhitespace메서드를 실행할 때 검사 대상 객체인 target을 파라미터로 전달하지 않았는데 어떻게 target 객체의 "name" 프로퍼티의 값을 검사할까?
@PostMapping("/register/step3")
public String handleStep3(RegisterRequest regReq, Errors errors) {
new RegisterRequestValidator().validate(regReq, errors);
if (errors.hasErrors())
return "register/step2";
try {
memberRegisterService.regist(regReq);
return "register/step3";
} catch (DuplicateMemberException ex) {
errors.rejectValue("email", "duplicate");
return "register/step2";
}
}
try{
...아이디와 비밀번호 인증 코드
} catch(WrongIdPasswordException ex) {
errors.reject("notMatchingIdPassword");
return "login/loginForm";
}
요청 매핑 어노테이션을 붙인 메서드에 Errors 타입의 파라미터를 추가할 때 주의할 점은 Errors 타입 파라미터는 반드시 커맨드 객체를 위한 파라미터 다음에 위치해야한다.
그렇지 않고, Errors 타입 파라미터가 커맨드 객체 앞에 위치하면 익셉션이 발생한다.
//Errors 타입 파라미터가 커맨드 객체 앞에 위치하면 실행 시점에 에러 발생
@PostMapping("register/step3")
public String handleStep3(Errors errors, RegisterRequest regReq) {
...
}
@PostMapping("register/step3")
public String handleStep3(RegisterRequest regReq, BindingResult errors) {
new RegisterRequestValidator().validate(regReq, errors);
...
}
Errors 인터페이스가 제공하는 에러 코드 추가 메서드 종류
defaultMessage 파라미터를 가진 메서드를 사용하면, 에러 코드에 해당하는 메시지가 존재하지 않을 때 익셉션을 발생시키는 대신 메시지를 출력한다.
ValidationUtils 클래스가 제공하는 메서드
rejectIfEmpty()메서드는 field에 해당하는 프로퍼티 값이 null이거나 빈 문자열("")인 경우 에러 코드로 errorCode를 추가한다.
rejectIfEmptyOrWhitespace() 메서드는 null이거나 빈 문자열인 경우, 공백 문자(스페이스, 탭)로만 값이 구성된 경우 에러코드를 추가한다.
스프링 MVC는 @Valid 어노테이션을 사용해서 커맨드 객체에 검증 기능을 적용할 수 있다.
글로벌 범위 Validator를 적용하기 위한 두가지 설정
글로벌 범위 Validator 설정 방법
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
@Override
public Validator getValidator() {
return new RegisterRequestValidator();
}
}
RegisterController 에 @Valid 어노테이션 사용 예시
@PostMapping("/register/step3")
public String handleStep3(@Valid RegisterRequest regReq, Errors errors) {
if (errors.hasErrors())
return "register/step2";
try {
memberRegisterService.regist(regReq);
return "register/step3";
} catch (DuplicateMemberException ex) {
errors.rejectValue("email", "duplicate");
return "register/step2";
}
}
!주의 사항
@Valid 어노테이션을 사용할 때 Errors 타입 파라미터가 없으면 검증 실패시 400에러를 응답한다.
ex) 예시 코드
public String handleStep3(@Valid RegisterRequest regReq)
RegisterRequestValidator 클래스는 RegisterRequest 타입의 객체만 검증할 수 있으므로 모든 컨트롤러에 적용할 수 있는 글로벌 범위 Validator를 사용하는 것은 적합하지 않다.
@InitBinder 어노테이션을 이용하면 컨트롤러 범위 Validator를 설정할 수 있다.
컨트롤러 범위 Validator 설정 RegisterController 코드
@PostMapping("/register/step3") public String handleStep3(@Valid RegisterRequest regReq, Errors errors) { if (errors.hasErrors()) return "register/step2";
try {
memberRegisterService.regist(regReq);
return "register/step3";
} catch (DuplicateMemberException ex) {
errors.rejectValue("email", "duplicate");
return "register/step2";
}
}
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new RegisterRequestValidator());
}
- @Vaild 어노테이션을 적용하기 때문에 handleStep3() 메서드에는 Validator 객체의 validate() 메서드를 호출하는 코드가 없다.
- @InitBinder 어노테이션을 적용한 메서드는 WebDataBinder 타입 파라미터를 갖는데 WebDataBinder#setValidator() 메서드를 이용해서 컨트롤러 범위에 적용할 Validator를 설정할 수 있다.
- binder.setValidator(new RegisterRequestValidator()); --> __RegisterRequest 타입을 지원하는 RegisterRequestValidator를 컨트롤러 범위 Validator로 설정했으므로 @Valid 어노테이션을 붙인 RegisterRequest를 검증할 때 이 Validator를 사용한다.__ RegisterRequestValidator.java에 정의되어 있다.
- @InitBinder가 붙은 메서드는 컨트롤러의 요청 처리 메서드를 실행하기전에 매번 실행된다. ex) handle1(), handle2(), handle3() 이라는 메서드가 있다면 각각의 모든 메서드를 실행하기 전에 initBinder() 메서드를 매번 호출해서 WebDataBinder를 초기화 한다.
#### 글로벌 범위 Validator와 컨트롤러 범위 Validator의 우선 순위
> setVaildator() 메서드를 사용하면 글로벌 범위 Validator 대신에 컨트롤러 범위 Validator를 사용한다.
>
> addValidator() 메서드를 실행하면 순서상 글로벌 범위 Validator() 뒤에 새로 추가한 컨트롤러 범위 Validator()가 추가된다.
### Bean Validation을 이용한 값 검증 처리
> Bean Validation이 제공하는 어노테이션을 이용해서 커맨드 객체의 값을 검증하는 방법
- Bean Validation과 관련된 의존을 설정에 추가한다.
- 커맨드 객체에 @NotNull, @Digits 등의 어노테이션을 이용해서 검증 규칙을 설정한다.
> pom.xml과 build.gradle을 이용해서 의존설정을 추가한다.
> 커맨드 클래스는 다음과 같이 Bean Validation과 프로바이더가 제공하는 어노테이션을 이용해서 값 검증 규칙을 설정한다.
```java
public class RegisterRequest {
@NotBlank
@Email
private String email;
@Size(min = 6)
private String password;
@NotEmpty
private String confirmPassword;
@NotEmpty
private String name;
}
Bean Validation 어노테이션을 사용했다면 OptionalValidationFactoryBean 클래스를 빈으로 등록해야 된다.
@EnableWebMvc 어노테이션을 사용하면 OptionalValidationFactoryBean을 글로벌 범위 Validator로 등록된다.
@Configuration
@EnableWebMvc //OptionalValidationFactoryBean을 글로벌 범위 Validator로 등록
public class MvcConfig implements WebMvcConfigurer {
...
}
사용할 컨트롤러의 메서드에 @Valid 어노테이션 사용해서 글로벌 범위 Validator로 검증한다.
@PostMapping("/register/step3")
public String handleStep3(@Valid RegisterRequest regReq, Errors errors) {
if (errors.hasErrors()) return "register/step2";
try {
memberRegisterService.regist(regReq);
return "register/step3";
} catch (DuplicateMemberException ex) {
errors.rejectValue("email", "duplicate");
return "register/step2";
}
}
@AssertTrue, @AssertFalse : 지원 타입 boolean, Boolean
@DecimalMax, @DecimalMin : 지원 타입 BigDecimal, BigInteger, CharSequence, 정수타입
@Max, @Min : 지원 타입 BigDecimal, BigInteger, 정수타입
@Digits : 지원 타입 BigDecimal, BigInteger, CharSequence, 정수타입
@Size : 지원 타입 CharSequence, Collection, Map, 배열
@Null, @NotNull
@Pattern : 지원 타입 CharSequence
@NotEmpty : 지원 타입 CharSequence, Collection, Map, 배열
@NotBlank : 지원 타입 CharSequence
@Posititve, @PositiveOrZero : 지원 타입 BigDecimal, BigInteger, 정수타입
@Negative, @NegativeOrZero : 지원 타입 BigDecimal, BigInteger, 정수타입
@Email : 지원 타입 CharSequence
@Future, @FutureOrPresent : 지원 타입 시간 관련 타입 ex) new Date(), ... 등
@Past, @PastOrPresent : 지원 타입 시간 관련 타입 ex) new Date(), ... 등