@Valid 를 이용해 @RequestBody 객체 검증하기

Springboot를 이용해서 어노테이션을 이용한 validation을 하는 방법을 적으려 한다. 
RestController를 이용하여 @RequestBody 객체를 사용자로부터 가져올 때, 들어오는 값들을 검증할 수 있는 방법을 소개한다.

Jakarata Bean Validation API Packages에 있는 javax.validation.constraints package에 있는 기본적인 검증 어노테이션을 이용한다.
@Valid를 이용하면, service 단이 아닌 객체 안에서, 들어오는 값에 대해 검증을 할 수 있다.

javax.validation.constraints 패키지를 보면 많은 어노테이션들이 존재한다. @Valid를 이용한 객체 검증 시 기본적으로 이 어노테이션을 이용한다. 사실 이름만 봐도 각각의 용도를 이해할 수 있다. 

포스팅 관련 소스코드 : https://github.com/mjung1798/Jyami-Java-Lab/tree/master/annotation-validation

 

mjung1798/Jyami-Java-Lab

Jyami의 Spring boot 및 Java 실험소. Contribute to mjung1798/Jyami-Java-Lab development by creating an account on GitHub.

github.com

 

1. @Valid를 사용하는 방법

간단한 API를 작성해보자.

@RestController
@Slf4j
public class TestController {

    @PostMapping("/user")
    public ResponseEntity<String> savePost(final @Valid @RequestBody UserDto userDto) {
        log.info(userDto.toString());
        return ResponseEntity.ok().body("postDto 객체 검증 성공");
    }
}

파라미터로 @RequestBody 어노테이션 옆에 @Valid를 작성하면, RequestBody로 들어오는 객체에 대한 검증을 수행한다. 이 검증의 세부적인 사항은 객체 안에 정의를 해두어야 한다.

@ToString
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserDto {

    @NotNull
    private String name;
    
    @Email
    private String email;
}

위와 같이 UserDto 객체를 정의한 후 각 필드에 맞는 어노테이션을 사용하면 된다.

@NotNull : 인자로 들어온 필드 값에 null 값을 허용하지 않는다
@Email : 인자로 들어온 값을 Email 형식을 갖추어야 한다.
필드에 맞는 어노테이션에 대한 설명은 아래로 스크롤을 내려보자 : 3. javax.constraint 어노테이션 이해하기

실제로 PostMan을 이용해서 valid가 적용되는지 확인하기 위해, email 값을 이상하게 줘보면,

자동으로 일정 규격에 맞춰서 springboot가 생성한 error 틀에 따라 response가 나간다.
즉, @Valid와 검증 어노테이션만 같이 잘 써줘도, 객체 단에서 에러를 잡을 수 있다는 뜻이다.

@Valid 어노테이션의 동작 여부를 확인하기 위한 테스트 코드는 간단한 Controller 테스트를 이용하여 확인할 수 있다.
아래 테스트 코드는 @NotNull 어노테이션에 따라, name을 null로 넣었을 경우를 가정한 테스트이다.

@SpringBootTest
@AutoConfigureMockMvc
class TestControllerTest {

    @Autowired private MockMvc mockMvc;
    @Autowired private ObjectMapper objectMapper;

    @Test
    @DisplayName("@NotNull 에러 테스트")
    void isValidObject() throws Exception {

        UserDto userDto = UserDto.builder()
                .email("mjung1798@gmail.com")
                .build();

        String userDtoJsonString = objectMapper.writeValueAsString(userDto);

        mockMvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(userDtoJsonString))
                .andExpect(status().isBadRequest());
    }
}

 

2. @Valid시 발생하는 Exception Handling 

@Valid로 requestBody로 들어온 객체의 검증이 이루어지면서 위와 같이 BadRequest가 나가는 경우에 custom 한 errorhandling도 할 수 있다. 

위에서 잘못된 객체 값이 나갔을 때 Springboot에 올라온 Log를 살펴보면 MethodArgumentNotValidException이 발생했음을 알 수 있어, 이 Exception을 사용하여 custom한 ErrorMessage를 response로 내보낼 수도 있다.

@ControllerAdvice를 이용한 전역 에러 핸들링, 혹은 @Controller단에서의 지역 에러 핸들링을 사용하면 된다.
MethodArgumentNotValidException에 대한 @ExceptionHandler 어노테이션을 지정하여 커스텀 에러 핸들링을 해보자

@RestControllerAdvice
public class ApiControllerAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex){
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors()
                .forEach(c -> errors.put(((FieldError) c).getField(), c.getDefaultMessage()));
        return ResponseEntity.badRequest().body(errors);
    }

}

ResponseEntity 값으로, error가 난 field 값과, 에러 메시지를 Map 형태로 만들어서, Response로 넣어주었다. 이때 Map으로 선언하여 forEach를 한 이유는 @Valid를 사용할 때, 해당 객체에서 valid에 실패한 내용을 모두 리턴해주기 때문에, 모든 error 값을 수용하기 위해서이다.

이 상태로 다시 서버를 Run 시켜서 Postman으로 확인을 해본다.
이때는 @NotNull, @Email 모두 validation이 안되도록 requestBody를 작성하였다.

Response값을 살펴보면, BadRequest인 status값, @Valid를 통과하지 못한 모든 필드 값에 대한 어려와, 에러 내용을 커스텀하게 내려준 것이 잘 반영되었음을 알 수 있다.

 

3. javax.constraint 어노테이션 이해하기

가장 아래에 관련 표를 넣어두었다 필요할 때 참고하자.

공식 문서 : https://docs.jboss.org/hibernate/beanvalidation/spec/2.0/api/

 

Jakarta Bean Validation API 2.0.2

 

docs.jboss.org

 

1. 문자열 유무 검증 (@NotBlank, @NotEmpty, @NotNull의 차이점)

@NotBlank 
- null 이 아닌 값이다.
- 공백이 아닌 문자를 하나 이상 포함한다

