Spring의 Validation

하루히즘·2021년 5월 18일
1

Spring Framework

목록 보기
4/15

서론

스프링 프레임워크에서는 객체를 검증할 때 Hibernate의 Validator나 직접 구현한 Validator 등을 사용할 수 있다. 현재 진행하고 있는 개인 프로젝트는 스프링 부트 기반이기 때문에 spring-boot-starter-validation에 포함된 Hibernate Validator를 사용하고 있다.

당연하지만 객체를 검증한다면 검증 결과를 활용할 수 있어야 한다. 이를 위해서는 BindingResult 인터페이스를 활용할 수 있으며 주로 스프링 MVC의 핸들러 메서드에 파라미터로 추가하여 사용한다.

본론

BindingResult

BindingResult 자체는 단순한 인터페이스로 Errors 인터페이스를 확장하고 있다. Errors 인터페이스는 특정 객체에 대한 Data Binding이나 Validation의 결과를 담고 에러를 반환하는 역할을 정의하고 있다. BindingResult 인터페이스는 이를 좀 더 Data Binding 위주로 사용할 수 있도록 정의하고 있다.

스프링 MVC에서는 사용자가 전달한 파라미터를 String, Integer 등으로 바인딩할 수 있다. 이때 변환할 수 없는 자료형이거나 검증 규칙을 위반했을 경우 그 결과가 BindingResult 인터페이스의 구현체에 담겨서 전달된다. 그래서 이 BindingResult 인터페이스를 아래처럼 핸들러 메서드의 파라미터로 등록하여 바인딩 결과를 확인할 수 있다.

@PostMapping("/register")
public String submitRegister(
    @ModelAttribute("command") @Valid RegisterRequestCommand command,
    BindingResult bindingResult){
    
    if(bindingResult.hasErrors()) {
        response.setStatus(HttpStatus.UNPROCESSABLE_ENTITY.value());
        return "account/register";
    }
    ...

Validation 규칙에 어긋나는 데이터를 전송하면 BeanPropertyBindingResult라는 BindingResult 인터페이스의 구현체가 파라미터로 전달된다. 이 클래스는 앞서 언급한 Errors, BindingResult 클래스를 구현하며 이름처럼 JavaBean 객체의 바인딩 에러를 등록 및 평가(evaluation)하는 역할을 수행하며 현재 요청을 처리할 때 발생한 문제를 담고 있다.

예를 들어 공백이 전달될 수 없는 username 항목에 공백이 전달됐을 경우 다음과 같은 에러 메시지를 볼 수 있다.

Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'command' on field 'username': rejected value []; codes [NotBlank.command.username,NotBlank.username,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [command.username,username]; arguments []; default message [username]]; default message [공백일 수 없습니다]]

어떤 예외가 발생했는지, 몇 개의 필드에 대한 검증이 실패했는지, 검증 종류에 따른 기본 에러 메시지 등을 볼 수 있다.

BindException

이런 검증 결과를 활용하려면 대개 Errors나 BindingResult 인터페이스를 활용하여 이 클래스에 접근하게 된다. BindingResult 인터페이스 파라미터는 검증 대상 객체의 바로 뒤 파라미터로 전달되어야 컨트롤러에서 처리할 수 있는데 파라미터로 전달하지 않거나 바로 뒤에 전달하는게 아닌 경우(위치가 중요하다) 스프링은 BindException을 발생시킨다.
초기 설정에서는 이런 Whitelabel 에러 페이지를 볼 수 있는데 이는 400 Bad Request 에러가 발생했을 때 출력된다. BindException은 스프링 MVC에서 발생한 예외를 적절한 HTTP 상태 코드로 변환하는 DefaultHandlerExceptionResolver에 의해 400 에러로 정의되어 있기 때문이다.

BindException은 ExceptionHandler로 처리할 수도 있고 resource/static/error 디렉토리에 400.html으로 에러 페이지를 등록하여 보여줄 수도 있다.

Validator

이런 Validation을 수행할 때는 Hibernate의 Validator를 사용할 수도 있고 직접 구현하여 사용할 수도 있다. 이 때 구현해야 하는 인터페이스가 Validator 인터페이스로 검증 대상 클래스를 확인하는 메서드(supports)와 검증 방식(validate)을 정의하고 있다.

