0. 시작하게 된 계기 및 다짐😮

  • 이번 코드스테이츠의 백엔드 엔지니어링 개발자 부트캠프에 참여하게 되면서 현직개발자 분들의 빠른 성장을 위한 조언 중 자신만의 블로그를 이용하여 배운 것 들을 정리하는게 많은 도움이 된다 하여 시작하게 되었다.

    • 그 날 배웠던 것을 길지 않아도 좋으니 정리하며 복습하는 습관 기르기
    • 주말에 다음주에 배울 내용들을 예습
    • 코딩 문제와 java코드들은 꾸준히 학습
    • 자료구조를 이용한 알고리즘 문제 해결 학습

1. 학습 목표 😮

목표결과
API 계층과 서비스 계층에서 발생하는 예외를 처리 및 클라이언트에 예외 메시지 전달O
@ExceptionHandler 애너테이션을 사용해서 예외를 처리O
@RestControllerAdvice 애너테이션을 사용해서 예외O
예외 발생 시, 클라이언트 쪽에 적절한 예외 메시지를 제공해 줄 수 있다.O

2. 정리 😮

@ExceptionHandler를 이용한 예외 처리

0. @애너테이션

1. @ExceptionHandler

  • 예외 처리를 위한 메서드에 애너테이션 적용

1. @ExceptionHandler를 이용한 Controller 레벨에서의 예외처리(MethodArgumentNotValidException)

  • Spring에서는 애플리케이션 문제가 발생시, 문제를 알려서 처리뿐 아니라 유효성 검증에 실패해도 예외를 던져 처리 유도

  • 에러메시지를 구체적으로 전송해주어, 어느 곳에서 문제가 발생한지 이해가 쉬움

    [예제Code]
    @ExceptionHandler
       public ResponseEntity handleException(MethodArgumentNotValidException e) {
    					// (1)
           final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
    
    					// (2)
           return new ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);
       }
    
     (1) MethodArgumentNotValidException : 메서드 에서의 유효성 검증 실패 
     (2) getBindingResult().getFieldErrors() : 위의 객체에서 발생한 에러의 정보를 확인
         => 이 FieldError를 엔티티에 담아서 요청 바디에 전달



2. ErrorResponse 클래스를 생성하여 위의 fieldError정보를 담고 필요한 부분만을 클라이언트쪽에 전달

  • 한 개 이상의 유효성 검증에 실패한 필드의 에러 정보를 담기 위해서 List 객체를 이용하며,
    이 한개의 필드 에러 정보는 FieldError 라는 별도의 static class를 ErrorResponse 클래스의 멤버 클래스
  • 이 클래스를 이용하여 기존 FieldError에서 실패한 필드(멤버 변수)의 Error정보만을 응답에 전송
 [예제 Code]
 @Getter
 @AllArgsConstructor
 public class ErrorResponse {
 		// (1)
     private List<FieldError> fieldErrors;

     @Getter
     @AllArgsConstructor
     public static class FieldError {
         private String field;
         private Object rejectedValue;
         private String reason;
     }
 }


  List<ErrorResponse.FieldError> errors =
                fieldErrors.stream()
                            .map(error -> new ErrorResponse.FieldError(
                                error.getField(),
                                error.getRejectedValue(),
                                error.getDefaultMessage()))
                            .collect(Collectors.toList());

       return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);

3. @ExceptionHandler의 단점

1). 각 Controller 클래스 내부에 @ExceptionHanlder 메서드와 같은 코드 중복이 발생
2). @유효성검증 실패 외의 각 예외를 처리하기 위한 @ExceptionHandler 핸들러 메서드를 만들어 줘야함
[Ex. MethodArgumentNotValidException(메서드 유효성 검증실패), ConstraintViolationException(제약조건)]

[Extra]

1). @ExceptionHandler 
  - https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-exceptionhandler

2). 메서드 매개변수 유효성검증(MethodArgumentNotValidException) 의 BindingResult 클래스
 - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/BindingResult.html

3). ContraintViolationException의 에러정보를 담고있는 ContraintVilation인터페이스와 구현 클래스
 - https://docs.oracle.com/javaee/7/api/javax/validation/ConstraintViolation.html
 - https://docs.jboss.org/hibernate/validator/5.3/api/org/hibernate/validator/internal/engine/ConstraintViolationImpl.html






@RestControllerAdvice를 이용한 예외처리

0. @애너테이션

1. @RestControllerAdvice

  • @ControllerAdvice + @ResponseBodㅛ

2. @ControllerAdvice

  • 여러 ExceptionHandler 메서드를 공유해서 사용

1. @RestControllerAdvie를 사용한 예외 처리 공통화

  • @RestControllerAdvice를 이용한 예외처리를 공통화 처리
  • @RestControllerAdvice 애너테이션을 추가하면 여러 Controller클래스에서
    (@ExceptionHandler, @InitBinder, @ModelAttribute)가 추가된 메서드를 공유해서 사용가능
    [@InitBinder, @ModelAttribute는 JSP, Thymeleaf와 같은 SSR에서 주로 사용]
  • 기존 MethodArgumentNoValidException, ConstraintViolationException과 같은 예외처리를 이곳에 모두 정의
  • 각 Controller마다 정의한 Handler메서드를 이 애너테이션이 붙은 클래스하나로 통합
 [예제 Code]
 @RestControllerAdvice
 public class GlobalExceptionAdvice {
		// (1)
     @ExceptionHandler
     public ResponseEntity handleMethodArgumentNotValidException(
             MethodArgumentNotValidException e) {
         final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
         List<ErrorResponse.FieldError> errors = .......
 
         return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
     }

		// (2)
     @ExceptionHandler
     public ResponseEntity handleConstraintViolationException(
             ConstraintViolationException e) {
         // TODO should implement for validation
         ......
         return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
     }
 } 

