[스프링] 예외 처리

스프링의 예외 처리

애플리케이션에서 코드 실행 도중 예기치 않은 예외가 발생하면 예외 스택 트레이스(stack trace)가 프론트 화면이나 응답 메시지에 그대로 보여진다. 서버는 클라이언트에게 서버에서 발생하는 예외 관련 정보를 그대로 전달하는 것보다 특정 에러 페이지 및 메시지나 HTTP 상태 코드를 응답으로 전달함으로써 서버에서 어떤 이유로 응답 실패가 발생하는지 관련 정보를 제공하는 것이 좋다.

스프링에서는 발생 가능한 예외를 해석(resolve)하여 처리하는 인터페이스 구현체를 애플리케이션 컨텍스트에 등록하거나 관련 어노테이션을 사용함으로써 예외 발생 시 뷰에 에러 관련 정보가 그대로 노출되는 것을 막을 수 있으며(스프링 MVC의 경우) 예외 발생 시 특정 데이터를 응답으로 전송(REST API 서비스인 경우)하는 등의 예외 처리가 가능하다.

스프링은 개발자가 언체크 예외(unchecked exception)를 사용함으로써 예외 계층 구조를 매우 상세하게 지정할 수 있게 해준다. 스프링 버전 3.2 이전에서는 @ExceptionHandler 어노테이션을 사용하거나 애플리케이션 컨텍스트에 예외 리졸버(exception resolver) 빈을 하나 이상 등록하여 코드 내 예외 처리를 통해 붙잡지 않은 언체크 예외를 해석하고 처리할 수 있다. 스프링 버전 3.2는 @ControllerAdvice 어노테이션을 도입하여 이전 두 방법을 통해서는 범용 예외 처리가 불가하였던 한계점을 개선하여 애플리케이션 전반에 걸친 통합 예외 처리 기능을 제공한다.

@ExceptionHandler 어노테이션을 통해 특정 예외를 처리하는 방법은 다음과 같다. 컨트롤러 클래스의 메서드에 @ExceptionHandler를 지정하여 예외 핸들러를 매핑한다. 작동 원리는 @RequestMapping과 비슷하다.

@Controller
public class MyController {
  @ExceptionHandler(예외클래스명.class)
  public String handle(예외클래스 ex) {
    return "예외페이지논리뷰명";
  }
  
  @ExceptionHandler
  public String handleDefault(Exception e) {
    return "예외페이지논리뷰명";
  }
} 


커스텀 예외 클래스를 파라미터로 가지는 메서드를 정의하고 메서드에 @ExceptionHandler 어노테이션을 지정한다. 이때 어노테이션 인자로 해당 예외 클래스를 전달한다. 이렇게 @ExceptionHandler 어노테이션을 사용하여 특정 예외 발생 시 특정 에러 페이지에 해당하는 뷰를 보여줄 수 있다. 직접 붙잡아 처리하지 않은 언체크 예외 발생 시 기본 에러 페이지를 보여주기 위해서는 Exception 예외 클래스를 파라미터로 가지는 메서드를 정의하고 메서드에 @ExceptionHandler 어노테이션을 지정한다.

@ExceptionHandler 메서드는 애플리케이션에 대해 전역적으로 동작하지 않으며 정의된 컨트롤러에 대해서만 동작하기 때문에 다른 컨트롤러에서 예외가 발생하면 해당 메서드가 호출되지 않는다. 해당 메서드를 모든 컨트롤러 클래스에 추가하는 방법이 있지만 발생 가능한 모든 예외를 처리하는 통합 예외 처리 방법으로는 적합하지 않아 보인다.

@ExceptionHandler 어노테이션 사용 시 주의점은 다음과 같다. 메서드 정의 시 @ExceptionHandler의 인자로 전달한 예외는 메서드의 인자로 전달되는 예외와 일치해야 한다. 일치하지 않을 경우 컴파일 시 에러가 발생하지는 않지만 런타임 시에 해당 예외가 실제로 발생하는 경우 예외 처리가 실패하여 IllegalStateException 예외가 발생하게 된다.

붙잡지 않은 예외는 HandlerExceptionResolver 인터페이스를 구현한 커스텀 예외 리졸버로 해석하여 처리할 수도 있다. HandlerExceptionResolver 인터페이스 구현체인 예외 리졸버는 일반적으로 애플리케이션 컨텍스트에 빈으로 등록되며 DispatcherServlet은 빈을 자동으로 감지한다. 보통 예외 종류 별로 각각의 에러 페이지를 매핑한다. 스프링에는 다음과 같이 다양한 HandlerExceptionResolver 인터페이스 구현체가 내장되어 있다.

  • SimpleMappingExceptionResolver
  • DefaultHandlerExceptionResolver
  • ExceptionHandlerExceptionResolver
  • HandlerExceptionResolverComposite
  • ResponseStatusExceptionResolver


SimpleMappingExceptionResolver 구현체를 사용하면 애플리케이션 컨텍스트에서 발생한 예외의 종류 별로 어떤 뷰(에러 페이지)를 보여줄지 하나씩 매핑할 수 있다.

