SpringBoot(5) 실무 모범 사례

Yeppi's 개발 일기·2022년 7월 4일
0

Spring&SpringBoot

목록 보기
13/16
post-thumbnail
post-custom-banner

앞서 배웠던 Validation 과 Exception 을 활용하여, 실무에서 적용되는 모범적인 사례를 살펴보자
계속 발생하는 에러를 뽑고, 뽑은 에러로 메서드 만들어서 에러 처리해보자


1. 실습

  • APIController.java
    • 전 실습에서 했던 것처럼, 해당 클래스에서 에러 메서드를 처리하면?

    • 우선적으로 에러 메서드를 타기 때문에, 원하는 에러메시지를 출력할 수 없다. 우선 주석처리해두자

      @RestController
      @RequestMapping("/api/user")
      @Validated
      public class APIController {
      
          @GetMapping("")
          public User get(
      			// 조건 추가
                  @Size(min = 1)
                  @RequestParam String name,
      
                  @NotNull
                  @Min(1)
                  @RequestParam Integer age) {
              User user = new User();
              user.setName(name);
              user.setAge(age);
      
              // 예외 발생
              int a = 10+age;
              return user;
          }
      
          @PostMapping("")
          public User post(@Valid @RequestBody User user) {
              System.out.println(user);
              return user;
          }
      
      /*  // 전 시간에 했던 예외 처리 메서드는 주석처리
      		// 특정 예외를 처리하는 메서드
          @ExceptionHandler(value = MethodArgumentNotValidException.class)
          public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e) {
              System.out.println("api controller");
              return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
          }*/
      
      }

  • APIControllerAdvice.java 에서 에러 메서드 처리하기.
    전 실습의 글로벌 에러처리 클래스이다.
    @RestControllerAdvice(basePackageClasses = APIController.class)
    public class APIControllerAdvice {
    
        // 예외 잡는 메서드
        @ExceptionHandler(value = Exception.class) // 모든 예외
        public ResponseEntity exception(Exception e) {
            System.out.println(e.getClass().getName());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
        }
    
        // 특정 예외를 처리하는 메서드
        @ExceptionHandler(value = MethodArgumentNotValidException.class)
        public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e) {
            BindingResult bindingResult = e.getBindingResult();
    
    				// 콘솔창에 친절한 에러 출력
            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);
                System.out.println(message);
                System.out.println(value);
            });
    
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
        }
    
    		// 추가적인 에러 처리
        @ExceptionHandler(value = ConstraintViolationException.class)
        public ResponseEntity constraintViolationException(ConstraintViolationException e) {
            e.getConstraintViolations().forEach(error ->{
                System.out.println(error);
            });
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
        }
    
    		// 추가적인 에러 처리
        @ExceptionHandler(value = MissingServletRequestParameterException.class)
        public ResponseEntity missingServletRequestParameterException(MissingServletRequestParameterException e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
        }
    
    }

  • 브라우저에서 POST 방식으로 요청
    {
      "name" : "",
      "age" : 0
    }
    response body 는 전 예제의 결과와 같고, 콘솔창에도 이제 친절히 설명해준다
    name
    크기가 1에서 10 사이여야 합니다
    
    name
    비어 있을 수 없습니다
    
    age
    1 이상이어야 합니다
    0

  • APIControllerAdvice.java 에서 에러 메서드 처리
    위예제에서 ConstraintViolationException 를 추가로 처리
    @RestControllerAdvice(basePackageClasses = APIController.class)
    public class APIControllerAdvice {
    
    .
    .
    .
    
    		@ExceptionHandler(value = ConstraintViolationException.class)
        public ResponseEntity constraintViolationException(ConstraintViolationException e) {
            e.getConstraintViolations().forEach(error ->{
                String fieldName = error.getLeafBean().toString();
                String message = error.getMessage();
                String invalidValue = error.getInvalidValue().toString();
    
                System.out.println("============");
                System.out.println(fieldName);
                System.out.println(message);
                System.out.println(invalidValue);
            });
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
        }
    
    }
  • 에러 처리 메서드마다, 길게 있던 에러 메시지를 각각 추출

    @RestControllerAdvice(basePackageClasses = APIController.class)
    public class APIControllerAdvice {
    
        . . .
    
        // 특정 예외를 처리하는 메서드
        @ExceptionHandler(value = MethodArgumentNotValidException.class)
        public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest httpServletRequest) {
            List<Error> errorList = new ArrayList<>();
    
            BindingResult bindingResult = e.getBindingResult();
            bindingResult.getAllErrors().forEach(error -> {
                FieldError fieldError = (FieldError) error;
    
                String fieldName = fieldError.getField();
                String message = fieldError.getDefaultMessage();
                String invalidValue = fieldError.getRejectedValue().toString();
    
                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);
        }
    
        @ExceptionHandler(value = ConstraintViolationException.class)
        public ResponseEntity constraintViolationException(ConstraintViolationException e, HttpServletRequest httpServletRequest) {
            List<Error> errorList = new ArrayList<>();
    
            e.getConstraintViolations().forEach(error ->{
                Stream<Path.Node> stream = StreamSupport.stream(error.getPropertyPath().spliterator(), false);
                List<Path.Node> list = stream.collect(Collectors.toList());
    
                String fieldName = list.get(list.size() -1).getName();
                //String fieldName = error.getPropertyPath().spliterator().;
                String message = error.getMessage();
                String invalidValue = error.getInvalidValue().toString();
    
                Error errorMessage = new Error();
                // 위 코드 동일
    
                errorList.add(errorMessage);
            });
    
            // 어떤 요청에 대해 에러 발생 시, 응답 정보
            // 위 코드 동일
        }
    
        @ExceptionHandler(value = MissingServletRequestParameterException.class)
        public ResponseEntity missingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest httpServletRequest) {
            List<Error> errorList = new ArrayList<>();
    
            String fieldName = e.getParameterName();
            //String fieldType = e.getParameterType();
            String invalidValue = e.getMessage();
    
            Error errorMessage = new Error();
            // 위 코드 동일
    
            // 어떤 요청에 대해 에러 발생 시, 응답 정보
            // 위 코드 동일
        }
    
    }

  • POST 방식의 브라우저 결과
    아주아주 친절하게 response body에 메시지가 담김

    {
      "name" : "yeppiyeppiyeppiyeppi",
      "age" : 11
    }
    // 응답결과
    {
        "statusCode": "400 BAD_REQUEST",
        "requestUrl": "/api/user",
        "code": null,
        "message": "",
        "resultCode": "FAIL",
        "errorList": [
            {
                "field": "name",
                "message": "크기가 1에서 10 사이여야 합니다",
                "invalidValue": "yeppiyeppiyeppiyeppi"
            },
            {
                "field": "age",
                "message": "1 이상이어야 합니다",
                "invalidValue": "0"
            }
        ]
    }


