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
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/
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 (이 숫자에 허용되는 최대 소수 자릿수) |
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://www.baeldung.com/spring-boot-bean-validation
https://www.baeldung.com/java-bean-validation-not-null-empty-blank
'스터디 > 스프링' 카테고리의 다른 글
Spring Data JDBC - 엔티티 생성편 (0) | 2020.05.05 |
---|---|
[미션] Producer - Consumer - 1단계 (0) | 2020.04.27 |
GCP Cloud Storage 객체 업로드, 다운로드 (0) | 2020.03.01 |
MockRestServiceServer를 이용해 RestTemplate Test 하기 (0) | 2020.01.31 |
SpringBoot + Vue 하나의 포트로 개발해보기 (0) | 2020.01.28 |