예외 리졸버 빈을 구성 클래스에 다음과 같이 등록한다. 구성 클래스는 WebMvcConfigurer를 구현한 후 configureHandlerExceptionResolvers 메서드를 재정의하여 커스텀 예외 리졸버 빈을 HandlerExceptionResolver 리스트에 추가한다.

@Configuration
public class ExceptionHandlerConfiguration implements WebMvcConfigurer {
  @Override
  public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
    exceptionResolvers.add(handlerExceptionResolver());
  }

  @Bean
  public HandlerExceptionResolver handlerExceptionResolver() {
    Properties exceptionMapping = new Properties();
    exceptionMapping.setProperty(예외클래스명.class.getName(), "예외페이지논리뷰명");
    SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
    exceptionResolver.setExceptionMappings(exceptionMapping);
    exceptionResolver.setDefaultErrorView("예외페이지논리뷰명");
    return exceptionResolver;
  }
}


Properties 객체를 사용하여 커스텀 예외 클래스를 논리 뷰에 매핑한 후 예외 리졸버의 exceptionMappings 프로퍼티를 Properties 객체로 지정함으로써 해당 예외 클래스에 따른 뷰를 매핑힌다. 예외 리졸버의 defaultErrorView 프로퍼티는 exceptionMapping 프로퍼티에 매핑되지 않은 예외가 발생하면 표시할 기본 뷰 이름이다. 코드 내에서 직접 붙잡아 처리하지 않은 언체크 예외 발생 시 기본 에러 페이지를 보여주기 위해 defaultErrorView 프로퍼티를 통해 기본 뷰를 지정하거나 exceptionMappings 프로퍼티에 모든 예외의 최상위 클래스인 java.lang.Exception 예외를 프로퍼티 키로하여 에러 페이지를 매핑한다. 이와 같이 SimpleMappingExceptionResolver를 사용하면 발생 가능한 모든 예외에 대한 처리가 가능하다.

DefaultHandlerExceptionResolverResponseStatusExceptionResolver구현체는 스프링 버전 3.0에 도입되었으며 DispatcherServlet에서 기본적으로 활성화되어 있다. DefaultHandlerExceptionResolver 구현체는 스프링 예외를 적절한 HTTP 상태 코드로 해석한다. HTTP 상태 코드에는 리디렉션 에러인 3xx 에러, 클라이언트 에러인 4xx 에러, 서버 에러인 5xx 에러 등이 있다. 각각의 HTTP 상태 코드에 대한 스프링 예외가 정의되어 있다. DefaultHandlerExceptionResolver 구현체는 발생 가능한 예외에 대해 특정 HTTP 상태 코드를 적절하게 매핑하여 응답으로 전달하지만 HTTP 응답 메시지의 본문(바디)을 구성하지는 않는다. REST API 서비스의 경우 예외를 HTTP 상태 코드로만 매핑하여 응답하면 서버의 실패에 대한 추가적인 정보를 응답 메시지로 전달할 수 없다. 클라이언트는 HTTP 상태 코드만으로는 서버의 실패 원인을 파악할 수 없다.

ResponseStatusExceptionResolver 구현체를 사용하면 커스텀 예외에 @ResponseStatus 어노테이션을 지정할 수 있으며 해당 예외를 HTTP 상태 코드에 매핑할 수 있다. 다음과 같이 커스텀 예외를 정의하고 해당 예외 발생 시 클라이언트에게 전달할 응답 메시지에 HTTP 상태 코드를 매핑하여 지정할 수 있다.

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class CustomNotFoundException extends RuntimeException {
  public CustomNotFoundException() {
    super();
  }
    
  public CustomNotFoundException(String message, Throwable cause) {
    super(message, cause);
  }
    
  public CustomNotFoundException(String message) {
    super(message);
  }
    
  public CustomNotFoundException(Throwable cause) {
    super(cause);
  }
}


ResponseStatusExceptionResolver 구현체도 DefaultHandlerExceptionResolver 구현체와 마찬가지로 HTTP 응답 메시지의 본문을 구성하지는 않는다.

예외 발생 시 HTTP 상태 코드 뿐만 아니라 추가적으로 HTTP 응답 메시지의 본문을 구성하여 클라이언트에게 예외 발생에 대한 정보를 제공하지 못하는 한계를 해결하기 위해 HandlerExceptionResolver 인터페이스를 직접 구현할 수도 있다. 이 방법은 AbstractHandlerExceptionResolver를 상속하여 doResolveException() 메서드 재정의 시 발생 가능한 예외 인스턴스의 타입을 판별한 후 클라이언트의 HTTP 요청 헤더의 Accept 값에 따라 적절한 형태의 콘텐츠로 응답 메시지를 구성한다. 이 때 ModelAndView 객체를 반환하여 응답 메시지의 본문을 구성한다. 그러나 이 방법은 HtttpServletResponse 객체를 사용하고 ModelAndView 객체를 사용하므로 MVC 모델에서만 구현 가능하다는 한계가 있다.