🍑 정리 🍑

클라이언트는 response 만 받아도 에러를 명시적으로 알 수 있음

  • 클라이언트가 서버에 검증을 요청하는 부분을 일관되게 처리한 것

  • 클라이언트는 메시지를 친절하게 받아서 서버를 편리하게 사용할 수 있음

  • 클라이언트의 요청 값도 APIController.java를 통해 @Annotation 를 일관되게 적용하여 관리가 편함


Spring 프레임워크는 엔터프라이즈 프레임워크이다

  • 위 예제처럼
    APIController.java 로 컨트롤러는 하나 만들어두고, API는 2개 뿐인데,
    굳이 번거롭게 APIControllerAdvice.java 처럼 일일히 다 처리해줄 필요는 없다

    컨트롤러에서 if문같은 분기처리 하는 것이 오히려 효율이 좋다


  • 그런데, 엔터프라이즈는 매우 방대한 데이터를 가지는 서버 프레임워크이기 때문에
    API 컨트롤러가 4,5개 이상, 주소가 10개 이상 정도되는 경우에는?

    위 예제처럼, 일관되게 적용시키기 위해
    Validation 사용, ControllAdvice 로 글로벌 예외처리를 해줘야한다


  • 이렇게 클라이언트에게 친절한 메시지를 제공하고, 숨겨야하는 메시지는 따로 숨기면서
    우리가 사용했던 일반적인 메시지를 내보내는 것이 가능하다
profile
imaginative and free developer. 백엔드 / UX / DATA / 기획에 관심있지만 고양이는 없는 예비 개발자👋
post-custom-banner

0개의 댓글