Spring & Spring boot

[Spring Boot] 예외 처리 - 1. ExceptionHandler 사용하기

jh4dev 2024. 8. 15. 19:13
<목차>

1. @ExceptionHandler 
2. @RestControllerAdvice / @ControllerAdvice 사용하여 처리하기
3. 컨트롤러에서 처리하기

 

REST API 를 제공하는 애플리케이션 입장에서는, 요청값을 처리하던 중 예외가 발생하면 예외를 복구하여 정상적으로 처리하기 보다는, 요청을 보낸 클라이언트에게 어떤 문제가 발생하였는지를 전달해주는 경우가 많다.

 

따라서, 각 레이어에서 예외가 발생했을 경우, 발생한 예외를 엔드포인트 레벨인 컨트롤러로 전달해야 하며, 컨트롤러에서 클라이언트로 오류 메시지를 전달하는 방법에 대해 알아보자.

 

Exception 에 대한 내용은, 아래 포스팅을 참고.

 

[JAVA] 예외처리 - Exception 이해하기

1. Error & Exception2. Exception Class 구조3. Exception 처리 방법 [Error & Exception]Error / 에러JAVA 에서 Error 는 JVM에서 발생시키는 것으로, 애플리케이션 코드에서 처리할 수 있는 부분이 거의 없다.대표적으

jh4dev.tistory.com

 


[@ExceptionHandler]

스프링 프레임워크에서 제공하는 어노테이션으로, 특정 예외가 발생했을 때 그 예외를 처리하는 메서드를 지정하는 데 사용한다.
주로 컨트롤러나 @ControllerAdvice 클래스 내에서 사용되며, 발생한 예외를 잡아 적절한 응답을 반환할 수 있도록 해준다.

  1. 특정 예외 처리: @ExceptionHandler는 특정 예외 클래스를 인자로 받아, 해당 예외가 발생했을 때 실행될 메서드를 정의하며, 여러 예외를 한 메서드에서 처리할 수도 있다.

  2. 컨트롤러 범위: 이 어노테이션 해당 컨트롤러 클래스에서 발생하는 예외에만 적용된다. 하지만, @ControllerAdvice와 함께 사용하면 애플리케이션 전체의 예외를 전역적으로 처리할 수 있다.

  3. 맞춤형 응답: @ExceptionHandler를 사용하여 발생한 예외에 대해 사용자 정의 응답을 생성할 수 있다. 예를 들어, JSON 형식의 에러 메시지를 반환하거나 특정 HTTP 상태 코드를 설정할 수 있다.

  4. 예외의 계층 구조 처리: 예외의 계층 구조에 따라 더 구체적인 예외부터 처리되며, 만약 특정 예외에 대한 핸들러가 없다면, 부모 클래스의 예외 핸들러가 호출될 수 있다.

  5. 간편한 예외 관리: @ExceptionHandler를 사용하면 예외 처리 로직을 분리하여 컨트롤러 코드의 가독성을 높이고, 다양한 예외 상황에 맞춰 일관된 에러 응답을 제공할 수 있다.

 

[@ControllerAdvice/ @RestControllerAdvice 사용하여 처리하기]

REST API 를 제공하는 서비스라는 전제 하에 @RestControllerAdvice 기준으로 작성하였다.

 

<CustomExceptionHandler> @RestControllerAdvice 어노테이션

    //@RestcontrollerAdvice(basePackages = "특정 패키지 지정")
    @RestControllerAdvice
    @Slf4j
    public class CustomExceptionHandler {

        @ExceptionHandler(value = RuntimeException.class)
        public ResponseEntity<Map<String, String>> handleException(RuntimeException re, HttpServletRequest request){

            HttpHeaders responseHeaders = new HttpHeaders();
            HttpStatus httpStatus = HttpStatus.BAD_REQUEST;

            log.error("Advice 내 handleException() 호출, {}, {}", request.getRequestURI(), re.getMessage());

            Map<String, String> responseMap = new HashMap<>();
            responseMap.put("error_type", httpStatus.getReasonPhrase());
            responseMap.put("code", "400");
            responseMap.put("message", re.getMessage());

            return ResponseEntity.status(httpStatus).body(responseMap);
        }
    }
  • @RestControllerAdvice : 결과 값을 JSON 형태로 반환한다. @Controller 어노테이션이 적용된 컨트롤러도 처리할 수 는 있으나, View 기반 응답을 반환하는 @Controller 에는 부적합할 수 있다.
  • @ExceptionHandler : @Controller 또는 @RestController 가 적용된 빈에서 발생하는 예외를 Catch 하여 처리하는 메서드를 정의할 때 사용한다.
    value 속성을 통해, 어떤 Exception 클래스를 처리할 지 지정할 수 있으며, 배열 형태로도 지정할 수 있다.