스프링 버전 3.2부터 제공되는 @ControllerAdvice 어노테이션을 사용하면 통합 예외 처리가 가능하다. 이 방법은 MVC 모델에서 벗어나 @ExceptionHandler의 타입 안전성과 유연성을 제공하며 REST API 서비스 구현을 위한 ResponseEntity 객체를 사용할 수 있게 해준다. 기존 @ExceptionHandler 어노테이션은 핸들러 메서드에 지정하여 해당 컨트롤러에 대해서만 적용할 수 있었지만 @ControllerAdvice 어노테이션을 사용하면 컨트롤러 클래스 별로 분산된 예외 처리 대신 단일 클래스로 전역 예외 처리를 통합적으로 할 수 있다.

예외 처리 메서드를 별도 컨트롤러 클래스에 정의하고 클래스 레벨에 @ControllerAdvice 어노테이션을 지정한다. 이렇게 하면 애플리케이션 컨텍스트에 존재하는 모든 컨트롤러에 어드바이스가 적용되어 커스텀 예외 또는 모든 예외에 대한 처리를 적용할 수 있다. @ControllerAdvice 어노테이션은 @ExceptionHandler 어노테이션을 전역적으로 적용하는 방법으로 볼 수 있다.

@ControllerAdvice
public class ExceptionHandlingAdvice {
  @ExceptionHandler(예외클래스명.class)
  public String handle(예외클래스명 ex) {
    return "예외페이지논리뷰명";
  }

  @ExceptionHandler
  public String handleDefault(Exception e) {
    return "예외페이지논리뷰명";
  }
}


ResponseEntityExceptionHandler 클래스에는 모든 발생 가능한 스프링 MVC 예외를 처리하는 @ExceptionHandler 메서드가 정의되어 있다. 예외 핸들러 메서드는 응답 본문에 예외 세부 사항이 포함되는 ResponseEntity 객체를 반환한다. 이 클래스는 전역 예외 처리를 위한 @ControllerAdvice 어노테이션을 지정한 예외 처리 클래스의 부모 클래스로 주로 사용된다. 서브 클래스는 특정 예외를 처리하는 개별 메서드를 재정의할 수 있다. 또한 handleExceptionInternal() 메서드를 재정의하여 모든 예외에 대한 공통 처리를 할 수 있으며 createResponseEntity() 메서드를 재정의하여 미리 정의된 HTTP 상태 코드, 헤더 및 본문으로 ResponseEntity 객체를 생성하는 마지막 단계를 가로채서 직접 구현할 수도 있다.

@ControllerAdvice
public class GlobalResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
  @ExceptionHandler(value = {
    IllegalArgumentException.class,
    IllegalStateException.class
  })
  protected ResponseEntity<Object> handleConflict(RuntimeException ex, WebRequest request) {
    String bodyOfResponse = "응답메시지본문";
    return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request);
  }
}


스프링 버전 5는 예외 처리를 위해 ResponseStatusException 클래스를 제공한다. ResponseStatusException 생성자는 HTTP 응답 상태인 HttpStatus 객체, 발생한 예외에 대한 메시지 지정할 수 있는 문자열, Throwable 객체를 파라미터로 갖는다.

컨트롤러의 요청 핸들러 메서드에서 발생 가능한 예외를 붙잡은 후 HttpStatus, 예외 관련 메시지, 예외 객체를 인자로 생성자를 호출하여 ResponseStatusException 인스턴스를 새로 생성한 후 던짐으로써 예외 처리 및 응답 메시지 구성이 가능하다.

@GetMapping(value = "/{id}")
public String findById(@PathVariable("id") Long id) {
  try {
    ...
  } catch (예외클래스명 ex) {
    throw new ResponseStatusException(HttpStatus.NOT_FOUND, "메시지", ex);
  }
}


ResponseStatusException 클래스를 사용하여 응답 메시지를 구성하는 경우 동일한 예외 타입에 대해 서로 다른 응답 메시지를 구성할 수 있다. 따라서 응답 메시지를 세분화하기 위해 많은 수의 커스텀 예외 클래스를 만들 필요가 없어진다. 이를 통해 예외 타입 정의를 세분화한 후 종류 및 특성에 따라 그룹화하고 응답 메시지를 그룹 별로 구성할 수 있다. @ExceptionHandler 어노테이션을 사용하는 경우에는 예외 처리 메서드 정의 시 하나 이상의 예외 클래스에 대해 하나의 응답 메시지를 구성하였다.

ResponseStatusException 클래스를 사용하는 경우 전역 예외 처리가 어려우므로 @ControllerAdvice 어노테이션 사용이 필요하다. 또한 컨트롤러 클래스 별로 예외 처리 메서드가 정의되므로 코드가 중복될 가능성이 있다. 동일한 예외 타입에 대해 서로 다른 응답 메시지 구성이 가능한 것은 장점이 될 수 있지만 이는 단점이 될 수도 있다. 하나의 예외 타입에 대해 서로 모순되는 응답 메시지를 구성하지 않도록 관리가 필요하다.


스프링 부트의 예외 처리

스프링 부트는 예외 처리를 위해 ErrorController 구현을 제공한다.


참고

Categories: ,

Updated:

Comments