Part3 - CH6. 스프링 기능 활용하기(1) Validation
Spring Boot Validation
Validation
- Validation이란?
값에 대한 검증을 하는 것을 말하며, 보통 java프로그래밍에서는 NPE(Null Point Exception)을 위한 Null값 검증을 하는것을 말한다.
public void run(String account, Spring pw, int age){
if(account == null || pw == null){
return;
}
if(age == 0){
return;
}
}
- 검증해야 할 값이 많은 경우 코드의 길이가 길어진다.
- 구현에 따라서 달라질 수 있지만 Service Logic과의 분리가 필요하다.
- 흩어져 있는 경우 어디에서 검증을 하는지 알기 어려우며, 재사용에 한계가 있다.
- 검증Logic이 변경되는 경우 테스트 코드 등 참조하는 클래스에서 Logic이 변경되어야 하는 부분이 발생할 수 있다.
위와 같은 이유로 Spring에서는 일관된 Validator로 관리하도록 어노테이션을 지원한다.
Annotation | 기능 |
---|---|
Email형식이 아닐시 불가 | |
@Size | 문자길이측정, int type불가 |
@NotNull | null값 불가 |
@NotEmpty | null, “” 불가 |
@NotBlank | null, “”, “ “ 불가 |
@Past | 과거 날짜 |
@PastOrPresent | 과거 혹은 현재 날짜 |
@Future | 미래 날짜 |
@FutureOrPresent | 미래 혹은 현재 날짜 |
@Pattern | 정규식 적용 |
@Max | 최대값 |
@Min | 최소값 |
@AssertTure/False | 별도 Logic 적용 |
@Valid | 해당 Object Validation 실행 |
Validation Settings
-
gradle dependency implementation ‘org.springframework.boot:spring-boot-starter-validation’
-
bean validation spec https://beanvalidator.org/2.0-jsr380/
-
핸드폰 번호 정규식 “^\d{2,3}-\d{3,4}-\d{4}$”
Validation 적용하기
일단 dto를 작성할때에 Validation을 할 내용의 어노테이션을 붙여준다.
public class User {
@NotBlank //null, "", " " 모두 불가
private String name;
@Min(value = 0, message = "아직 태어나지 않았습니까?") //최소값
@Max(value = 90, message = "90살 이하만 가능합니다.") //최대값
private int age;
@Email //이메일 양식
private String email;
//정규식 패턴
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxxx-xxxx")
private String phoneNumber;
}
위와 같이 가능하며, 각 validation이 실패하였을때의 message도 정의 가능하다.
그럼 컨트롤러에서 validation을 적용시키려면 다음과 같이한다.
@PostMapping("/user")
public ResponseEntity user(@Valid @RequestBody User user, BindingResult bindingResult){
System.out.println(user);
if(bindingResult.hasErrors()){
StringBuilder sb = new StringBuilder();
bindingResult.getAllErrors().forEach(error ->{
FieldError field = (FieldError) error;
String message = error.getDefaultMessage();
// System.out.println("field : " + field.getField());
// System.out.println(message);
sb.append("field : " + field.getField());
sb.append(", message : " + message + "\n");
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
}
return ResponseEntity.ok(user);
}
validaiton을 활성화 시키려면 활성화 시키려는 곳에 @Valid 어노테이션을 붙여서 적용해준다. 그리고 여기 에서는 아직 예외처리를 배우지 않았다고 가정하고 BindingResult라는 객체를 통해 에러상태를 리턴해주도록 하였다.
이렇게 작성하면, API요청하는 곳에서 어떤 값의 Validation이 실패하였는지 바로 에러메세지를 받아서 볼 수 있게 된다.
Custom validation
- @AssertTure/False 와 같은 method 지정을 통해서 Custom Logic 적용 가능
- ConstraintValidator 를 적용하여 재사용 가능한 Custom Logic 적용 가능
@AssertTure/False
@AssertTrue/False 어노테이션은 validation로직을 적용할 메소드앞에 붙여서 사용할 수 있다.
@AssertTrue/False 어노테이션을 붙이는 메소드의 이름은 is로 시작해야한다.
public class User {
@NotBlank
private String name;
@Min(value = 0, message = "아직 태어나지 않았습니까?")
@Max(value = 90, message = "90살 이하만 가능합니다.")
private int age;
@Email
private String email;
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxxx-xxxx")
private String phoneNumber;
@Size(min = 6, max = 6)
private String reqYearMonth; // yyyymmdd
@AssertTrue(message = "yyyyMM의 형식에 맞지 않습니다.")
public boolean isReqYearMonthValidation() { //method 명이 is로 시작해야함
try {
LocalDate localDate = LocalDate.parse(getReqYearMonth() + "01", DateTimeFormatter.ofPattern("yyyyMMdd"));
}catch (Exception e){
return false;
}
return true;
}
//toString, getter, setter생략
}
ConstraintValidator
그런데 위와 같이, AssertTrue를 이용한 커스텀어노테이션을 매번 dto를 만들때마다 붙여주기는 매우 번거로 우니 이를 재사용하기 위해서는 어노테이션을 직접 만들어서 붙여주는 방법을 사용해야 한다.
다른 validator어노테이션을 참고하여 다음과 같이 어노테이션 클래스를 만들어준다.
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = { YearMonthValidator.class }) //어떤 클래스를 가지고 validation할것인지
public @interface YearMonth {
String message() default "yyyyMM의 형식에 맞지 않습니다.";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
String pattern() default "yyyyMMdd"; //validator에서 01을 붙여서 검증할 것이니 default를 바꾸자.
}
그 다음, 실제로 YearMonth어노테이션이 동작하게 하기 위한 ConstraintValidator를 상속받은 YearMonthValidator를 하나 만들어준다.
public class YearMonthValidator implements ConstraintValidator<YearMonth, String> {
private String pattern; //"yyyyMM"
@Override
public void initialize(YearMonth constraintAnnotation) {
this.pattern = constraintAnnotation.pattern();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// yyyyMM01
// value에 어노테이션 붙인 dto의 값이 들어올 것임.
try {
LocalDate localDate = LocalDate.parse(value + "01", DateTimeFormatter.ofPattern(this.pattern));
}catch (Exception e){
return false;
}
return true;
}
}
그런 다음에 위에서 적용했던 커스텀 validator인 isReqYearMonthValidation대신에 @YearMonth 어노테이션만 붙여주게되면 Validation이 완료된다. 그리고 다른곳에도 재사용 할 수 있다.
추가실습
지난시간 ObjectMapper수업에서 사용했던 Car dto를 복사해 온 후, User에 List
name, carNumber, type에 @NotBlank어노테이션을 붙인 후 해당 값들을 공백으로 보내 보면 정상응답이 반환되는 것을 볼 수 있다. 그런데 이는 User클래스에서 cars를 추가 할 때 @Valid 어노테이션을 통해서 해당 클래스의 Validator 어노테이션들이 동작하도록 해줘야만 한다.
@Valid
private List<Car> cars;