[Spring] @Valid와 @Validated를 이용한 유효성 검사

·2024년 5월 20일

spring

목록 보기
7/18

1. 유효성 검사란?

  • 데이터의 값이 유효한지, 조건에 타당한지 확인하는 것이다.
  • 올바르지 않은 데이터를 서버 또는 DB로 전송되는 것을 막기 위함이다.

2. @Vaild를 이용한 유효성 검사

2.1 @Valid의 개념 및 사용 방법

@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;
}
  • 필드에 애노테이션을 통해 제약 조건 설정 후 컨틀롤러의 메소드에 @Valid 애노테이션을 지정하면 된다.
@PostMapping("/schedules")
public ScheduleResponseDto createSchedule(@RequestBody @Valid ScheduleRequestDto requestDto) {
    ScheduleResponseDto responseDto = scheduleService.createSchedule(requestDto);
    return responseDto;
}

2.2 @Valid의 동작 원리

모든 요청은 프로트 컨트롤러인 디스패처 서블릿을 통해 컨트롤러로 전달된다. 전달 과정에서는 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver가 동작하는데, @Valid 역시 ArgumentResolver에 의해 처리된다.

대표적으로 @RequestBody는 JSON 메세지를 객체로 변환해주는 작업이 ArgumentResolver의 구현체인 RequestResponseBodyMethodProcessor가 처리하며, 이 내부에서 @Valid로 시작하는 애노테이션이 있을 경우에 유효성 검사를 진행한다. (이러한 이유로 커스톰 애노테이션인 @ValidMangKyu도 동작한다.)

만약 @ModelAttribute을 사용한다면 ModelAttributeMethodProcessor에 의해 @Valid가 처리된다.

검증에 오류가 있다면 MethodArgumentNotValidException 예외가 발생되고, 디스패처 서블릿에 기본으로 등록된 예외 리졸버(Exception Resolver)인 DefaultHandlerExceptionResolver에 의해 400 BadRequest 에러가 발생한다.

@Valid는 기본적으로 컨트롤러에서만 동작하며 다른 계층에서는 검증이 되지 않는다. 다른 계층에서 파라미터를 검증하기 위해서는 @Validated와 결합하여야 한다.

3. @Vaildated를 이용한 유효성 검사

3.1 @Vaildated의 개념 및 사용 방법

입력 파라미터의 유효성 검증은 컨트롤러에서 최대한 처리하고 넘겨주는 것이 좋다. 불가피하게 다른 계층에서 파라미터를 검증해야 할 수 있다. Spring에서는 이를 위해 AOP 기반으로 메소드의 요청을 가로채서 유효성 검증을 진행해주는 @Vaildated를 제공한다. @Vaildated는 JSR 표준 기술이 아니며 스프링 프레임워크에서 제공하는 애노테이션이다.

@Service
@Validated
public class UserService {
	public void addUser(@Valid AddUserRequest addUserRequest) {
		...
	}
}
  • 다음과 같이 클래스에 @Vaildated를 지정하고, 유효성을 검증할 파라미터에 @Valid를 지정하면 된다.
  • 유효성 검증이 실패하면 ConstraintViolationException 예외가 발생한다.

3.2 @Vaildated의 동작 원리

@Vaildated는 AOP 기반으로 메소드 요청을 인터셉터하여 처리된다. @Vaildated를 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 AOP의 어드바이스 또는 인터셉터(MethodValidationInterceptor)가 등록된다. 그리고 해당 클래스의 메소드들이 호출될 때 AOP의 포인트 컷으로써 요청을 가로채서 유효성 검증을 진행한다.

이러한 이유로 @Vaildated를 사용하면 컨트롤러, 서비스, 레포지토리 등 계층에 무관하게 스프링 빈이라면 유효성 검증을 진행할 수 있다.

3.3 @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) {
      ...
}

4. @Valid의 예외 처리

4.1 View에서의 유효성 검사 예외 처리

@PostMapping
public String processOrder(@Valid Order order, Errors errors) {
	if(errors.hasErrors()) {
		return "orderForm";
	}
	return "redirect:/";
}
  • 파라미터에 Error라는 validation의 객체를 선언하면, 에러 발생 시 에러의 상세 내열이 Error 객체에 저장되어 전달된다.
<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>
  • 타임리프에서는 th:if를 통해 hasError() 메서드를 사용하여 에러가 있는지 검사 후 에러가 발생하면 사전에 지정해놓은 에러 메세지를 출력할 수 있다.

4.2 @RestControllerAdvice를 통한 예외 처리

/**
 * 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()));
}
  • BindingResult의 FieldError에서 검증이 실패한 필드명, 메시지, 입력값을 가져와 메시지를 만들어 반환한다.

5. @Valid와 @Validated 유효성 검사 차이

  • @Valid
    • JSR-303 자바 표준 스펙
    • 특정 ArgumentResolver를 통해 진행되어 컨트롤러 메소드의 유효성 검사만 가능하다.
    • 유효성 검사에 실패할 경우 MethodArgumentNotValidException 예외가 발생한다.
  • @Validated
    • 자바 표준 스펙이 아닌 스프링 프레임워크가 제공하는 기능이다.
    • AOP 기반으로 스프링 빈의 유효성 검증을 위해 사용되며 클래스에는 @Validated를, 메소드에는 @Valid를 지정해야 한다.
    • 유효성 검사에 실패할 경우 ConstraintViolationException 예외가 발생한다.

6. Validation 애노테이션 정리

Anotation제약조건
@NotNullNull 불가
@NullNull만 입력 가능
@NotEmptyNull, 빈 문자열 불가
@NotBlankNull, 빈 문자열, 스페이스만 있는 문자열 불가
@Size(min=,max=)문자열, 배열등의 크기가 만족하는가?
@Pattern(regex=)정규식을 만족하는가?
@Max(숫자)지정 값 이하인가?
@Min(숫자)지정 값 이상인가
@Future현재 보다 미래인가?
@Past현재 보다 과거인가?
@Positive양수만 가능
@PositiveOrZero양수와 0만 가능
@Negative음수만 가능
@NegativeOrZero음수와 0만 가능
@Email이메일 형식만 가능
@Digits(integer=, fraction = )대상 수가 지정된 정수와 소수 자리 수 보다 작은가?
@DecimalMax(value=)지정된 값(실수) 이하인가?
@DecimalMin(value=)지정된 값(실수) 이상인가?
@AssertFalsefalse 인가?
@AssertTruetrue 인가?

7. Validation 커스텀 애노테이션으로 유효성 검사하기

7.1 검증을 위한 커스텀 애노테이션 생성

@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 : 유효성 검증 시에 전달할 메타 정보

7.2 검증을 위한 Validator 구현

JSR에서 제공하는 javax.validation의 ConstraintValidator 인터페이스를 구현해야 한다.

public interface ConstraintValidator<A extends Annotation, T> {

    default void initialize(A constraintAnnotation) {
    }

    boolean isValid(T value, ConstraintValidatorContext context);
}
  • initialize : Validator를 초기화하기 위한 메서드 초기화할 작업이 없다면 따로 구현하지 않아도 된다.
  • isValid : 유효성을 검증하는 메서드
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();
    }
}

0개의 댓글