[Spring Boot] Validation 적용하는 법 - BindingResult 적용하기

devdo·2021년 12월 29일
5

SpringBoot

목록 보기
9/35
post-thumbnail

Validation이란?

Validation이란 프로그래밍에 있어서 가장 필요한 부분입니다.

예를들어, java에서는 null값에 대해서 접근하려고 할 때 null pointer eception(NPE)이 발생함으로, 이러한 부분을 방지하기 위해서 미리 검증을 하는 과정들을 Validation 이라고 한다.


Spring Boot Validation 적용하는 방법

@Valid 어노테이션이 작동이 안 한다면?

spring boot 2.3 version 이상부터는 spring-boot-starter-web 의존성 내부에 있던 validation이 사라졌습니다.

때문에 사용하시는 spring boot version이 2.3 이상이라면 validation 의존성을 따로 추가해주셔야 사용할 수 있습니다.

  • 2.3 이상 버전 의존성 추가

gradle

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation', version: '2.5.2'

현재) 버전없이 동작해줄 수 있습니다.

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

maven

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.5.2</version>
</dependency>

유효성 검사가 필요한 Request 객체에 Validation 어노테이션을 통해 필요한 유효성 검사를 적용합니다.


유효성 검사에 사용할 수 있는 어노테이션 정리

@Null  // null만 혀용합니다.
@NotNull  // null을 허용하지 않습니다. "", " "는 허용합니다.
@NotEmpty  // null, ""을 또는 리스트 [] 빈값 허용하지 않습니다. " "는 허용합니다.
@NotBlank  // null, "", " " 모두 허용하지 않습니다.

@Email  // 이메일 형식을 검사합니다. 다만 ""의 경우를 통과 시킵니다. @Email 보다 아래 나올 @Patten을 통한 정규식 검사를 더 많이 사용합니다.
@Pattern(regexp = )  // 정규식을 검사할 때 사용됩니다.
@Size(min=, max=)  // 문자길이를 제한할 때 사용, int는 불가!

@Max(value = )  // 숫자 value 이하의 값을 받을 때 사용됩니다.
@Min(value = )  // 숫자 value 이상의 값을 받을 때 사용됩니다.

@Pattern(regexp = )		// 정규표현식으로 검증식 세울 수 있다.

@Positive  // 값을 양수로 제한합니다.
@PositiveOrZero  // 값을 양수와 0만 가능하도록 제한합니다.

@Negative  // 값을 음수로 제한합니다.
@NegativeOrZero  // 값을 음수와 0만 가능하도록 제한합니다.

@Future  // 현재보다 미래
@Past  // 현재보다 과거

@AssertFalse  // false 여부, null은 체크하지 않습니다.
@AssertTrue  // true 여부, null은 체크하지 않습니다.

@Valid	// 해당 object validation 실행

✅ 헷갈리는 어노테이션(hibernate용 검증 어노테이션)

1) Null 또는 빈문자열 검증

  • @NotNull - Null 허용하지 않는 것 - String 외 타입에 사용
  • @NotEmpty - Null 과 "" 둘 다 허용하지 않는 것 - String, List<> 에만 사용!
  • @NotBlank - Null 과 "", " " 모두 허용하지 않는 것 - String에만 사용!

2) 문자열길이 검증

  • @Size - javax.validation.constraints
  • @Length - org.hibernate.validator.constraints

3) 숫자길이 검증

  • @Max, @Min - javax.validation.constraints
  • @Range - org.hibernate.validator.constraints

@Valid vs @Validated

@Valid는 자바 표준 검증 애노테이션
@Validated는 스프링 전용 검증 애노테이션이다.

@Valid, @Validated는 거의 동일한 역할을 한다.
정확히 말하면, @Validated는 group 기능이 추가된 @Valid이다.

하지만 @Validated의 group 기능은 잘 쓰지는 않는다.

이유는 데이터를 한곳에서 가지고 오지 않고, 다른 데이터를 함께 사용하기 때문이다.

그리고 Bean Validation이라는 데이터 유효성 검사 프레임워크를 사용할 수도 있다.


구현 예시

ApiController

@PostMapping("/user")
    public ResponseEntity user(@Valid @RequestBody UserDto user, BindingResult bindingResult){
        System.out.println("user :" + user);

        if (user.getPhoneNumber() == "xxx-xxxx-xxxx") {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(user);
        }

        if (user.getAge() >= 90) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(user);
        }
        return ResponseEntity.ok(user);
    }

User

package com.example.validation.dto;

import javax.validation.constraints.Email;

public class User {

	@NotBlank
    @NotEmpty
    @Size(min = 2, message = "이름은 2글자 이상이어야 합니다.")
    private String name;

    @NotBlank
    @Range(min = 2, max = 16, message = "이름은 2글자 이상 16글자 이하여야 합니다.")
    private String nickname;

	@Max(value = 90, message = "나이는 90 이하여야 합니다.")
    private int age;

    @Email
    private String email;

	@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx(x)-xxxx")
    private String phoneNumber;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                ", phoneNumber='" + phoneNumber + '\'' +
                '}';
    }
}

BindingResult

BindingResult가 스프링이 제공하는 값 검증 오류 처리의 핵심이다.

