Bean Validation

Spring Framework에서 제공하는 Validator를 사용하여 로직을 분리하는 방법도 있지만, 본 포스팅에서는 더 편리하고 자주 사용되는 방법인 Bean Validation에 대해 다룬다.

a1.png
Bean Validation 관련 라이브러리 추가가 필요하다.

예제 코드

환경 설정

errors.png

  • errors.properties 생성
spring.application.name=ValidationExample
spring.messages.basename=errors
  • spring.messages.basename에 errors 추가하여 errors.properties 등록

User 클래스

@Data
public class User {

    @NotNull
    @Min(1)
    @Max(100)
    private final int age;

    @NotNull
    private final String name;
}
  • NotNull, NotEmpty, NotBlank의 차이는 다음과 같다.
    • NotNull: null값을 허용하지 않는다.
    • NotEmpty: null값과 ""(초기화된 String)을 허용하지 않는다.
    • NotBlank: null값과 "", ” “를 모두 허용하지 않는다.
  • NotNull(message = "이름은 null일 수 없습니다") 와 같이 디폴드 메세지를 지정 가능하다.
  • 추가로 @Range 어노테이션도 있는데, 이는 Hibernate에서 제공한다.

UserController 클래스

@RestController
@RequiredArgsConstructor
public class userController {

    private final MessageSource messageSource;

    @PostMapping("/test")
    public ResponseEntity<Object> userEcho(@Validated @RequestBody User user, BindingResult bindingResult) {

        // 특정 필드가 아닌 복합 룰 검증
        if(user.getName().equals("Dennis Ritchie") && user.getAge() > 70)
        {
            bindingResult.rejectValue(null, "InvalidAge", new Object[]{user.getName(), 70}, null);
        }

        // 에러 발생 시 BadRequest
        if(bindingResult.hasErrors()) {
            String errMsg;
            if(bindingResult.getFieldError() != null) { // 필드 에러가 있다면 우선적으로 보여준다.
                errMsg = messageSource.getMessage(bindingResult.getFieldError(), LocaleContextHolder.getLocale());
                return ResponseEntity.badRequest().body(errMsg);
            }
            else { // 필드 에러가 없다면 오브젝트 에러를 반환한다.
                errMsg = messageSource.getMessage(bindingResult.getGlobalError(), LocaleContextHolder.getLocale());
                return ResponseEntity.badRequest().body(errMsg);
            }
        }

        return ResponseEntity.ok(user);
    }
}
  • Spring에서 자동으로 빈을 등록해주는 MessageSource를 Autowired해주기 위해 @RequiredArgsConstructor 사용
  • 인자로 받는 User@Validated어노테이션을 추가하여 검증 수행
  • 인자로 BindingResult를 추가하여 오류 발생시에도 함수가 정상적으로 동작
    • BindingResultErrors를 상속하는 인터페이스
    • 스프링에의해 자동으로 구현체가 생성됨
  • 필드 에러가 아닌 경우 복합룰의 경우에는 직접 코드로 검증하는 것이 더 나음
  • 에러가 여러개인 경우 모두 보여주고 싶다면 루프문 추가 필요.
  • 에러 메세지를 어떻게 긁어오는지 확인하고 싶다면 errors의 code를 확인한다.

errors.properties

Min={1} 이상이어야함!!!
Min.age=나이는 {1} 이상이어야함!!!
InvalidAge={0}의 나이는 {1}을 넘을 수 없음
  • {}안의 내용은 arguments로 들어감.
    • @Min(1)의 1의 경우가 arguments의 예시. 자동으로 매핑해준다.
  • 에러 메세지를 가져오는 우선순위는 다음과 같다.
    • 먼저 코드를 확인함, 더 세부적인게 존재하면 세부적인 것을 선택.
    • 우선순위를 정하는 기준은 다음과 같다.

      객체 오류

      객체 오류의 경우 다음 순서로 2가지 생성 
      1.: code + "." + object name 
      2.: code
      
      예) 오류 코드: required, object name: item
      1.: required.item
      2.: required

      필드 오류

      필드 오류의 경우 다음 순서로4가지 메시지 코드 생성 
      1.: code + "." + object name + "." + field 
      2.: code + "." + field
      3.: code + "." + field type
      4.: code
      
      예) 오류 코드: typeMismatch, object name "user", field "age", field type: int 
      1. "typeMismatch.user.age"
      2. "typeMismatch.age"
      3. "typeMismatch.int"
      4. "typeMismatch"

검증 순서

  1. @ModelAttribute 각각의 필드에 타입 변환 시도
    1. 성공하면 다음으로
    2. 실패하면 typeMismatch 로 FieldError 추가
  2. Validator 적용

위 얘시 코드는 @ModelAttribute가 아닌 @RequestBody를 사용하고 있기 때문에 typeMismatch 검출을 위해서는 @ExceptionHandler(TypeMismatchException.class)를 정의해줘야 한다.

@ControllerAdvice
public class GlobalExceptionHandler {
   @ExceptionHandler(TypeMismatchException.class)
   public ResponseEntity<Object> handleTypeMismatch(TypeMismatchException ex) {
       String errorMsg = "Invalid input type for request body";
       return ResponseEntity.badRequest().body(errorMsg);
   }
}
  • @ControllerAdvice@Component 어노테이션의 특수한 케이스로, 스프링 부트 애플리케이션에서 전역적으로 예외를 핸들링할 수 있게 해주는 어노테이션이다.
  • 기본적으로 ExceptionHandler는 해당 컨트롤러에서만 동작하나, @ControllerAdvice에 정의하면 전역으로 사용 가능.

MessageCodesResolver

스프링은 MessageCodesResolver로 오류 매세지의 우선순위를 정하는 기능을 지원한다.

  • Validation 프로세스에서 MessageCodesResolver는 오류에 대한 메시지 코드를 생성한다.
  • 이 메시지 코드는 MessageSource에 전달되어 실제 메시지로 변환된다.
  • 따라서 MessageCodesResolver는 메시지 코드를 생성하는 역할을 담당하고, MessageSource는 이를 실제 메시지로 변환하여 사용자에게 제공한다.

테스트

승인
test1.png
거절
test2.png