프로그래밍에 있어서 가장 중요한 부분
예를 들어 Java에서는 null 값에 접근 시, null pointer exception이 발생한다.
이러한 부분을 방지 하기 위해서 미리 검증을 하는 과정 = Validation
검증해야 할 값이 많은 경우 코드의 길이가 길어짐
Service Logic과 분리
흩어져 있는 경우 어디에서 검증을 하는지 알기 어렵다
재사용의 한계
검증 Logic이 변경 되는 경우
참조하는 클래스(해동 Logic 을 사용하는 쪽)에서 Logic이 변경 발생
@Annotation
종류 | 기능 |
---|---|
@Size | null 불가능 |
@NotNull | null, “” 불가능 |
@NotBlank | null, “”, “ “ 불가능 |
@Past | 과거 날짜 |
@PastOrPresent | 오늘 or 과거 날짜 |
@Future | 미래 날짜 |
@FutureOrPresent | 오늘 or 미래 날짜 |
@Pattern | 정규식 적용 |
@Max | 최대값 |
@Min | 최소값 |
@AssertTrue/False | 별도 로직 적용 |
@Valid | 객체 검증(Validation) 실행 |
인텔리제이로 실습을 진행했다
gradle dependecies
https://hibernate.org/validator/
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation' // 추가
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
bean validation 예시
https://hibernate.org/validator/
예전 방식
@RestController
@RequestMapping("/api")
public class ApiController {
@PostMapping("/user")
public Object user( @RequestBody User user) {
System.out.println(user);
// 옛날식 코드 => 개수가 증가하면 관리 힘듦
if(user.getAge() >= 90 /*user.getPhoneNumber() == "xxx-xxxx-xxxx"*/) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(user);
}
return ResponseEntity.ok(user);
}
}
@Vaild
사용해보기
User.java
에 @Email
추가@Email
private String email;
@Valid
추가@PostMapping("/user")
public Object user(@Valid @RequestBody User user) { . . . }
User{name='이예삐', age=20, email='aaa@naver.com', phoneNumber='01012345678'}
"^\\d{2,3}-\\d{3,4}-\\d{4}$"
로 설정해서 검증하는 예제@Pattern()
추가@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$")
private String phoneNumber;
콘솔에도 잘 출력
User{name='이예삐', age=20, email='aaa@naver.com', phoneNumber='010-1234-5678'}
ApiController.java
// 에러 메시지 출력
@PostMapping("/user")
public ResponseEntity user(@Valid @RequestBody User user, BindingResult bindingResult) {
// 에러 처리
if(bindingResult.hasErrors()) {
StringBuilder sb = new StringBuilder();
bindingResult.getAllErrors().forEach(objectError -> {
FieldError field = (FieldError) objectError;
String message = objectError.getDefaultMessage();
System.out.println("filed : " + field.getField());
System.out.println(message);
// JSON body에 친절한 에러 메시지 전달
sb.append("field : " + field.getField());
sb.append(("message : " + message));
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
}
// 실질적인 logic 실행되는 부분
System.out.println(user);
return ResponseEntity.ok(user);
}
@Pattern()
에 message 추가@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx(x)-xxxx")
private String phoneNumber;
@NotBlank
, @Max
📌User.java
@NotBlank
private String name;
@Max(value = 100)
private int age;
위에서 설명한 Validation @Annotation
를 사용하고 테스트 작성 시, 꼭 예외 케이스가 나오게 된다.
이러한 예외 사항들을 검사하기 위해 Custom Validation
를 활용한다.
대표적인 ex. 날짜 형식
@Past
등을 이용하지 않고, String
형태로 값을 사용
방법 2가지
AssertTrue / False
와 같은 method
지정을 통해서 Custom Logic 적용 가능
⇒ 재사용이 불가능
ConstraintValidator
를 적용하여 Custom Logic 적용 가능
⇒ 재사용 가능
@AssertTrue
📌재사용이 불가능하지만, 간단한 방식
boolean
값을 리턴하는 메서드는 is
를 메서드명 앞에 붙이기
@AssertTrue(message = "yyyyMM 의 형식에 맞지 않습니다.")
public boolean isReqYearMonthValidation() {
System.out.println("assert true call");
}
@Constraint
📌YearMonthValidator.class
에서 사용하는 인터페이스 YearMonth.java
@Constraint(validatedBy = {YearMonthValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface YearMonth {
String message() default "yyyyMM 의 형식에 맞지 않습니다.";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
String pattern() default "yyyyMMdd";
}
@YearMonth
의 구현
public class YearMonthValidator implements ConstraintValidator<YearMonth, String> {
private String pattern;
@Override
public void initialize(YearMonth constraintAnnotation) {
// 초기화했을 때 패턴 값 = 어노테이션의 패턴 값
this.pattern = constraintAnnotation.pattern();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 패턴에 값이 잘 들어갔는 지 value 값으로 확인
// yyyyMM01
try {
LocalDate localDate = LocalDate.parse(value + "01", DateTimeFormatter.ofPattern(this.pattern));
} catch (Exception e) {
return false; // 파싱이 안되면, false
}
return true; // 파싱이 잘되면, true
}
}
사용하기
@YearMonth
private String reqYearMonth; // yyyyMM
브라우저 실행 결과 및 콘솔창 출력 결과
filed : reqYearMonth
yyyyMM 의 형식에 맞지 않습니다.
👉 여러곳에서 재사용하여 사용가능
car.java
@NotBlank
private String name;
@NotBlank
@JsonProperty("car_number")
private String carNumber;
@NotBlank
@JsonProperty("TYPE")
private String type;
user.java
@Valid
를 꼭 붙여야함 @NotBlank
private String name;
@Max(value = 100)
private int age;
@Valid // 꼭 붙여야 Car 객체에 있는 어노테이션 동작
private List<Car> cars;
브라우저에서 실행하면 잘 실행
{
"name": "홍길동",
"age": 10,
"cars": [
{
"name": "K5",
"car_number": "11가 1111",
"TYPE": "sedan"
},
{
"name": "Q5",
"car_number": "22가 1111",
"TYPE": "SUV"
}
]
}
만약 "car_number": ""
이렇게 body 데이터가 잘못되면
콘솔창에도
filed : cars[0].carNumber
공백일 수 없습니다