BindingResult는 스프링이 제공하는 검증 오류를 보관하는 객체이기 때문이다.

데이터 유효성 검사를 실패하면 ConstraintViolationException을 발생시키는데,
데이터가 유효하지 않은 속성이 있으면 그에 대한 에러 정보를 BindingResult에 담는다.

정상적인 동작에서는 BindingResult에 담은 오류 정보를 가지고 컨트롤러를 호출한다.

하지만 BindingResult가 없다면 4xx 오류가 발생하면서 컨트롤러가 호출되지 않고 오류 페이지로 이동하게 된다.

이런 BindingResult에 검증 오류를 적용하는 방법은

@ModelAttribute의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 스프링이 FieldError을 생성해서 BindingResult에 넣어준다.

그다음, new FieldError를 만들어서 개발자가 직접 넣어준다.
Validator를 사용(@Valid, @Validated)

보통은 @ModelAttribute를 검증하기 때문에 BindingResult를 @ModelAttribute 뒤에 두고 사용한다.

BindingResult 메소드를 사용하는 방식이 있다.

boolean hasErrors() : 에러의 유무를 판단한다.
boolean hasGlobalErrors() : 글로벌 에러의 유무를 판단한다.
void addError(ObjectError error) : field, type, value 등의 에러를 출력할 수 있다.
void rejectValue() : field, errorCode, defaultMessage 등을 받아서 reject 됐을 때 데이터를 남길 수 있다.

이외에도 많은 것들이 존재

주로 전달하는 파라미터

objectName : 오류가 발생한 객체의 이름
field : 오류 필드
rejectedValue : 사용자가 입력한 값(거절된 값)
bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
codes : 메시지 코드
arguments : 메서지에서 사용하는 인자
defaultMessage : 기본 오류 메시지


구현 예시

주의!) ApiController 값 검증할 필드가 있는 DTO에 @Valid 를 꼭 붙여줘야 한다!

// BindingResult objectError 메시지 전달하기
    @PostMapping("/user")
    public ResponseEntity user(
    			@Valid @RequestBody UserDto user, // @Valid 붙여줘야
    			BindingResult bindingResult) {

        if (bindingResult.hasErrors()) {
            StringBuilder sb = new StringBuilder();
            bindingResult.getAllErrors().forEach(objectError -> {

                FieldError field = (FieldError) objectError;
                String message = objectError.getDefaultMessage();

                System.out.println("field :" + field.getField());
                System.out.println("message :" + message);

                sb.append("field :" + field.getField());
                sb.append("message :" + message);

            });

            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
        }   // logic
        return ResponseEntity.ok(user);
    }

  • 결과
{
	"name" : "dsg",
	"age" : 91,
	"email" : "ehtjd33@gmail.com",
	"phoneNumber" : "010-1234-1444"
}

console
message :90 이하여야 합니다
message :yyyyMM의 형식에 맞지 않습니다.


ExceptionHandler에서 처리하기

BindingResult를 Controller에서 파라미터로 써 예외처리를 하지않고
ExceptionHandler에서 사용할 수 있다!


@Slf4j
@RestControllerAdvice
public class CustomExceptionHandler {

    //BindingResult Validation 처리
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                                  HttpHeaders headers,
                                                                  HttpStatus status,
                                                                  WebRequest request) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String message = error.getDefaultMessage();
            errors.put(fieldName, message);
        });

        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

Validation Exception 종류

1) MethodArgumentNotValidException
: @Valid를 사용하여 요청 데이터의 유효성을 검사할 때 유효하지
않은 값이 전달되면 발생.

	@ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<?> handleIllegalArgumentException(MethodArgumentNotValidException e) {

        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(error -> {
            errors.put(error.getField(), error.getDefaultMessage());
        });
        log.error("handleIllegalArgumentException errors: {}", errors);

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
    }

2) HttpMessageNotReadableException
: JSON 형식 오류나 요청 본문 파싱 오류를 처리

	@ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseEntity<String>
    handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body("Invalid request body: " + ex.getMessage());
    }

3) NoSuchElementException
: Optional.isPresent(), Optional.get() 등에서 없을 때 발생!

    @ExceptionHandler(NoSuchElementException.class)
    protected ResponseEntity<?> notExist(NoSuchElementException e) {

        String msg = e.getMessage();

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("msg", msg)); // "msg": "No value present"
    }

3) EntityNotFoundException
: JPA 엔티티를 찾지 못했을 때 발생하는 예외를 처리

    @ExceptionHandler(EntityNotFoundException.class)
    protected ResponseEntity<?> notExist(EntityNotFoundException e) {

        String msg = e.getMessage();

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("msg", msg));
    }

4) MethodArgumentTypeMismatchException
: 클라이언트가 전달한 요청 값의 타입과 컨트롤러 메소드의 매개변수 타입이 일치하지 않을 때 발생

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ResponseEntity<String>
    handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException
                                             ex) {
        String errorMessage = String.format(
                "Invalid argument: '%s'. Expected type: '%s'.",
                ex.getValue(),
                ex.getRequiredType().getSimpleName()
        );
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMessage);
    }


참고

profile
배운 것을 기록합니다.

0개의 댓글