ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring MVC 2 - API 예외 처리 @ExceptionResolver
    Web/Spring 2023. 11. 22. 00:39

     

    - 스프링 부트가 기본으로 제공하는 ExceptionResolver는 다음과 같다.

     

    1. ExceptionHandlerExceptionResolver : @ExceptionHandler 처리 (API 예외 처리 대부분 이 기능 사용)

    2. ResponseStatusExceptionResolver : HTTP 상태 코드 지정해줌 (@ResponseStatus)

    3. DefaultHandlerExceptionResolver : 스프링 내부 기본 예외를 처리 

     

    *  HandlerExceptionResolverComposite 에 위 순서로 등록됨 -> null반환하면 계속 다음 것 찾음 -> 없으면 걍 500일 듯(WAS)

     

     

    1. ReponseStatusExceptionResolver 

     

    - 예외에 따라 HTTP 상태 코드를 지정해주는 역할을 수행  (HandlerExceptionResolver에서 response.sendError 편의 기능)

    - 두 가지 경우 위 Resolver가 호출되어 처리 : 1. @ResponseStatus가 달려있는 예외 , 2 ResponseStatusException 예외

                                                                                (사용자 정의 예외에 적용 어노테이션)

     

    1.1 @ResponseStatus

    @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
    public class BadRequestException extends RuntimeException{
    
    }

     

    - 위 Exception이 컨트롤러 밖으로 나가면, ResponseStatusExceptionResolver가 

       던저진 예외의 어노테이션 확인 -> 오류 코드 변경, 메시지 담기 해준다.

    -  결국 response.sendError()해주는 것과 같다. ( HandlerExceptionResolver에서도 해봤음)

     

    *reason은 MessageSource 기능도 제공한다.

     

    1.2 ResponseStatusException 

     

    - @ResponseStatus는 사용자가 변경 불가능한 예외에 대해서는 적용이 안된다.

    - 예외에 위 어노테이션을 넣어야하는데.. 라이브러리 코드 등에서는 적용하기 힘듦)

    - 또한 동적 변경도 어렵다 

    - 이럴때 ResponseStatusException을 사용하자

     

    @GetMapping("/api/response-status-ex2")
    public String responseStatusEx2() {
     throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new
    IllegalArgumentException());
    }

     

    - 에러가 발생하면 한번 wrapping해서 다시 던지는 방식 

     

     

    2. API 예외 처리 - 스프링 기본 제공 (DefaultHandlerExceptionResolver)

     

    - DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결한다.

    - 대표적으로 파라미터 바인딩 시점에 타입 오류 (TypeMismatchException) 

      (ArgumentResolver -> TypeConverter 시점에서 발생하는 오류 등)

    - 원래는 예외 발생 500 Error이지만, 바인딩 오류는 클라이언트 측 오류임 -> 400으로 바꿔준다.

    - DefaultHandlerExceptionResolver.handleTypeMismatch를 보면 -> response.sendError()하는 것을 볼 수 있다.

     

     

    * 기본으로 제공되는 2가지는 상태코드 변경하는 정도의 기능만 제공해주었다. 

       다른 처리를 위해 HandlerExceptionResolver를 사용할 수도 있지만, 매우 복잡하다.

       스프링은 이 문제를 해결하기 위해 @ExceptionHandler를 제공해준다.

     

    3. API 예외 처리 - @ExceptionHandler + ExceptionHandlerExceptionResolver

     

    - HTML 화면 오류 

      웹 브라우저에 HTML 화면을 제공하고 싶으면, BasicErrorController를 사용

    - API 오류

      ExceptionHandlerExceptionResolver를 활용

     

     

    -> 기존의 BasicErrorController + HandlerExceptionResolver 조합 : ModelAndView를 반환해야함 -> API 응답에 필요x

                                                                                                          response를 직접 사용 -> 매우 불편 + 거의 서블릿 

     

    -> 또한 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어렵다. (위에 제공된 건 둘자 글로벌함)

     

     

    *HandlerExceptionResolver에서 예외를 모두 잡기 (헤더확인 ->API 바로응답, HTML -> ModelAndView 활용

      -> 일단 예외가 발생하면 작동 

    *BasicErrorController (헤더가 Application/json (HTML이 아니면) 기본 json에러 만들어서 줌 -> ExceptionResolver에서 예외 상태만 변경한 경우 사용됨)

     -> 예외 어디서 안잡거나, ExceptionResolver에서 response.sendError만 한 경우 Basic작동 

     

    3.1 ExceptionHandler

     

    - 위에 살펴본 두가지 ExceptionResolver의 한계를 @ExceptionHandler로 해결가능하다. 

    - 스프링은 ExceptionHandlerExceptionResolver를 기본으로 제공 우선순위도 높고 대부분 이걸로 처리 가능 

     

    @Data
    @AllArgsConstructor
    public class ErrorResult {
     private String code;
     private String message;
    }

     

    @Slf4j
    @RestController
    public class ApiExceptionV2Controller{
    
    
    
    		@RequestStatus(HttpStatus.BAD_REQUEST)
            @ExceptionHandler(IllegalArgumentException.class)
            public ErrorResult illegalExHandle(IllegalArgumentException e){
            	log.error("exceptionHandle  ex",e);
                return new ErrorResult("BAD",e.getMessage());
            }
            
            @ExceptionHandler
            public ResponseEntity<ErrorResult> userExHandle(UserException e){
            
            	ErrorResult errorResult = new ErrorResult("USER-EX",e.getMessage());
                return new ResponseEntity<>(errorResult,HttpStatus.BAD_REQUEST);
            }
    		
            @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     		@ExceptionHandler
     		public ErrorResult exHandle(Exception e) {
     		log.error("[exceptionHandle] ex", e);
     		return new ErrorResult("EX", "내부 오류");
     		}
     
     		@GetMapping("/api2/members/{id}")
     		public MemberDto getMember(@PathVariable("id") String id) {
     		if (id.equals("ex")) {
     		throw new RuntimeException("잘못된 사용자");
     		}
    		 if (id.equals("bad")) {
     		throw new IllegalArgumentException("잘못된 입력 값");
     		}
             if (id.equals("user-ex")) {
    		 throw new UserException("사용자 오류");
    		 }
     		return new MemberDto(id, "hello " + id);
    		 }
    }

     

    - @ExceptionHandler 어노테이션 선언 -> 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주자

       (컨트롤러 예외 발생시 일단 @ExceptionHandler가 있다면 해결을 시도한다) + 지정 예외 자식 까지 포함

    - 예외 우선순위는 항상 자세한 것이 우선순위가 있다.

    - @ExceptionHandler가 작동하면 예외를 처리했음으로 정상 흐름으로 진행된다 (상태코드 지정 x시 200임)

    - 마치 Exception으로 매핑된 컨트롤러를 다시 호출하는 것으로 생각할 수 있다.

     

    > 다양한 예외 : @ExceptionHandler({})배열로 한번에 처리할 수 있다.

    > 예외 생략 : 파라미터에 정의된 예외를 자동으로 추정해준다.

    > 파라미터 : @ExceptionHandler는 마치 스프링 컨트롤러 처럼 다양한 파라미터를 지정할 수 있다 (컨트롤러보단 적음)

                        *공식메뉴얼 참고 

     

    *일반적인 view 반환도 가능함 -> @RestController가 아닐 시 , 또한 ModelAndView를 반환할 수 있다. 

      String 반환하면 view로 생각하고 처리해줌 물론 ResponseBody를 적어두면 다시 body에 적음 

     

    * ResponseEntity를 사용하면 HTTP 응답 코드를 동적으로 변경할 수 있다. (어노테이션은 동적으로 처리 불가)

     

     

    4. API 예외 처리 - @ControllerAdvice

     

    - @ExceptionHandler로 예외를 깔끔하게 처리할 수 있다. 근데 정상 코드와 예외 코드가 하나에 컨트롤러에 섞여 있다.

    - @ControllerAdvice, @RestControllerAdvice를 사용하면 이를 분리할 수 있다.

     

    @RestControllerAdvice
    pulbic class ExControllerAdvice{
    
    
    	@ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(IllegalArgumentException.class)
     public ErrorResult illegalExHandle(IllegalArgumentException e) {
     log.error("[exceptionHandle] ex", e);
     return new ErrorResult("BAD", e.getMessage());
     }
     @ExceptionHandler
     public ResponseEntity<ErrorResult> userExHandle(UserException e) {
     log.error("[exceptionHandle] ex", e);
     ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
     return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
     }
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     @ExceptionHandler
     public ErrorResult exHandle(Exception e) {
     log.error("[exceptionHandle] ex", e);
     return new ErrorResult("EX", "내부 오류");
     }
    
    }

     

    - @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder기능을 부여해줌

    - 대상을 지정하지 않으면, 모든 컨트롤러에 적용된다.

    - @RestControllerAdvice는 @ControllerAdvice와 같고, @RespnseBody가 추가되어 있다.

     

    4.1 타겟 지정 방법

     

    // Target all Controllers annotated with @RestController
    @ControllerAdvice(annotations = RestController.class)
    
    // Target all Controllers within specific pakages
    @ControllerAdvice("org.example.controllers")
    
    //Target all Controllers assinable to specific classes 
    @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})

     

     - 자세한 사항은 공식문서 확인하자 

Designed by Tistory.