스프링 문서에서 제공하는 Validator 구현 예시는 다음과 같다.

 public class UserLoginValidator implements Validator {

    private static final int MINIMUM_PASSWORD_LENGTH = 6;

    public boolean supports(Class clazz) {
       return UserLogin.class.isAssignableFrom(clazz);
    }

    public void validate(Object target, Errors errors) {
       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
       UserLogin login = (UserLogin) target;
       if (login.getPassword() != null
             && login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
          errors.rejectValue("password", "field.min.length",
                new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
                "The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
       }
    }
 }

직접 validate 메서드에서 검증이 필요한 객체의 필드를 검사하는 것을 볼 수 있다.

그러면 이렇게 직접 구현한 Validator를 어떻게 사용할 수 있을까? 우선 이 Validator를 Bean 객체로 등록한 후 컨트롤러의 핸들러 메서드에서 해당 Validator를 주입받아 validate 메서드로 직접 검증해야 한다. 그리고 두 번째 파라미터에 위에서 언급한 BindingResult를 넘겨서 검증 결과를 받아올 수 있다.

@Autowired UserLoginValidator userLoginValidator;

@PostMapping
public String submit(RequestCommand command, BindingResult result) {
    userLoginValidator.validate(command, result);
    ...

Errors 인터페이스는 BindingResult의 부모 인터페이스기 때문에 이를 메서드에 넘길 수 있다. 그리고 검증 결과는 hasErrors 메서드 등으로 확인하여 문제가 있다면 별도로 처리할 수 있다.

JSR-303

대부분의 웹 서비스는 비슷한 검증 과정을 필요로 한다. 예를 들어 로그인 시 아이디나 비밀번호 필드는 비워둘 수 없다던가 전송된 데이터는 일정 크기를 초과할 수 없다던가 하는 제약은 거의 모든 웹 서비스에서 필요한 제약 조건이며 이를 검증하는 Validator를 일일히 구현하는 것은 불필요한 작업일 것이다.

그렇기 때문에 JavaBean 객체에 대한 검증 과정을 표준화한 JSR-303이라는 명세(specification)가 정의되었고 Hibernate 팀에서 이를 구현한 Hibernate Validator를 제공하고 있다. 언급했듯이 스프링 부트에서 스타터 설정을 사용한다면 자동으로 Hibernate Validator를 사용하게 된다.

JSR-303에서는 어떤 클래스든 상관없이 검증하고자 하는 필드를 어노테이션으로 간편하게 검증할 수 있도록 정의하고 있다. 즉 @Max, @NotBlank 등 미리 정의된 다양한 어노테이션을 사용하여 클래스마다 별도의 Validator를 구현할 필요 없이 객체를 간편하게 검증할 수 있다.

public class RegisterRequestCommand {
    @NotBlank
    private String userid;
    @NotBlank
    private String username;
    @NotBlank @Length(min = 4)
    private String password;
    @NotBlank @Email
    private String email;
}

@NotBlank 어노테이션은 해당 문자열 객체가 null이거나 빈 문자열(한 글자도 없거나 공백으로만 이루어진 문자열)이지 않아야 한다는 제약 조건을 나타낸다. 그렇기 때문에 만약 userid나 username 필드에 ""같은 빈 문자열이 전달된다면 검증이 실패하게 된다.

@PostMapping("/register")
public String submitRegister(@Valid RegisterRequestCommand command,
                             BindingResult bindingResult) {
                             
    if(bindingResult.hasErrors()) {
        response.setStatus(HttpStatus.UNPROCESSABLE_ENTITY.value());
        return "account/register";
    }
    ...

컨트롤러의 핸들러 메서드에서는 이전처럼 Validator를 주입받아 사용할 필요 없이 @Valid 어노테이션을 이용하여 간편하게 검증할 수 있다. 검증 과정에서 발생한 위반 사항들은 BindingResult에 담기기 때문에 훨씬 간편하게 검증할 수 있다.

결론

처음에는 BindException과 MethodArgumentNotValidException의 차이를 알아보기 위해 시작했지만 BindingResult와 Validation 쪽으로 더 많이 알아보게 된 것 같다. 기본적인 개념을 정리하는 데 도움이 되었다.

BindException과 MethodArgumentNotValidException의 차이는 이 이슈를 참고하면 좋을 것 같다. 전자는 언급했듯이 Data Binding이나 Validation이 실패하면 발생하고 후자는 RequestBody 등으로 전달된 데이터를 변환하는 데 실패했을 때 발생한다고 한다. 두 예외는 상속 관계지만 서로 다른 목적으로 취급되는 듯.

참고자료

Spring MVC Custom Validator Example
Spring Bean Validation – JSR-303 Annotations

profile
YUKI.N > READY?

0개의 댓글