전역 예외 처리

1. 전역 예외 처리

 

Spring은 전역적으로 ExceptionHandler를 적용할 수 있는 @ControllerAdvice와 @RestControllerAdvice annotation을 제공

 

2. ControllerAdvice

@ControllerAdivce는 여러 컨트롤러에 대해 전역적으로 ExceptionHandler를 적용

 

에러를 핸들링하는 클래스를 만들어 어노테이션을 붙여주면 에러 처리를 위임하게 됨

 

만약 특정 클래스에만 제한적으로 적용하고 싶다면, @RestControllerAdvice의 basePackages 등을 설정함으로써 제한이 가능

 

@RestControllerAdvice는 @ControllerAdvice와 달리 @ResponseBody가 붙어 있어 응답을 Json으로 반환

 

2.1 ControllerAdvice 장점

 

하나의 클래스로 모든 컨트롤러에 대해 전역적으로 예외 처리가 가능

 

직접 정의한 에러 응답을 일관성 있게 클라이언트에게 반환 가능

 

별도의 try-catch문이 없어 코드의 가독성이 높아짐

 

2.2 ControllerAdvice 단점

 

한 프로젝트당 하나의 ControllerAdivce만 관리하는 것이 좋음

 

만약 여러 ControllerAdvice가 필요하다면 basePackages나 annotations 등을 지정해야 함

 

직접 구현한 Exception 클래스들은 한 공간에서 관리

 

3. ResponseEntityExceptionHandler 추상 클래스

 

Spring은 스프링 예외를 미리 처리해 둔 ResponseEntityExceptionHandler를 추상 클래스로 제공

 

ResponseEntityExceptionHandler에는 스프링 예외에 대한 ExceptionHandler가 모두 구현되어 있으므로 @ControllerAdvice 클래스가 이를 상속해서 사용하면 됨

 

이 추상 클래스를 상속받지 않는다면 스프링 예외들은 DefaultHandlerExceptionResolver가 처리하게 되는데, 그러면 예외 처리기가 달라지므로 클라이언트가 일관되지 못한 에러 응답을 받지 못하므로 ResponseEntityExceptionHandler를 상속시키는 것이 좋음

 

@RestControllerAdvice
public class ExceptionAdvice extends ResponseEntityExceptionHandler {

		// NotFoundAccountException 발생시 에러 처리
    @ExceptionHandler(NotFoundAccountException.class)
    public ResponseEntity<?> handleNotFoundEntity(NotFoundException e) {
        return handleExceptionInternal(e.getExceptionCode());
    }
}

 

3.1 handleExceptionInternal()

 

ResponseEntityExceptionHandler의 handleExceptionInternal() 메소드를 오버라이딩하여 응답을 customizing 가능

 

4. 예외 처리 흐름

 

  1. ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
    1. 예외가 발생한 컨트롤러 안에 적합한 @ExceptionHandler가 있는지 검사
    2. 컨트롤러의 @ExceptionHandler에서 처리가 가능하다면 처리하고, 그렇지 않으면 ControllerAdivce로 넘어감
    3. ControllerAdvice 안에 적합한 @ExceptionHandler가 있는지 검사하고, 없으면 다음 처리기로 넘어감
  2. ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
    1. @ResponseStatus가 있는지 또는 ResponseStatusException인지 검사
    2. 맞으면 ServletResponse의 sendError()로 예외를 서블릿까지 전달하고, 서블릿이 BasicErrorController로 요청을 전달
  3. DefaultHandlerExceptionResolver: 스프링 내부의 기본 예외들을 처리
    1. Spring의 내부 예외인지 검사하여, 맞으면 에러를 처리하고 아니면 넘어감
  4. 적합한 ExceptionResolver가 없으므로 예외가 서블릿까지 전달되고, 서블릿은 SpringBoot가 진행한 자동 설정에 맞게 BasicErrorController로 요청을 다시 전달

 

5. 적용 예시

 

@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value = {GlobalException.class})
    protected ResponseEntity<ErrorResponse> handleGlobalException(GlobalException e) {
        log.error("{}: {}", e.getGlobalErrorCode(), e.getMessage());
        return handleExceptionInternal(e.getGlobalErrorCode());
    }

    private ResponseEntity<ErrorResponse> handleExceptionInternal(GlobalErrorCode globalErrorCode) {
        return ResponseEntity.status(globalErrorCode.getHttpStatus().value())
                .body(new ErrorResponse(globalErrorCode));
    }
}
@Getter
public class ErrorResponse {

    private final LocalDateTime timestamp = LocalDateTime.now();
    private final Boolean isSuccess;
    private final String code;
    private final String message;

    public ErrorResponse(GlobalErrorCode globalErrorCode) {
        this.isSuccess = false;
        this.code = globalErrorCode.getCode();
        this.message = globalErrorCode.getMessage();
    }
}