모르는게 많은 개발자

[스프링] Validation 방법(Validator, Bean Validation) 설명/예제 본문

스프링

[스프링] Validation 방법(Validator, Bean Validation) 설명/예제

Awdsd 2021. 8. 7. 23:59
반응형
이번 글에서는 스프링의 Validation 하는 방법인 Validator과 Bean Validation에 대해 알아보자.

Validator

첫번째 방법은 Validator Interface를 사용한 방법이다.

Validation Interface는 다음과 같이 정의되어있다.

public interface Validator {
    boolean supports(Class<?> clazz);
    void validate(Object target, Errors errors);
}

위 Interface를 구현하고 스프링에서 제공하는 WebDataBinder를 통해 등록해서 사용할 수 있다. WebDataBinder는 스프링 파라미터의 바인딩과 검증 기능을 제공한다.

 

Validator 구현체를 다음과 같이 구현했다.

@Component
public class TestValidator implements Validator {
    /**
     * 검증하려는 클래스를 체크
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(Test1.class);
    }

    /**
     * 검증
     */
    @Override
    public void validate(Object target, Errors errors) {
        Test1 test1 = (Test1) target;
        //age가 1000보다 클 경우
        if(test1.getAge() > 1000) {
            //에러 field와 에러 code를 입력
            errors.rejectValue("age", "age가 1000보다 큽니다");
        }
        //name이 입력되지 않았을 경우
        if(!StringUtils.hasText(test1.getName())) {
            errors.rejectValue("name", "이름이 입력되지 않았습니다.");
        }
    }
}
@Data
public class Test1 {
    String name;
    Integer age;
}
supports() : 검증하려는 클래스를 체크하는 메소드이다. 위 같은 경우 Test1 클래스를 검증하기 위해 Test1를 체크한다.
validate() : 실제로 데이터를 검증하는 로직이 들어있다.

 

Controller는 다음과 같다.

@RestController
@Slf4j
@RequiredArgsConstructor
public class ValidateTestController {

    private final TestValidator testValidator;

    /**
     * 컨트롤러 호출될 때마다 이 메소드 호출
     */
    @InitBinder
    public void init(WebDataBinder webDataBinder) {
        webDataBinder.addValidators(testValidator);
    }

    @GetMapping("/validate")
    public Object validateTest(@ModelAttribute Test1 test1, BindingResult bindingResult) {
        testValidator.validate(test1, bindingResult);
        if(bindingResult.hasErrors()) {
            return bindingResult.getFieldErrors();
        }
        return "success";
    }
}
@InitBinder 어노테이션은 해당 Controller가 호출될 때마다 해당 메소드가 수행되게 한다. 이 구문에서 WebDataBinder를 통해 Validator를 등록한다.

이제 요청(/validate)를 수행하면 Test1 VO에 데이터를 입력해 testValidator.validate()를 통해 검증을 수행한다.

BindResult는 검증을 수행하면서 발생한 검증 에러를 담는 객체이다.

검증이 끝난후 bindingResult.hasErrors()를 이용하면 에러가 있었는지 체크할 수 있고 getFieldErrors()로 에러 목록을 가져올 수 있다.

geFieldErrors() 출력

 

모든 Controller의 메소드마다 검증을 수행하려면 validate()를 수행해야할까? @Validated 어노테이션을 이용해 생략할 수 있다.

@RestController
@Slf4j
@RequiredArgsConstructor
public class ValidateTestController {

    private final TestValidator testValidator;

    /**
     * 컨트롤러 호출될 때마다 이 메소드 호출
     */
    @InitBinder
    public void init(WebDataBinder webDataBinder) {
        webDataBinder.addValidators(testValidator);
    }

    @GetMapping("/validate")
    public Object validateTestV2(@Validated @ModelAttribute Test1 test1, BindingResult bindingResult) {
        //@Validated를 사용하면 testValidator.validate()를 생략할 수 있음.
        if(bindingResult.hasErrors()) {
            return bindingResult.getAllErrors();
        }
        return "success";
    }
}

위처럼 검증할 객체에 @Validated를 붙여줌으로써 자동으로 검증하여 BindingResult에 담아준다. 결과는 위와 동일하다.


Bean Validation

Bean Validation은 특정 기능 구현체가 아니라 Bean Validation2.0(JSR-380)이라는 기술 표준이다.

쉽게 이야기해서 검증 어노테이션과 여러 인터페이스 모음이다.EX)JPA는 표준 기술, 하이버네이트는 구현체

Bean Validatio의 일반적인 구현체는 하이버네이트 Validator이다.

 

Bean Validation을 사용하기 위해 아래 의존성을 추가해야 한다.

implementation 'org.springframework.boot:spring-boot-starter-validation'

Bean Validation을 적용한 예시는 다음과 같다.

@Data
public class Test1 {
    
    @NotBlank
    String name;
    
    @Max(1000)
    Integer age;
}

위 처럼 각 속성마다 검증하고 싶은 Bean Validation Annotation을 붙이면 된다. 위의 어노테이션은 Validator에서 봤던 검증 기능을 그대로 구현한것이다.

Bean Validation 어노테이션은 다음과 같이 여러가지가 있다.

