스프링

예외처리

영범 2024. 8. 12. 18:10

프로젝트에서 오류를 관리하는 것은 서버를 유지/보수하고 사용자 경험에 좋은 영향을 미치기 위해 아주 중요한 부분입니다.

이 글에서는 스프링 프레임워크에서 발생한 예외를 처리하는 방법에 대해서 공부한 내용을 기록하겠습니다.

 

가장 핵심포인트는 애플리케이션 내에서 발생한 에러를 잡아서 이 예외를 처리한 후, 사용자에게 원하는 에러코드와 메세지로 응답을 해주어야 한다는 것 입니다.

 

@ExceptionHandler

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {
	//...
    
    @ExceptionHandler({ CustomException.class })
    public void handleException() {
    	//...
    }
}

 

컨트롤러에서 @ExceptionHandler를 사용해 특정예외를 포착해 처리합니다.

이 방법은 해당 컨트롤러에서 발생하는 예외에 대해서 세밀하게 처리를 할 수 있다는 장점이 있습니다.

 

하지만, 여러 컨트롤러에서 찾은 예외를 처리해야하는 경우 코드 중복이 발생할 수 있고, 프로젝트가 커짐에 따라서 예외처리를 관리하기 어려워 프로젝트 전역에서 일관된 예외처리를 유지하기 어려울 수 있습니다.


HandlerExceptionResolver

전역으로 에러를 처리하는 방법중에 하나는 HandlerExceptionResolver 인터페이스를 활용하는 하는 것 입니다.

기본적으로 스프링 프레임워크에서 몇 가지 기본 구현체를 제공합니다.

 

ExceptionHandlerExceptionResolver

Spring 3.1에서 도입된 리졸버로 DispatcherServlet에서 활성화됩니다.

@ExceptionHandler 어노테이션과 함께 동작하며 특정예외를 처리합니다.

 

ResponseStatusExceptionResolver

Spring 3.0에서 도입되어서 기본적으로 활성화된 리졸버입니다.

Spring 예외를 HTTP 상태코드 (3xx, 4xx, 5xx)로 매핑합니다.

단, 상태 코드는 적절하게 설정되지만 응답 본문이 비어있어서 에러에 대한 충분한 내용을 전달하지 못한다는 점이 있습니다.

 

DefaultHandlerExceptionResolver

Spring 3.0에서 도입되어서 기본적으로 활성화된 리졸버입니다.

@ResponseStatus 어노테이션을 사용해서 사용자 정의 예외를 HTTP 상태 코드로 매핑합니다.

마찬가지로 본문이 비어있어서 에러에 대한 충분한 내용을 전달하지 못한다는 점이 있습니다.

 

커스텀 HandlerExceptionResolver 구현

위의 HandlerExceptionResolver 구현체 외에도 원하는 커스텀 리졸버를 구현할 수 있습니다.

@Component
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {

    @Override
    protected ModelAndView doResolveException(
      HttpServletRequest request, 
      HttpServletResponse response, 
      Object handler, 
      Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                return handleIllegalArgument(
                  (IllegalArgumentException) ex, response, handler);
            }
            ...
        } catch (Exception handlerException) {
            logger.warn("Handling of [" + ex.getClass().getName() + "] 
              resulted in Exception", handlerException);
        }
        return null;
    }

    private ModelAndView 
      handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response) 
      throws IOException {
        response.sendError(HttpServletResponse.SC_CONFLICT);
        String accept = request.getHeader(HttpHeaders.ACCEPT);
        ...
        return new ModelAndView();
    }
}

 

동작원리

 

  • 요청처리중 예외가 발생하면, Spring의 DispatcherServlet은 이 예외를 처리하기 위해 등록된 여러 HandlerExceptionResolver를 순차적으로 호출합니다.
    • ExceptionHandlerExceptionResolver는 컨트롤러 내의 @ExceptionHandler 메서드를 찾아 예외를 처리합니다.
    • ResponseStatusExceptionResolver는 @ResponseStatus 어노테이션이 있는 예외를 처리합니다.
    • DefaultHandlerExceptionResolver는 표준 Spring 예외를 HTTP 상태 코드로 매핑합니다.
    • 필요에 따라 커스텀 HandlerExceptionResolver를 추가해 예외 처리를 확장할 수 있습니다.
  • 최종적으로 HandlerExceptionResolver가 에러 응답을 생성해 클라이언트에게 전달합니다.

 


@ControllerAdvice

@ControllerAdvice는 애플리케이션 전체에서 발생할 수 있는 예외를 중앙 집중식으로 처리할 수 있게 해줍니다.

@ControllerAdvice를 선언한 클래스에서 @ExceptionHandler를 정의하면 모든 컨트롤러에서 발생하는 예외에 대해서 처리를 할 수 있습니다.

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<String> handleRuntimeException(RuntimeException e) {
        return new ResponseEntity<>("Handled globally: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(NullPointerException.class)
    public ResponseEntity<String> handleNullPointerException(NullPointerException e) {
        return new ResponseEntity<>("Handled NullPointerException: " + e.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

 


참고

https://www.baeldung.com/exception-handling-for-rest-with-spring

https://conanmoon.medium.com/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%9D%BC%EA%B8%B0-exceptionhandler-e644bf0100c6

https://dev.to/tienbku/global-exception-handler-in-spring-boot-3mbp

https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc