@Valid는 JSR-303 표준 스펙으로써 빈 검증기(Bean Validator)를 이용해 제약 조건을 검증하도록 지시하는 애노테이션이다.
의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
사용 방법
@Getter
@Setter
public class ScheduleRequestDto {
@NotEmpty(message = "제목을 작성해주세요.")
@Size(max = 200, message = "제목은 최대 200자 이내로 작성해주세요.")
private String title;
@NotEmpty(message = "내용을 작성해주세요.")
@Size(max = 400, message = "내용은 최대 400자 이내로 작성해주세요.")
private String content;
@NotEmpty(message = "담당자를 작성해주세요.")
@Email(message = "담당자는 이메일 형식으로 작성해주세요.")
private String writer;
@NotEmpty(message = "비밀번호를 입력해주세요.")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*?_]).{8,16}$",
message = "비밀번호는 최소 8자에서 16자까지, 영문자, 숫자 및 특수 문자를 포함해야 합니다.")
private String password;
}
@PostMapping("/schedules")
public ScheduleResponseDto createSchedule(@RequestBody @Valid ScheduleRequestDto requestDto) {
ScheduleResponseDto responseDto = scheduleService.createSchedule(requestDto);
return responseDto;
}
모든 요청은 프로트 컨트롤러인 디스패처 서블릿을 통해 컨트롤러로 전달된다. 전달 과정에서는 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver가 동작하는데, @Valid 역시 ArgumentResolver에 의해 처리된다.
대표적으로 @RequestBody는 JSON 메세지를 객체로 변환해주는 작업이 ArgumentResolver의 구현체인 RequestResponseBodyMethodProcessor가 처리하며, 이 내부에서 @Valid로 시작하는 애노테이션이 있을 경우에 유효성 검사를 진행한다. (이러한 이유로 커스톰 애노테이션인 @ValidMangKyu도 동작한다.)
만약 @ModelAttribute을 사용한다면 ModelAttributeMethodProcessor에 의해 @Valid가 처리된다.
검증에 오류가 있다면 MethodArgumentNotValidException 예외가 발생되고, 디스패처 서블릿에 기본으로 등록된 예외 리졸버(Exception Resolver)인 DefaultHandlerExceptionResolver에 의해 400 BadRequest 에러가 발생한다.
@Valid는 기본적으로 컨트롤러에서만 동작하며 다른 계층에서는 검증이 되지 않는다. 다른 계층에서 파라미터를 검증하기 위해서는 @Validated와 결합하여야 한다.
입력 파라미터의 유효성 검증은 컨트롤러에서 최대한 처리하고 넘겨주는 것이 좋다. 불가피하게 다른 계층에서 파라미터를 검증해야 할 수 있다. Spring에서는 이를 위해 AOP 기반으로 메소드의 요청을 가로채서 유효성 검증을 진행해주는 @Vaildated를 제공한다. @Vaildated는 JSR 표준 기술이 아니며 스프링 프레임워크에서 제공하는 애노테이션이다.
@Service
@Validated
public class UserService {
public void addUser(@Valid AddUserRequest addUserRequest) {
...
}
}
@Vaildated는 AOP 기반으로 메소드 요청을 인터셉터하여 처리된다. @Vaildated를 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 AOP의 어드바이스 또는 인터셉터(MethodValidationInterceptor)가 등록된다. 그리고 해당 클래스의 메소드들이 호출될 때 AOP의 포인트 컷으로써 요청을 가로채서 유효성 검증을 진행한다.
이러한 이유로 @Vaildated를 사용하면 컨트롤러, 서비스, 레포지토리 등 계층에 무관하게 스프링 빈이라면 유효성 검증을 진행할 수 있다.
동일한 클래스에 대해 제약조건이 요청에 따라 달라질 수 있다. Spring은 이를 위해 제약 조건이 적용될 검증 그룹을 지정할 수 있는 기능을 제공한다.
검증 그룹을 지정하기 위해서는 (내용이 없는)마커 인터페이스를 간단히 정의해야 한다.
public interface UserValidationGroup {}
public interface AdminValidationGroup {}
그리고 해당 제약 조건이 적용될 그룹을 groups 속성을 통해 지정할 수 있다.
@NotEmpty(groups = {UserValidationGroup.class, AdminValidationGroup.class} )
private String name;
@NotEmpty(groups = UserValidationGroup.class)
private String userId;
@NotEmpty(groups = AdminValidationGroup.class)
private String adminId;
그리고 컨트롤러에서도 제약 조건 검증을 적용할 클래스를 지정해주면 된다.
@PostMapping("/users")
public ResponseEntity<Void> addUser(
@RequestBody @Validated(UserValidationGroup.class) AddUserRequest addUserRequest) {
...
}
@PostMapping
public String processOrder(@Valid Order order, Errors errors) {
if(errors.hasErrors()) {
return "orderForm";
}
return "redirect:/";
}
<label for="ccNumber">Credit Card #: </label>
<input type="text" th:field="*{ccNumber}" />
<span class="validationError"
th:if="${#fields.hasErrors('ccNumber')}"
th:errors="*{ccNumber}">CC Num Error
</span>
/**
* Valid 애노테이션을 이용한 유효성 검사 예외처리
* @param e MethodArgumentNotValidException
* @return 예외 상태코드, 메시지
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleException(MethodArgumentNotValidException e){
e.printStackTrace();
BindingResult bindingResult = e.getBindingResult();
StringBuilder builder = new StringBuilder();
for (FieldError fieldError : bindingResult.getFieldErrors()) {
builder.append(fieldError.getField()).append(" : ")
.append(fieldError.getDefaultMessage()).append(", 입력된 값 : ")
.append(fieldError.getRejectedValue()).append("\n");
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ExceptionDto(builder.toString()));
}
| Anotation | 제약조건 |
|---|---|
| @NotNull | Null 불가 |
| @Null | Null만 입력 가능 |
| @NotEmpty | Null, 빈 문자열 불가 |
| @NotBlank | Null, 빈 문자열, 스페이스만 있는 문자열 불가 |
| @Size(min=,max=) | 문자열, 배열등의 크기가 만족하는가? |
| @Pattern(regex=) | 정규식을 만족하는가? |
| @Max(숫자) | 지정 값 이하인가? |
| @Min(숫자) | 지정 값 이상인가 |
| @Future | 현재 보다 미래인가? |
| @Past | 현재 보다 과거인가? |
| @Positive | 양수만 가능 |
| @PositiveOrZero | 양수와 0만 가능 |
| @Negative | 음수만 가능 |
| @NegativeOrZero | 음수와 0만 가능 |
| 이메일 형식만 가능 | |
| @Digits(integer=, fraction = ) | 대상 수가 지정된 정수와 소수 자리 수 보다 작은가? |
| @DecimalMax(value=) | 지정된 값(실수) 이하인가? |
| @DecimalMin(value=) | 지정된 값(실수) 이상인가? |
| @AssertFalse | false 인가? |
| @AssertTrue | true 인가? |
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
@Documented
public @interface Phone {
String message() default "Invalid Phone";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
애노테이션
- @Target({FIELD}) : 해당 애노테이션을 필드에만 선언 가능함
- @Retention(RUNTIME) : 해당 애노테이션이 유지되는 시간으로써 런타임까지 유효함
- @Constraint(validatedBy = PhoneValidator.class) : PhoneValidator를 통해 유효성 검사를 진행함
- @Documented : JavaDoc 생성 시 Annotation에 대한 정보도 함께 생성
속성
- message : 유효하지 않을 경우 반환할 메세지의 기본값
- groups : 유효성 검증이 진행될 그룹
- payload : 유효성 검증 시에 전달할 메타 정보
JSR에서 제공하는 javax.validation의 ConstraintValidator 인터페이스를 구현해야 한다.
public interface ConstraintValidator<A extends Annotation, T> {
default void initialize(A constraintAnnotation) {
}
boolean isValid(T value, ConstraintValidatorContext context);
}
public class PhoneValidator implements ConstraintValidator<Phone, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Pattern pattern = Pattern.compile("\\d{3}-\\d{4}-\\d{4}");
Matcher matcher = pattern.matcher(value);
return matcher.matches();
}
}