😡😎

위와 같이 단순하게 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);
🎅 위와같이 해주면
