Dev/Spring Boot

스프링의 다양한 기능(Vaildation, Exception처리)

OK-가자 2022. 3. 2. 17:41

스프링의 기능을 활용해 보자.

😡😎

Spring Boot Validation

validation은 정말 중요한 부분입니다. Java는 null값에 대해서 접근하려고 할 때 null pointer exception이 발생 함으로, 이러한 부분을 방지하기 위해서 미리 검증하는 과정을 Validation 이라고 합니다.

위와 같이 단순하게 validation하면 되지만, 객체가 많으면 굉장이 많은 코드를 써야한다. 정상적인 로직이 들어가야하는데 비즈니스에 상관없는 코드들이 많이 들어간다. 정리하면

  • 검증해야 할 값이 많은 경우 코드의 길이가 길어 진다.
  • 구현에 따라서 달라 질 수 있지만 Service Logic과의 분리가 필요하다.
  • 흩어져 있는 경우 어디에서 검증을 하는지 알기 어려우며, 재사용의 한계가 있다.
  • 구현에 따라 달라 질 수 있지만, 검증 Logic이 번경 되는 경우 테스트 코드 등 참조하는 클래스에서 Logic이 변경되어야 하는 부분이 발생 할 수 있다.

그래서 어노테이션 기반으로 지원해준다.

사용해보자.

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;

😎 휴....

Custom Validation

    1. AsserTrue/False 와 같은 method지정을 통해서 Custom Logic 적용 가능
    1. ConstraintValidator를 적용하여 재사용이 가능한 Custom Logic 적용 가능
    @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넣으면 된다.

Exception 처리

😎 Web Application의 입장에서 바라 보았을때, 에러가 났을 때 내려줄 수 있는 방법은 많지 않다.

    1. 에러 페이지
    1. 4XX Error or 5XX Error
    1. Client 가 200외에 처리를 하지 못 할 때는 200을 내려주고 별도의 에러 Message 전달

한번 해 보자.

    @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에러만 던져준다.

어떻게 처리할까?

ControllerAdvice와 ExceptionHandler사용

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 하나 더 만들어서 예외 내용을 던저준다. 이쁘지는 않지만 알아볼수 있다.

🔔😁🎅🤔

🤔조금더 심화해서 Exception과 Validation을 써보자.

//@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);

🎅 위와같이 해주면

WoW 위와같이 깔끔하게 정리된 상태로 클라이언트에게 에러정보를 보여줄수있다!!

'Dev > Spring Boot' 카테고리의 다른 글

스프링의 다양한 기능2(Filter, Interceptor)  (0) 2022.03.02
스프링의 다양한 기능(Filter, Interceptor)  (0) 2022.03.02
스프링의 핵심  (0) 2022.03.02
스프링부트 입문  (0) 2022.03.02
REST Client 설치[Talend ApI Tester]  (0) 2022.03.02