2. ConstraintViolationException 처리를 위한 response클래스 수정

  • of(메서드) : 주로, 객체 생성시 어떤 값들의(of~)객체를 생성한다는 의미로 자주 사용하는 네이밍 컨벤션이다.

  • 예제로 코드들 분석

    1). ErrorResponse 클래스 정의 , ErrorCode의 필요한 부분만을 반환하기 위한 클래스

    [예제 Code]
    
    @Getter
    public class ErrorResponse {
       // (1) : 메서드 유효성 검사에러(MethodArgumentNotValidException )의 에러 정보를 담는 매개변수
       private List<FieldError> fieldErrors;
    
       // (2) : URI변수 유효성에러( ConstraintViolationException)의 에러 정보를 담는 매개변수
       private List<ConstraintViolationError> violationErrors;
      	
       // (3) : private 접근제어자로 생성자를 만들어, new를 통한 객체 생성 금지
       private ErrorResponse(final List<FieldError> fieldErrors,
                             final List<ConstraintViolationError> violationErrors) {
       
           this.fieldErrors = fieldErrors;
           this.violationErrors = violationErrors;
       }
    
       // (4) : [MethodArgumentNotValidException]의 에러정보를 [BindingResult]객체로 받아 ErrorResponse 객체 생성
       public static ErrorResponse of(BindingResult bindingResult) {
           return new ErrorResponse(FieldError.of(bindingResult), null);
       }
    
      // (5) : [Set<ConstraintViolation<?>>] 객체에 대한 ErrorResponse 객체 생성
       public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
           return new ErrorResponse(null, ConstraintViolationError.of(violations));
       }
    
       // (6) 메서드 유효성 검증의 에러 정보 생성
       @Getter
       public static class FieldError {
           private String field;
           private Object rejectedValue;
           private String reason;
    
    					private FieldError(String field, Object rejectedValue, String reason) {
               this.field = field;
               this.rejectedValue = rejectedValue;
               this.reason = reason;
           }
    
           public static List<FieldError> of(BindingResult bindingResult) {
               final List<org.springframework.validation.FieldError> fieldErrors =
                                                           bindingResult.getFieldErrors();
               return fieldErrors.stream()
                       .map(error -> new FieldError(
                               error.getField(),
                               error.getRejectedValue() == null ?
                                               "" : error.getRejectedValue().toString(),
                               error.getDefaultMessage()))
                       .collect(Collectors.toList());
           }
       }
    
      // (7) : ConstraintViolation Error 정보 생성
       @Getter
       public static class ConstraintViolationError {
           private String propertyPath;
           private Object rejectedValue;
           private String reason;
    
    					private ConstraintViolationError(String propertyPath, Object rejectedValue,
                                      String reason) {
               this.propertyPath = propertyPath;
               this.rejectedValue = rejectedValue;
               this.reason = reason;
           }
    
           public static List<ConstraintViolationError> of(
                   Set<ConstraintViolation<?>> constraintViolations) {
               return constraintViolations.stream()
                       .map(constraintViolation -> new ConstraintViolationError(
                               constraintViolation.getPropertyPath().toString(),
                               constraintViolation.getInvalidValue().toString(),
                               constraintViolation.getMessage()
                       )).collect(Collectors.toList());
           }
       }
    }

    2). @RestControllerAdvice를 통해 생성한 클래스로, Controller에서 발생한 에러들을 공통화하여 처리할 수 있는 클래스

    • 예외 에러코드를 받아 해당 Handler메서드를 통해 ErrorResponse클래스로 전달하고 필요한 에러정보를 가지고있는
      ErrorResponse 객체를 받아 이를 리턴함.
    [예제 Code]
    @RestControllerAdvice
    public class GlobalExceptionAdvice {
        @ExceptionHandler
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public ErrorResponse handleMethodArgumentNotValidException(
                MethodArgumentNotValidException e) {
            final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
    
            return response;
        }
    
        @ExceptionHandler
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public ErrorResponse handleConstraintViolationException(
                ConstraintViolationException e) {
            final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
    
            return response;
        }
    }

[Extra]

1). Controller Advice
 - https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-controller-advice
 - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RestControllerAdvice.html
 - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ControllerAdvice.html



3. 피드백 😮

  • API계층에서의 Controller 레벨에서의 예외가 발생할 시, 처리하는 예외처리를 학습하였다. 기본적으로 @ExceptionHandler를 이용하여 에러를 처리할 핸들러메서드를 정의해주고 ErrorResponse클래스를 따로 정의하여 이 에러를 받아 원하는 정보 만을 얻을 수 있다.

  • @RestControllerAdvice를 이용하여 각 클래스에 예외처리를 해줘야하는 것을 이 애너테이션이 붙은 클래스로 모든 클래스의 예외를 처리해 줄 수 있다.

  • @RestControllerAdvice가 붙은 클래스 안에 각 에러에 따른 예외처리를 각 Handler메서드를 정의해주고 이에 ErrorResponse 클래스의 객체를 이용하여 필요한 정보만을 받는다.

  • ErrorResponse클래스를 정의해주지 않고 따로 정의해 줄 수 있지만, 이를 사용해야 코드의 중복을 막고 코드를 간결하게 하고, 에러메시지를 원하는대로 컨트롤할 수 있다.

4. 앞으로 해야 될 것 😮

  • 매일 꾸준히 할 것
    • 꾸준히 velog 작성
    • Java 언어 및 Algorithm 공부(Coding-Test)
    • 틈틈히 운동 하기

  • 내일 해야 할 것
    • Business 레벨에서의 예외처리
    • Custom Exception을 통한 예외처리 작성
profile
Will be great Backend-developer

0개의 댓글