@NotEmpty
- Type :
 CharSequence (length of character) Collection (collection size) Map (map size Array (array length)
- null 이거나 empty(빈 문자열)가 아니어야 한다.

@NotNull
- Type :
 어떤 타입이든 수용한다.
- null 이 아닌 값이다.

@Null
- Type :
어떤 타입이든 수용한다.
- null 값이다.

이 부분은 헷갈리는 부분이라 DTO와 Contoller를 만들어서 확인해보자.

@NoArgsConstructor
@Getter
@ToString
public class NotDto {
    @NotNull
    private String notNull;
    @NotEmpty
    private String notEmpty;
    @NotBlank
    private String notBlank;
}

결과는 아래와 같다. 각각의 error message를 통해 각 validation 방법을 확인 할 수 있다.

  • @NotNull : 반드시 값이 있어야 한다.
  • @NotEmpty : 반드시 값이 존재하고 길이 혹은 크기가 0보다 커야한다.
  • @NotBrank : 반드시 값이 존재하고 공백 문자를 제외한 길이가 0보다 커야 한다.

 

  null "" " "
@NotNull Invalid Valid Valid
@NotEmpty Invalid Invalid Valid
@NotBlank Invalid Invalid Invalid

용도에 맞게 validation을 할 수 있도록 확인하자.

 

2. 최대 최소에 대한 검증

suppportType 
- BigDecimal BigInteger CharSequence byte, short, int, long, 이에 대응하는 Wrapper 클래스
- double, float는 rounding error 때문에 지원하지 않는다.
- null도 valid로 간주된다.

Validation
- @DecimalMax : 지정된 최대 값보다 작거나 같아야 한다.
      Require : String value  => max 값을 지정한다.
- @DecimalMin : 지정된 최대 값보다 크거나 같아야 한다.
      Require : String value  => min 값을 지정한다.
- @Max : 지정된 최대 값보다 작거나 같아야 한다.
      Require : int value  => max 값을 지정한다.
- @Max : 지정된 최대 값보다 크거나 같아야 한다.
      Require : int value  => min 값을 지정한다.

Usage

@NoArgsConstructor
@Getter
@ToString
public class MinMaxDto {
    @DecimalMax(value = "1000000000")
    private BigInteger decimalMax;
    @DecimalMax(value = "1")
    private BigInteger decimalMin;
    @Max(value = 1000)
    private Integer max;
    @Max(value = 1)
    private Integer min;
}

DecimalMax/Min과, Max/Min의 차이는 범위 값의 차이이다. String을 사용하느냐 Integer를 사용하느냐에 따라 범위 값이 현저히 달라지기 때문이다

 

3. 범위 값에 대한 검증

suppportType 
- BigDecimal BigInteger CharSequence byte, short, int, long, double, float 이에 대응하는 Wrapper 클래스
- null도 valid로 간주된다.

Validation
- @Positive : 양수인 값이다.
- @PositiveOrZero : 0이거나 양수인 값이다.
- @Negative : 음수인 값이다.
- @NegativeOrZero : 0이거나 음수인 값이다.

Usage

@NoArgsConstructor
@Getter
@ToString
public class RangeDto {
    @Positive
    private Integer positive;
    @PositiveOrZero
    private Integer positiveOrZero;
    @Negative
    private Integer negative;
    @NegativeOrZero
    private Integer negativeOrZero;
}

 

4. 시간 값에 대한 검증

suppportType 
- java.util.Date java.util.Calendar java.time.Instant java.time.LocalDate java.time.LocalDateTime java.time.LocalTime java.time.MonthDay java.time.OffsetDateTime java.time.OffsetTime java.time.Year java.time.YearMonth java.time.ZonedDateTime java.time.chrono.HijrahDate java.time.chrono.JapaneseDate java.time.chrono.MinguoDate java.time.chrono.ThaiBuddhistDate
- null도 valid로 간주된다.

Validation
- @Future : Now 보다 미래의 날짜, 시간이어야 한다.
- @FutureOrPresent : Now 거나 미래의 날짜, 시간이어야 한다.
- @Past : Now 보다 과거 의의 날짜, 시간이어야 한다.
- @PastOrPresent : Now 거나 과거의 날짜, 시간이어야 한다.

Now의 기준 : ClockProvider의 가상 머신에 따라 현재 시간을 정의하며 필요한 경우 default time zone을 적용한다.

Usage

@NoArgsConstructor
@Getter
@ToString
public class TimeDto {
    @Future
    private Date future;
    @FutureOrPresent
    private Date futureOrPresent;
    @Past
    private Date past;
    @PastOrPresent
    private Date pastOrPresent;
}

 

4. 이메일 검증

suppportType 
- null도 valid로 간주된다.

Validation
- @Email : 올바른 형식의 이메일 주소여야 한다. (@가 들어가야한다.)

Usage

@NoArgsConstructor
@Getter
@ToString
public class EmailDto {
    @Email
    private String email;
}

 

5. 자릿수 범위 검증

suppportType 
- BigDecimal BigInteger CharSequence byte, short, int, long, 이에 대응하는 Wrapper 클래스
- null도 valid로 간주된다.

Validation
- @Digits : 허용된 범위 내의 숫자이다.
      Require : int integer  => 이 숫자에 허용되는 최대 정수 자릿수
      Require : int fraction  => 이 숫자에 허용되는 최대 소수 자릿수

Usage

@NoArgsConstructor
@Getter
@ToString
@Builder
@AllArgsConstructor
public class DigitsDto {
    @Digits(integer = 5, fraction = 5)
    private Integer digits;
}

 

6. Boolean 값에 대한 검증

suppportType 
- Boolean, boolean

Validation
- @AssertTrue : 값이 항상 True 여야 한다.
- @AssertFalse : 값이 항상 False 여야 한다.

Usage

@NoArgsConstructor
@Getter
@ToString
public class BooleanDto {
    @AssertTrue
    private boolean assertTrue;
    @AssertFalse
    private boolean assertFalse;
}

 

7. 크기 검증

suppportType 
- CharSequence (length of character sequence) Collection (collection size) Map (map size) Array (array length)
- null도 valid로 간주된다.

Validation
- @Size : 이 크기가 지정된 경계(포함) 사이에 있어야 한다.
      Require : int max  => element의 크기가 작거나 같다.
      Require : int min  => element의 크기가 크거나 같다.

Usage

@NoArgsConstructor
@Getter
@ToString
public class SizeDto {
    @Size(max = 5, min = 3)
    private String size;
}

 

8. 정규식 검증

suppportType 
- CharSequence
- null도 valid로 간주된다.

Validation
- @Pattern : 지정한 정규식과 대응되는 문자열 이어야 한다. Java의 Pattern 패키지의 컨벤션을 따른다
      Require : String regexp  => 정규식 문자열을 지정한다

Usage

@NoArgsConstructor
@Getter
@ToString
public class PatternDto {
    //yyyy-mm-dd 형태를 가지는 패턴 조사
    @Pattern(regexp = "^(19|20)\\d{2}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[0-1])$")
    private String pattern;
}

 

4. @Valid 정리 표

@AssertTrue Boolean, boolean 값이 항상 True 여야 한다.  
@DecimalMax 실수 제외 숫자 클래스. 지정된 최대 값보다 작거나 같아야 하는 숫자이다. String : value (max 값을 지정한다.)
@DecimalMin 실수 제외 숫자 클래스. 지정된 최소 값보다 크거나 같아야하는 숫자이다. String : value (min 값을 지정한다.)
@Digits BigDecimalBigIntegerCharSequencebyte, short, int, long, 이에 대응하는 Wrapper 클래스 허용된 범위 내의 숫자이다. int : integer (이 숫자에 허용되는 최대 정수 자릿수)
int : fraction (이 숫자에 허용되는 최대 소수 자릿수)
@Email null도 valid로 간주된다. 올바른 형식의 이메일 주소여야한다.  
@Future 시간 클래스 Now 보다 미래의 날짜, 시간  
@FutureOrPresent 시간 클래스 Now의 시간이거나 미래의 날짜, 시간  
@Max 실수 제외 숫자 클래스. 지정된 최대 값보다 작거나 같은 숫자이다. long : value (max 값을 지정한다)
@Min 실수 제외 숫자 클래스. 지정된 최소 값보다 크거나 같은 숫자이다. long : value (min 값을 지정한다)
@Negative 숫자 클래스 음수인 값이다.  
@NegativeOrZero 숫자 클래스 0이거나 음수인 값이다  
@NotBlank   null 이 아닌 값이다.공백이 아닌 문자를 하나 이상 포함한다  
@NotEmpty CharSequence,Collection, Map, Array null이거나 empty(빈 문자열)가 아니어야 한다.  
@NotNull 어떤 타입이든 수용한다. null 이 아닌 값이다.  
@Null 어떤 타입이든 수용한다. null 값이다.  
@Past 시간 클래스 Now보다 과거의 날짜, 시간  
@PastOrPresent 시간클래스 Now의 시간이거나 과거의 날짜, 시간  
@Pattern 문자열 지정한 정규식과 대응되는 문자열이어야한다. Java의 Pattern 패키지의 컨벤션을 따른다 String : regexp (정규식 문자열을 지정한다)
@Positive 숫자 클래스 양수인 값이다  
@PositiveOrZero 숫자 클래스 0이거나 양수인 값이다.  
@Size CharSequence,Collection, Map, Array 이 크기가 지정된 경계(포함) 사이에 있어야한다. int : max (element의 크기가 작거나 같다)
int : min (element의 크기가 크거나 같다)

[@ValidAnnotation].List : 동일한 요소에 여러개의 @ValidAnnotation[] 제약조건을 정의한다.

실제로 잘 사용하지는 않는 것으로 보인다.

 

[관련 블로그]

같은 내용의 글을 팀 블로그에도 업로드 한 상태이다.

https://javabom.tistory.com/

 

자바봄

자바 스프링 블로그 입니다.

javabom.tistory.com

 

참고 자료

https://www.baeldung.com/spring-boot-bean-validation

https://www.baeldung.com/java-bean-validation-not-null-empty-blank

 

Comments 0