어노테이션 이름 기능
@AssertFalse False일 경우
@AssertTrue True일 경우
@DecimalMax(value=) 지정 값 이하 실수
@DecimalMin(value=) 지정 값 이상 실수
@Digits(integer=,fraction=) 속성 값이 지정된 정수화 소수 자리수보다 적을 경우
@Future 속성 날짜가 현재보다 미래인 경우
@Past 속성 날짜가 현재보다 과거인 경우
@Max(value) 지정 값이하인 경우
@Min(value) 지정 값이상인 경우
@NotNull 널이 아닌 경우
@Null 널인 경우
@Pattern(regex=, flag=) 해당 정규식 통과인 경우
@Size(min=, max=) 문자열 또는 배열이 지정값 사이인 경우
@Valid 확인 조건 만족한 경우

 

Controller는 어떻게 변경되는지 보자.

@RestController
@Slf4j
@RequiredArgsConstructor
public class ValidateTestController {

    @GetMapping("/validate2")
    public Object validateTestV2(@Validated @ModelAttribute Test1 test1) {
        return "success";
    }
}

위에서 작성했던 @Initbinder 어노테이션, BindingResult, Validator가 모두 사라진 것을 볼 수 있다.

Test1.name에 빈값, Test1.age에 1000이상의 숫자를 요청했을 때 결과는 다음과 같다.

Field error in object 'test1' on field 'name': rejected value []; codes [NotBlank.test1.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [test1.name,name]; arguments []; default message [name]]; default message [공백일 수 없습니다]
Field error in object 'test1' on field 'age': rejected value [1001]; codes [Max.test1.age,Max.age,Max.java.lang.Integer,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [test1.age,age]; arguments []; default message [age],1000]; default message [1000 이하여야 합니다]]

그리고 400 BadRequest를 던진다.

 

물론 Validator때와 같이 BindingResult를 사용할 수 있다.. 아래는 Bean Validation으로 검증했을 때 BindingResult에 담기는 에러이다.

또한 에러 코드 메시지를 보면 default로 '공백일 수 없습니다' 또는 '1000 이하여야 합니다'가 발생하는데 메시지를 수정하고 싶다면 다음과 같이 어노테이션에 message 속성을 사용해 메세지를 변경할 수 있다. 

@Data
public class Test1 {

    @NotBlank(message = "공백X")
    String name;

    @Max(value = 1000, message = "1000 초과 X")
    Integer age;
}
- Bean Validatioin이 작동하는 과정 -
위 spring-boot-starter-validation 의존성을 등록하면 Spring에서 자동으로 'LocalValidatorFactoryBean'을 Global Validator에 등록하는데 이 Validator가 검증 어노테이션(@NotNull등)을 통해 검증을 수행한다. 그렇기 때문에 검증을 원하는 객체는 @Validated 어노테이션을 적용해야한다.

또한 Bean Validation은 Binding에 성공한 필드만 적용된다. 즉, Integer 타입의 속성에 String값이 들어오면 Bean Validation검증전에 TypeMismatch로 처리된다.

 

특정 상황에만 Validation 어노테이션을 적용하고 싶은 경우가 있을텐데 이럴때는 group 속성 기능을 사용할 수 있다.

먼저 특정 상황을 구별할 빈 Interface 두개를 만들자.

public interface Test1Check {
}

public interface Test2Check {
}

이제 아래와 같이 Validation 어노테이션에 group속성을 이용해 interface를 등록한다.

@Data
public class Test1 {

    @NotBlank(message = "공백X", groups = {Test1Check.class, Test2Check.class})
    String name;

    @Max(value = 1000, message = "1000 초과 X", groups = {Test1Check.class})
    Integer age;
}

그리고 Controller에서는 아래처럼 @Validated 어노테이션에 수행할 validation 어노테이션에 등록된 인터페이스를 명시해준다.

@RestController
@Slf4j
@RequiredArgsConstructor
public class ValidateTestController {

    @GetMapping("/validate")
    public Object validateTestV2(@Validated(Test2Check.class) @ModelAttribute Test1 test1, BindingResult bindingResult) {
        if(bindingResult.hasErrors()) {
            return bindingResult.getFieldErrors();
        }
        return "success";
    }
}

위 코드는 Test2Check 인터페이스가 등록된 Validation 어노테이션을 검증하라는 뜻이다. 즉, age 속성에 등록된 @Max 어노테이션 검증은 수행되지 않는다.

 

마지막으로 검증 에러가 발생하면 BindException 예외가 발생하는데 @ControllerAdvise를 이용해 커스텀 Response를 보내는 코드는 다음과 같다.

@ControllerAdvice
public class ControllerExceptionHandler {
    @ExceptionHandler(BindException.class)
    public ResponseEntity<String> bindExceptionHandler(BindException e) {
        return new ResponseEntity<>(
                e.getFieldErrors().stream()
                        .map(fe -> fe.getField() + " " + fe.getRejectedValue() + " " + fe.getDefaultMessage())
                        .collect(Collectors.joining(", "))
                , HttpStatus.BAD_REQUEST);
    }
}

 위와 같이 BindException이 걸렸을 경우 필드와 입력값 메시지를 출력하고 BadRequest(400)을 보내게끔 했다. 결과는 아래와 같다.

name, age 모두 검증 에러 발생했을 때 결과


참고

https://meetup.toast.com/posts/223

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard 인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술(김영한 개발자님)

 

반응형
Comments