Spring Boot @Valid, BindingResult

Spring Boot 프로젝트에서 등록, 수정 등의 기능을 구현할 때 입력 값의 validation을 확인하는 작업을 하게 됩니다. 서비스 로직에서 직접 조건을 검사하는 방법도 있지만 javax.validation에서 제공해주는 @Valid를 애노테이션을 사용하면 좀 더 쉽게 구현할 수 있습니다.

@Valid를 사용하게 되면 자동으로 Bean Validation(JSR 380) 구현체인 Hibernate Validator가 내부적으로 실행되며 Hibernate Validator 6.0.1.Final 버전이 사용되기 때문에 Spring Boot 2.0 이상 버전을 써야 됩니다. Hibernate Validator는 매개변수(DTO)의 validation constraints 애노테이션을 검사하여 유효하지 않는 필드들이 검출되면 예외를 발생시킵니다.

아래 간단한 예제를 보면서 사용법을 설명하겠습니다.

SampleDto.java

import lombok.*;
import javax.validation.constraints.NotBlank;

@Getter
@Setter
@NoArgsConstructor
public class SampleDto {
    @NotBlank(message = "name is empty")
    private String name;
    @NotBlank(message = "address is empty")
    private String address;
    @NotBlank(message = "email is empty")
    private String email;
}

Controller.java

import javax.validation.Valid;
import org.springframework.validation.BindingResult;

@RestController
public class SampleController {
    // (1) Get 요청에서 @Valid 사용
    @GetMapping(value = "/testGetValidation")
    public String validate(@Valid SampleDto dto) {
        return "ok";
    }
    
    // (2) Post 요청에서 @Valid 사용.
    @PostMapping(value = "/testPostValidation")
    public String validate(@Valid @RequestBody SampleDto dto) {
        return "ok";
    }
    
    // (3) @Valid와 BindingResult를 함께 사용
    @GetMapping(value = "/testGetValidation")
    public String validate(@Valid SampleDto dto, BindingResult bindingResult) {
        if(bindingResult.hasErrors()){
            return "error";
        }
        return "ok";
    }
}

(1) Get 요청에서 @Valid 사용

매개변수 dto의 필드에 정의된 @NotBlank 등의 validation constraints를 검증하여 유효하지 않는 필드가 검출되면 BindException 예외가 발생합니다.

(2) Post 요청에서 @Valid 사용

매개변수 dto의 필드에 정의된 @NotBlank 등의 validation constraints를 검증하여 유효하지 않는 필드가 검출되면 MethodArgumentNotValidException 예외가 발생합니다.

(3) @Valid와 BindingResult를 함께 사용

BindingResult는 spring에서 제공하는 validation 패키지에 포함된 인터페이스이고 Errors 인터페이스를 상속 받습니다. hasErrors()와 같은 Errors에 포함된 기능을 사용할 경우에는 BindingResult, Errors 둘 중 아무거나 사용해도 됩니다.

dto의 필드에 정의된 validation constraints를 검증하여 유효하지 않는 필드가 검출되면 예외를 발생시키지 않고, BeanPropertyBindingResult 구현체에 오류 내용을 담아서 요청한 메서드의 매개 변수로 정의된 BindingResult로 전달합니다. 개발자는 bindingResult.hasErrors()로 오류 여부에 따라 분기처리 할 수 있습니다.

(1), (2) 처럼 @Valid만 사용한 경우에는 예외가 발생하므로 @ControllerAdvice 클래스에서 @ExceptionHandler 메서드로 공통 처리가 가능합니다.

@ControllerAdvice
public class CommonExceptionHandler {
    ...
    @ExceptionHandler(BindException.class)
    public String handleValidationExceptions(BindException e, Model model) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        log.error(e.getMessage(), e);
        model.addAttribute("errors", errors);
        return ERROR_PAGE;
    }
    ...
}

(1) Get 요청의 경우에는 BindException (2) Post 요청의 경우에는 MethodArgumentNotValidException 예외가 발생하는데 MethodArgumentNotValidException는 BindException 를 상속 받은 예외이기 때문에 BindException으로 같이 처리 할 수 있습니다.

결론

validation constraints를 검증 실패할 경우 공통오류 페이지를 보여줘야 할 경우에는 @Valid만 사용하고, 각 요청에서 특정한 처리와 함께 특정 페이지를 보여줘야하는 경우에는 @Valid와 BindingResult 또는 Errors를 함께 사용하는 것이 좋을 것 같습니다. (예를 들면 등록페이지 -> 검증 실패 -> 등록페이지(오류 값 화면에 표시) )

댓글남기기