😡😎
위와 같이 단순하게 validation하면 되지만, 객체가 많으면 굉장이 많은 코드를 써야한다. 정상적인 로직이 들어가야하는데 비즈니스에 상관없는 코드들이 많이 들어간다. 정리하면
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
😎디펜던시 추가하고.
public class User {
private String name;
private int age;
@Email
private String email;
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$")
private String phoneNumber;
...
...
}
😎User 클래스의 멤버에 어노테이션을 걸어준다.
@PostMapping("/user")
public User user(@Valid @RequestBody User user){
System.out.println("user : "+user);
return user;
}
😎API의 매개변수에 @Valid 를 붙여주면 끝
😎위와같이 재대로 넣으면 성공이 뜨지만
😎위와같이 이메일 형식을 위반하거나, 정규표현식 Pattern(전화번호)에 만족하지못하면
2022-01-19 18:16:55.333 WARN 7248 --- [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.validation.dto.User com.example.validation.controller.ApiController.user(com.example.validation.dto.User) with 2 errors: [Field error in object 'user' on field 'phoneNumber': rejected value [0101234-1234]; codes [Pattern.user.phoneNumber,Pattern.phoneNumber,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.phoneNumber,phoneNumber]; arguments []; default message [phoneNumber],[Ljavax.validation.constraints.Pattern$Flag;@715f31ba,^\d{2,3}-\d{3,4}-\d{4}$]; default message ["^\d{2,3}-\d{3,4}-\d{4}$"와 일치해야 합니다]] [Field error in object 'user' on field 'email': rejected value [asdf]; codes [Email.user.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@41a14204,.*]; default message [올바른 형식의 이메일 주소여야 합니다]] ]
😡이렇게 애러가 뜬다. 에러의 메세지는 어떻게 설정할까?
public User user(@Valid @RequestBody User user
, BindingResult bindingResult) { //BindingResult는
if(bindingResult.hasErrors()){ //에러가 있으면
StringBuilder sb = new StringBuilder();
bindingResult.getAllErrors().forEach(objectError -> {
FieldError fidld = (FieldError) objectError; //어떤필드 애러인가?
String message = objectError.getDefaultMessage(); //그 애러 매세지 뭔가?
System.out.println("field : " + fidld.getField());
System.out.println(message);// 메세지출력
});
}
System.out.println("user : " + user);
return user;
}
😡이때 메세지가 에러 메세지인데.... 이상한 메세지가 나올꺼다.
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$",message = "핸드폰 번호의 양식과 맞지 않습니다.. 01x-xxxx-xxxx")
private String phoneNumber;
😎 휴....
@AssertTrue(message = "yyyyMM 의 형식에 맞지 않습니다.")
public boolean isReqYearMonthValidation() { //boolean메서드는 is를 붙여주자
System.out.println("그럼 여기는?");
//DateTimeFormatter는 yyyyMM"dd" 까지 붙이기 때문에 01일을 임의로 붙여줌
try{
//String "000000" 을 날짜로 파싱
LocalDate localDate = LocalDate.parse(getReqYearMonth() + "01", DateTimeFormatter.ofPattern("yyyyMMdd"));
}catch (Exception e){
return false;
}
return true;
}
이렇게 User 클래스 안에 넣어도 되지만 재사용이 불가능 하고 만약 재사용 하려면 일일이 복사해서 넣어야 한다. 코드도 길어지고 중복도 늘어난다. 그렇기 때문에 이걸 어노테어션으로 만들어주자.
@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";
}
😎위 어노테이션이 참고할 조건도 만들어 줘야함
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) {
// yyyyMM
try{
LocalDate localDate = LocalDate.parse(value+"01" , DateTimeFormatter.ofPattern(this.pattern));
}catch (Exception e){
return false;
}
return true;
}
}
😎 그럼 위 조건을 검사할 곳에 @annotation 넣어주면 끝이다.~~
// @Size(min = 6, max = 6)
@YearMonth
private String reqYearMonth; //yyyyMM
😎 그럼 이제 yyyyMM 형식을 검사하려면 @YaearMoth넣으면 된다.
😎 Web Application의 입장에서 바라 보았을때, 에러가 났을 때 내려줄 수 있는 방법은 많지 않다.
@NotEmpty
@Size(min = 1,max = 10)
private String name;
@Min(1)
@NotNull
private Integer age;
😎 user dto 하나 만들어주고 Vaild조건들을 걸어준다.
@GetMapping("")
public User get(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age){
User user =new User();
user.setName(name);
user.setAge(age);
int a = 10+age;
return user;
}
😎 Controller 만들고
아무것도 안 넣고 Get호출하면 Nullpointexection뜬다.
java.lang.NullPointerException: null
😎 기본적으로 스프링에서 자체적으로 예외에 대해서 간단히 500에러를 보내주는걸 볼 수 있다.
😎Post호출도 마찬가지다. 조건에 맞지 않게 보내면 그냥 400에러만 던져준다.
GlobalControllerAdvice 만들어서
//@ControllerAdvice // ViewResolver 쓰면 이거 쓰면됨
@RestControllerAdvice //REST쓰면
public class GlobalControllerAdvice {
@ExceptionHandler(value = Exception.class)
public ResponseEntity exception(Exception e){
System.out.println("==============================");
System.out.println(e.getLocalizedMessage()); //에러 메세지 확인해보기
System.out.println("==============================");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(""); // 서버에서 일어난 에러 500에러
}
}
다시 한번 요청해봐
==============================
Validation failed for argument [0] in public com.ex......
==============================
😎이렇게 콘솔창에 뜬다 그럼 내가 원하는 값을 던져줄수 있겠지? 굿굿
😎하지만 이렇게하면 따로 어떤에러인지 모르고 모든 에러가 이렇게 보일꺼니까 조금 더 설 정해주자
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
ExceptionHandler 하나 더 만들어서 예외 내용을 던저준다. 이쁘지는 않지만 알아볼수 있다.
🔔😁🎅🤔
//@ControllerAdvice // ViewResolver 쓰면 이거 쓰면됨
@RestControllerAdvice(basePackageClasses = ApiController.class) //REST쓰면
public class GlobalControllerAdvice {
@ExceptionHandler(value = Exception.class)//모든 Exception
public ResponseEntity exception(Exception e){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(""); // 서버에서 일어난 에러 500에러
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)//
public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}
🎅 (basePackageClasses = ApiController.class) 이거 추가하면 이 클래스에서 일어난 것만 처리해 준다.
@Validated
public class ApiController {
@GetMapping("")
public User get(
@Size(min=2)
@RequestParam String name,
@NotNull
@Min(1)
@RequestParam Integer age){
User user =new User();
user.setName(name);
user.setAge(age);
return user;
}
🎅 GetMapping을 하면 따로 dto를 거쳐오는게 아니니까 바로 걸어주면 된다. 클래스 위에 @Validated를 넣는거 까먹지 말자.
🤔 그럼 Get은 보통 ConstraintViolationException 에러랑 Missing....에러를 주로 띄우니까 기본적으로 잡아주자.
@RestControllerAdvice(basePackageClasses = ApiController.class) //REST쓰면
public class GlobalControllerAdvice {
@ExceptionHandler(value = Exception.class) //모든 Exception
public ResponseEntity exception(Exception e){
System.out.println(e.getClass().getName() + " 예외가 나온 클래스 이름입니다.");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(""); // 서버에서 일어난 에러 500에러
}
@ExceptionHandler(value = MethodArgumentNotValidException.class) // 잘못된 값을 Post받았을때 나오는 애러
public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
@ExceptionHandler(value = ConstraintViolationException.class) // 조건에 벗어나면 나오는 애러 (GET)
public ResponseEntity constraintViolationException(ConstraintViolationException e){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
@ExceptionHandler(value = MissingServletRequestParameterException.class) //아무값도 입력안하면 나오는 애러 (GET)
public ResponseEntity missingServletRequestParameterException(MissingServletRequestParameterException e){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}
🎅 이렇게 하면 400애러가 나오겠지? 참고로 절때로 500애러가 나오지 않게 해둬야한다.=모든 애러는 미리 예상하고 잡아 둬야한다는 소리다.
@ExceptionHandler(value = MethodArgumentNotValidException.class) // 잘못된 값을 Post받았을때 나오는 애러
public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e){
BindingResult bindingResult = e.getBindingResult(); //bindingResult는 애러 정보드를 담고있다.
bindingResult.getAllErrors().forEach(error -> { //안을 한번 보자.
FieldError fieldError = (FieldError) error; //에러를 형변환 시켜주고
String fieldName = fieldError.getField(); // 이름
String message = fieldError.getDefaultMessage(); // 에러 메세지
String value =fieldError.getRejectedValue().toString(); //어떤 값이 주입됬는지 보자
System.out.println("fieldName : " +fieldName);
System.out.println("message : " +message);
System.out.println("value : " +value);
System.out.println();
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
🎅 Post Valid에러 부터 정보를 확인해보았다. 위 처럼 작성하고 값들을 보내보자.
fieldName : name
message : 비어 있을 수 없습니다
value :
fieldName : name
message : 크기가 1에서 10 사이여야 합니다
value :
fieldName : age
message : 1 이상이어야 합니다
value : 0
🎅 consol창에 이렇게 에러 정보를 볼수있다 .이런방식으로 모두 잡아주자.
@ExceptionHandler(value = MethodArgumentNotValidException.class) // 잘못된 값을 Post받았을때 나오는 애러
public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e){
BindingResult bindingResult = e.getBindingResult(); //bindingResult는 애러 정보드를 담고있다.
bindingResult.getAllErrors().forEach(error -> { //안을 한번 보자.
FieldError fieldError = (FieldError) error; //에러를 형변환 시켜주고
String fieldName = fieldError.getField(); // 이름
String message = fieldError.getDefaultMessage(); // 에러 메세지
String value =fieldError.getRejectedValue().toString(); //어떤 값이 주입됬는지 보자
System.out.println("fieldName : " +fieldName);
System.out.println("message : " +message);
System.out.println("value : " +value);
System.out.println();
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
@ExceptionHandler(value = ConstraintViolationException.class) // 조건에 벗어나면 나오는 애러 (GET)
public ResponseEntity constraintViolationException(ConstraintViolationException e){
e.getConstraintViolations().forEach(error ->{
Stream<Path.Node> stream = StreamSupport.stream(error.getPropertyPath().spliterator(), false);
List<Path.Node> list = stream.collect(Collectors.toList());
String field = list.get(list.size()-1).getName();
String message = error.getMessage();
String invalidValue = error.getInvalidValue().toString();
System.out.println("field : " +field);
System.out.println("message : " +message);
System.out.println("invalidValue : " +invalidValue);
System.out.println();
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
@ExceptionHandler(value = MissingServletRequestParameterException.class) //아무값도 입력안하면 나오는 애러 (GET)
public ResponseEntity missingServletRequestParameterException(MissingServletRequestParameterException e){
String fieldName = e.getParameterName();
String fieldType = e.getParameterType();
String invalidValue = e.getMessage();
System.out.println("fieldName : " + fieldName);
System.out.println("fieldType : " + fieldType);
System.out.println("invalidValue : " + invalidValue);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
위와 같이 디버거를 사용해서 보거나
간단하기 System.out.println로 에러의 내용을 보고 이쁘게 클라이언트가 볼수 있도록, 이해할 수 있도록 만드는것이 핵심!!!!!!!
public class ErrorResponse {
//클라이언트가 알아보기 편하게 해야함
private String statusCode;
private String requestUrl;
private String code;
private String message;
private String resultCode;
private List<Error> errorList;
...
...
}
🎅 에러들을 담을 ErrorResponse클래스 만들고
public class Error {
private String field;
private String message;
private String invalidValue;
🎅 에러세부정보를 담는 Error 클래스 만들어준다.
List<Error> errorList = new ArrayList<>();
BindingResult bindingResult = e.getBindingResult(); //bindingResult는 애러 정보드를 담고있다.
bindingResult.getAllErrors().forEach(error -> { //안을 한번 보자.
...
...
에러 정보꺼내오는 코드들...
...
Error errorMessage = new Error();
errorMessage.setField(fieldName);
errorMessage.setMessage(message);
errorMessage.setInvalidValue(invalidValue);
errorList.add(errorMessage);
});
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setErrorList(errorList);
errorResponse.setMessage("");
errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
errorResponse.setResultCode("FAIL");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
🎅 위와같이 해주면