Spring Validation/입력값 검증

선종우·2023년 5월 17일
2

1. 공부배경

  • Spring을 이용해 개발을 할 때 편하게 @Valid 및 검증용 애노테이션만 사용해왔으나, Custom Validator가 필요한 일이 생겨 Spring Framework의 입력값 검증에 대해 정리를 해보았다.
  • 공식 문서 : Documentation, Reference,

2. 공부내용

2-1. 애노테이션을 이용한 Validation/입력값 검증

  • 설명

    • 검증하고자 하는 필드에 검증용 애노테이션(예 : @Min(10), @Email)을 붙여 입력값을 검증하는 방법이다. 특별한 일이 없다면 이 방식을 사용하는 게 가장 편리하다.
    • 구현체는 LocalValidatorFactoryBean 으로 스프링이 글로벌 Validator로 자동등록하여 작동한다.(Validator 등록에 대해서는 뒤에서 설명)
  • 관련 문서 : Reference(Hibernate 애노테이션 종류)

  • 2-1-1. @ModelAttribute, @RequestBody DTO를 사용하는 경우

    • 일반적인 경우 사용하는 방법으로 빠르고 쉽게 입력값 검증을 진행할 수 있다.

    • 사용방법

      1. 파라미터 앞에 @Valid or @Validated 선언
        *@Valid는 java 표준, @Validated는 Hibernate 구현 애노테이션

      2. 다음 파라미터로 BindingResult 선언
        이때 BindingResult는 검증하고자 하는 값 뒤에 선언해야 한다.
        * BindingResult를 선언하지 않으면 Exception을 발생시키며 이때는 ExceptionHandler이용 처리 필요

      3. 객체 각 필드에 검증 애노테이션 선언

    • 예시

      //DTO
      public RequestDto{
         @Email //이메일 형식 검증
      	private String email;
         @Max(10) // 최대 10까지만 입력 가능
          private int peopleLimit;
         @size(min = 10, message = "10자 이상 입력 필요") //message를 입력하면 defaultMessage가 설정된다.
          String content;
      }
      
      //Controller
      @Controller
      public ArticleController{
         /*검증하고자 하는 값에 @Valid 또는 @Validated를 붙여준다. 
           그리고 검증하고자 하는 값 뒤에 BindingResult를 선언한다.
         */
         @GetMapping("/")
         public String getArticles(@Valid @ModelAttribute RequestDto requestDto, BindingResult bindingResult){
             // 오류 검증값은 bindingResult 변수에 담긴다.
             if(bindingResult.hasErrors()){
                 에러가 있을 경우 진행하는 logic
             }
         
         /* RequestBody에도 사용할 수 있다.
         */
         @GetMapping("/")
         @ResponseBody
          public ResponseEntity getArticles(@Valid @RequestBody RequestDto requestDto, BindingResult bindingResult){
             // 오류 검증값은 bindingResult 변수에 담긴다.
             if(bindingResult.hasErrors()){
                 에러가 있을 경우 진행하는 logic
             }
         
       }
      
    • 주의할 점 : ParsingError가 발생했을 때 @ModealAttribute@RequestBody일 때 결과가 다르다. (예시 : Integer 필드에 String값 입력했을 경우)

      • @ModelAttribute의 경우 ParsingError가 발생한 필드는 default 값(보통 null)로 할당하고 객체를 생성한다. 해당 필드에 typeMismatch코드로 오류 정보가 생성되어 BindingResult에 저장된다. 이후 다른 필드에 대한 Validation을 계속 진행한다.
        (-> 오류가 발생하더라도 객체를 생성하기 때문에 BindingResult를 이용한 Controller 로직 진행 가능)
      • 반면 @RequestBody의 경우 HttpMessageConverter객체 생성을 중단하고 예외를 발생시키기 때문에 Validation 및 Controller 로직이 진행되지 않는다.
        (-> ExceptionHandler로 처리 필요)
  • 2-1-2. @PathVariable, @RequestParam을 이용하는 경우

    • 파라미터 앞에 애노테이션을 붙이는 방식은 동일하나, Controller에 @Validated를 선언해주어야 한다. 검증에 실패할 경우 예외를 발생시킨다.
    • 사용하기 위해서는 MethodValidationPostProcessor빈을 등록해주어야 한다(Springboot는 기본으로 등록하기 때문에 별도 설정 불필요)
    • 예시
    @Controller
    public UserController{
    
    	//@RequestParam은 생략 가능
    	@GetMapping("/login")
    	public String login(@email String email, String password){
       }
    }

2-2. Custom Validator를 이용한 Validation/입력값 검증

  • 설명 : 입력값 검증 과정에서 추가적인 로직을 추가하고 싶은 경우에는 스프링 Validator인터페이스를 이용하면 된다.
  • 사용방법
    1. Validator 인터페이스 구현
    2. Controller에 @Init 을 선언하여 구현체를 해당 Controller에 등록
      글로벌하게 등록할 수도 있으나, 이 경우 LocalValidatorFactoryBean가 작동하지 않을 수 있다.
  • 예시

    //customValidator 생성
    public class CustomValidator implements Validator{
    
           //특정 파라미터가 검증 대상인지 체크하는 메소드
           //return 반환값이 true일 경우 검증 대상이 된다.
    		@Override
    		public boolean supports(Class<?> class){
    			return RequestDto.class.isAssingnableFrom(clazz);
    		}
           /*검증로직 구현 및 error에 오류내용 입력
           Error는 BindingResult의 부모 클래스이다. 
           Spring은 Errors에 앞서 Controller에 선언한 BindingResult를 validate의 매개변수로 전달한다.
           */
    		@Override
       		public void validate(Object target, Errors errors) {
    			RequestDto requestDto = (RequestDto) target;
           		//필드 오류 검증로직
           		if(requestDto.getCotent() == null){
                      errors.rejectValue(에러 필드 및 입력 값 설정); //특정 필드 오류로 지정하고 싶은 경우
                  }
           		
                 //글로벌 오류(객체 오류) 검증로직
                 if(검증로직){
                	errors.reject(입력 값 설정); //target Object 자체가 오류라고 지정하고 싶은 경우
                 }
                   
       		}
    }
    
    //Controller에 CustomValidator 등록
    @RequiredArgsConstructor
    public class ArticlController{
    	private final Validator customValidator; //빈 주입 필요
       
       @Init
       public void init(WebDatBinder databinder){
       		databinder.addValidators(customValidator);
       }
       
       @GetMapping("/")
       public String getArticles(@Valid @ModelAttribute RequestDto requestDto, BindingResult bindingResult){
               // 오류 검증값은 bindingResult 변수에 담긴다.
               if(bindingResult.hasErrors()){
                   에러가 있을 경우 진행하는 logic
               }
       }
       
    }
  • 여러 개의 Validator를 동시에 적용할 수도 있다(다중 Validator 등록). 이때 기본적으로 등록되는 LocalValidatorFactoryBean도 같이 적용된다.

    • 예시
 
 //Controller에 CustomValidator 등록
 @RequiredArgsConstructor
 public class ArticlController{
 	private final CustomValidator1 customValidator1; //빈 주입 필요
    private final CustomValidator2 customValidator2; //빈 주입 필요
    
    @InitBinder
    public void init(WebDatBinder databinder){
    		//validator 여러 건 등록
    		databinder.addValidators(customValidator1, customValidator2);
    }
    
    @GetMapping("/")
    public String getArticles(@Valid @ModelAttribute RequestDto requestDto, BindingResult bindingResult){
            // 오류 검증값은 bindingResult 변수에 담긴다.
            if(bindingResult.hasErrors()){
                에러가 있을 경우 진행하는 logic
            }
    }
    
 }
  • Controller에서 여러 타입의 DTO를 사용하는 경우 @Init에 추가적인 옵션을 주어야 한다.(옵션이 없는 경우 @Valid가 붙은 모든 DTO에 검증기 적용)
    • 예시
 
 //Controller에 CustomValidator 등록
 @RequiredArgsConstructor
 public class ArticlController{
 	private final CustomValidator1 customValidator1; //빈 주입 필요
    private final CustomValidator2 customValidator2; //빈 주입 필요
    
    //검증하려는 클래스이름(첫글자는 소문자)
    @InitBinder("requestDto")
    public void init(WebDatBinder databinder){
    		//validator 여러 건 등록
    		databinder.addValidators(customValidator1);
    }
    
    @InitBinder("postRequestDto")
    public void init(WebDatBinder databinder){
    		//validator 여러 건 등록
    		databinder.addValidators(customValidator2);
    }
    
    @GetMapping("/")
    public String getArticles(@Valid @ModelAttribute RequestDto requestDto, BindingResult bindingResult){
            // 오류 검증값은 bindingResult 변수에 담긴다.
            if(bindingResult.hasErrors()){
                에러가 있을 경우 진행하는 logic
            }
    }
    
    @GetMapping("/")
    public String postArticles(@Valid @ModelAttribute PostRequestDto postRequestDto, BindingResult bindingResult){
            // 오류 검증값은 bindingResult 변수에 담긴다.
            if(bindingResult.hasErrors()){
                에러가 있을 경우 진행하는 logic
            }
    }
    
 }

3. 정리

  • 특별히 복잡한 로직이 없는 @ModealAttribute, @RequestBody가 붙은 DTO를 사용할 때는 DTO의 각 필드에 애노테이션(@MAX, @MIN 등)을 붙여 검증을 수행한다.
    • 이 방식은 스프링이 자동으로 등록하는 LocalValidatorFactoryBean를 이용한 방법이다.
      DTO 필드에 있는 애노테이션을 이용해 검증을 수행하며, 그 결과는 BindingResult에 저장된다.
  • @RequestBody의 경우 객체 생성 중 오류가 발생하면 예외를 발생시키므로 BindingResult를 이용한 에러 처리를 할 수 없다.
    -> ExceptionHandler를 이용한 예외처리 필요, 별도 처리 없을 경우 400Error에 해당되는 에러페이지 처리 로직 실행
  • @RequestParam, @PathVariable에도 애노테이션을 이용해 값 검증을 할 수 있다. 이 경우 에러 발생 시 예외를 발생시킨다. 따라서 사용자에게 원하는 오류 메시지를 전달하려면 적절한 처리가 필요하다.
  • 조금 더 복잡한 로직을 사용하고 싶은 경우에는 Validator인터페이스를 구현 및 등록(Cotroller에 @Init 등록 메소드 추가)하여 사용할 수 있다. 다양한 Validator를 1개의 DTO에 적용할 수 있으며, DTO 타입별로도 Validator를 구분 적용할 수 있다.

0개의 댓글