Web/Spring

Spring MVC 2 - API 예외 처리 @ExceptionResolver

now0204 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})

 

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