본문 바로가기
BackEnd/Spring

@ControllerAdvice와 @ExceptionHandler

by 규난 2023. 1. 8.
728x90

이번 포스트에서는 @ExceptionHandler와 @ControllerAdvice에 대해서 알아보도록 하겠습니다.

 

@ExceptionHandler

Controller 내 메소드에 정의되어 해당 컨트롤러에서 발생하는 예외를 받아 처리합니다.

package com.example.oauth.config.controller;

import com.example.oauth.config.common.exception.GlobalErrorResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;

@Slf4j
@RestController
@RequestMapping("/api/test2")
public class TestController2 {

    @GetMapping("/illegalStateException")
    public void getIllegalStateExceptionTest() {
        throw new IllegalStateException("TestController2 getIllegalStateExceptionTest");
    }

    @ExceptionHandler(IllegalStateException.class)
    public ResponseEntity<?> handlerIllegalStateException(Exception e) {
        log.error("handlerIllegalStateException is occurred.", e);
        GlobalErrorResult result = GlobalErrorResult.of(e.getMessage());
        return new ResponseEntity<>(result, INTERNAL_SERVER_ERROR);
    }
}

위의 예제를 보면 /api/test2/illegalStateException으로 요청이 오게 되면 강제로 IllegalStateException 예외를 터트리게 했고

그 결과 TestController2 내에 있는 @ExceptionHandler가 정의된 handlerIllegalStateException 메소드가 받아서 예외를 처리한 것을 볼 수 있습니다.

@ExceptionHandler는 정의된 Controller 내에서만 동작하기 때문에 동일한 예외인데도 Controller마다 @ExceptionHandler를 가진 메소드를 정의해 줘야 하는 번거로움이 있습니다.

 

이러한 번거로움을 해결해 줄 수 있는 방법이 있는데 그것이 바로 @ControllerAdvice입니다.

 

@ControllerAdvice

모든 Controller 전역에서 발생하는 예외를 처리해주는 어노테이션입니다.

 

여러 곳에서 공통적인 일을 처리하는 중복된 @ExceptionHandler, @InitBinder, @ModelAttribute를 @ControllerAdvice 한 곳에 정의해두고 필요할 때 사용할 수 있게 만들어줍니다.

// GlobalExceptionHandler
package com.example.oauth.config.common.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handlerException(Exception e) {
        log.error("Exception is occurred.", e);
        GlobalErrorResult result = GlobalErrorResult.of(e.getMessage());
        return new ResponseEntity<>(result, INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<?> handlerRuntimeException(Exception e) {
        log.error("handlerRuntimeException is occurred.", e);
        GlobalErrorResult result = GlobalErrorResult.of(e.getMessage());
        return new ResponseEntity<>(result, INTERNAL_SERVER_ERROR);
    }
}

// TestController
package com.example.oauth.config.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/api/test")
public class TestController {

    @GetMapping("/runtimeException")
    public void getRuntimeExceptionTest() {
        throw new RuntimeException("TestController getRuntimeExceptionTest");
    }

    @GetMapping("/illegalStateException")
    public void getIllegalStateExceptionTest() {
        throw new IllegalStateException("TestController getIllegalStateExceptionTest");
    }
}

/api/test/reuntimeException, /api/test/illegalStateException으로 요청을 보내게 되면

RuntimeException과 llegalStateException 예외가 터지도록 구현했고 RuntimeException은 GlobalExceptionHandler 안에 handlerRuntimeException 메소드에서 처리하게 되며 IllegalStateException도 TestController2 안에 있는 handlerIllegalStateException 메소드에서 예외를 처리하는 게 아니라 GlobalExceptionHandler 안에 handlerRuntimeException 메소드에서 처리하게 됩니다. IlleagalStateException 예외가 터지는데 RuntimeException을 처리하는 handlerRuntimeException 메소드가 동작하는 이유는 해당 Exception을 처리하는 @ExceptionHandler가 없는 경우 상위(부모) 클래스인 Exception을 처리하는 @ExceptionHandler가 있는지 확인하고 있으면 해당 메소드에서 처리가 가능하기 때문입니다. 이런 구조로 동작하기 때문에 IlleagalStateException 예외가 터져있을 때 RuntimeException을 처리하는 handlerRuntimeException 메소드가 동작할 수 있게 되는겁니다.

 

@ControllerAdvice와 @ExceptionHandler가 실행되는 구조

요청이 들어오게 되면 DispatcherServlet에서 doDispatch를 실행하다가 예외가 발생하게 되면 @ControllerAdvice나 @RestControllerAdvice가 정의된 빈 이 주입된 HandlerExceptionResolver가 실행되는 구조입니다.

 

여기서 HandlerExceptionResolver란 Controller 작업 중 발생한 예외를 어떻게 처리할지 결정하는 전략입니다.

728x90