<ExceptionController> 테스트 컨트롤러 생성

    @RestController("/exception")
    @Slf4j
    public class ExceptionController {

        @GetMapping("/basic")
        public String basicExceptionController() throws RuntimeException {
            throw new RuntimeException("기본 Exception Handler 테스트");
        }
    }

 

  • 위에서 작성한 CustomExceptionHandler 에서 RuntimeException 을 처리하게 하였으므로, 강제로 RuntimeException 을 발생시키는 컨트롤러를 구성하여 테스트 진행.

테스트 결과

테스트 결과, 아래와 같은 JSON 형태의 Response 가 반환된 것을 확인할 수 있다.

{
  "code": "400",
  "error_type": "Bad Request",
  "message": "기본 Exception Handler 테스트"
}

 


[컨트롤러에서 처리하기]

컨트롤러에서 처리하는 방법은 간단하다.

필요로 하는 컨트롤러 내에서, @ExceptionHandler 어노테이션으로 처리할 예외를 작성해주면 된다.

 

위에서 작성한 내용을 사용하자면,

CustomExceptionHandler의, handleException() 을 컨트롤러로 옮겨주면 된다.

<ExceptionController>

    @RestController("/exception")
    @Slf4j
    public class ExceptionController {

        @ExceptionHandler(value = RuntimeException.class)
        public ResponseEntity<Map<String, String>> handleException(RuntimeException re, HttpServletRequest request){

            HttpHeaders responseHeaders = new HttpHeaders();
            HttpStatus httpStatus = HttpStatus.BAD_REQUEST;

            log.error("Controller 내 handleException() 호출, {}, {}", request.getRequestURI(), re.getMessage());

            Map<String, String> responseMap = new HashMap<>();
            responseMap.put("error_type", httpStatus.getReasonPhrase());
            responseMap.put("code", "400");
            responseMap.put("message", re.getMessage());

            return ResponseEntity.status(httpStatus).body(responseMap);
        }

        @GetMapping("/basic")
        public String basicExceptionController() throws RuntimeException {
            throw new RuntimeException("기본 Exception Handler 테스트");
        }
    }

 

다만, 이미 @RestControllerAdvice 를 통해 작성된 Handler 가 있다면 다음과 같은 규칙으로 우선순위가 지정되어 핸들러가 호출된다.

 

  1. 예외 타입의 레벨에 따른 우선순위
    • @ExceptionHandler(Exception.class)@ExceptionHandler(RuntimeException.class) 두 핸들러가 존재한다면, 보다 구체적인 @ExceptionHandler(RuntimeException.class) 가 우선순위를 갖는다.
  • 핸들러 위치에 따른 예외 처리 우선 순위
    • 지금과 같이, @RestControllerAdvice 와 같은 글로벌 예외처리와, @Controller 내 의 컨트롤러 예외처리가 존재한다면, 범위가 좁은 컨트롤러의 핸들러가 우선순위를 갖는다.

 

테스트를 진행하여 우선순위를 확인해보자.

CustomExceptionHandler 에 작성된 핸들러 메서드와 ExceptionController 에 작성된 핸들러 메서드는 로그 적재 부분을 다르게 하였다.

<console>

[ERROR] [http-nio-8080-exec-6] com.springboot.valid.controller.ExceptionController Controller 내 handleException() 호출, /basic, 기본 Exception Handler 테스트

 

위와 같이 Controller 내 작성된 핸들러 메서드가 호출된 것을 확인할 수 있다.