Spring

[Spring] Custom exception로 공통 예외 처리하기(feat. @ExceptionHandler, @ControllerAdvice)

날아 2024. 5. 7. 03:51

토이 프로젝트에서 예외를 처리 하는 과정에서 custom exception을 구현했다.

(가장 큰 이유는 실무 로직을 짜다보면 다양한 상황에 대해 예외가 발생할 수 있는데, 협업을 함에 있어 프론트단에 구체적인 예외를 알려주는게 좋다고 생각했다. 또한 예외처리를 한 곳에서 관리할 수 있다는 점에서 유지보수에 용이하다고 생각한다.)

 

스프링은 예외처리를 하기 위해 @ExceptionHandler라는 유연한 기능을 제공한다. 

그리고 이러한 예외처리를 전역에서 제공하기 위해 @ControllerAdvice, @RestControllerAdvice를 사용할 수 있다.

 

이번 포스팅은 각 어노테이션이 어떠한 기능을 수행하며, 어떻게 예외처리를 깔끔하게 할 수 있는지 정리해보고자 한다. 

 

1. Spring의 기본 예외처리 

일반적으로 Spring의 요청 흐름은 아래와 같다.

WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러

 

 

스프링은 기본적으로 에러 처리를 위한 컨트롤러를 구현해두었고(BasicErrorController), 에러가 발생하면 /error로 에러 요청을 다시 전달하도록 WAS 설정을 해두었다. 

 

만약, 컨트롤러 하위에서 예외가 발생할 경우 별도의 예외처리를 하지 않으면 에러가 WAS 까지 전달되고 WAS는 해당 예외를 처리한다. 

WAS는 스프링 부트가 등록한 에러 설정(/error)에 맞게 요청을 전달하는데 전체 흐름은 아래와 같다. 

WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러 -> 컨트롤러(예외발생)
-> 인터셉터 -> 서블릿(디스패처 서블릿) -> 필터 -> WAS(톰캣) 
-> WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러(BasicErrorController)

 

결국, 에러가 발생하면 에러 컨트롤러를 한 번 호출하는 것이다.

 

 

아주 친숙할(...) 기본적인 에러 페이지이다. 

다만 이러한 기본적인 에러 처리는 클라이언트 입장에서 유용하지 못하다. 

  • 에러가 발생하면 컨트롤러, 필터, 인터셉터가 두 번씩 호출된다.
  • 상태코드 500에 Internal Server Error 만을 응답하기 때문에 클라이언트는 유의미한 에러 응답을 받을 수 없다. 

 

따라서 실무에서는 별도의 예외처리 전략을 사용한다. 

 


2. HandlerExceptionResolver

HandlerExceptionResolver는 컨트롤러 하위에서 발생한 예외를 서블릿 컨테이너까지 전달하지 않고, 스프링 내에서 처리하기 위해 만들어진 객체이다. 

대부분의 HandlerExceptionResolver는 발생한 예외를 catch 하고 HTTP 상태나 응답 메시지등을 설정한다. 따라서 WAS 입장에서는 해당 요청이 정상적으로 응답된 것으로 인식하여 (catch 했기 때문에), 위에서 설명한 복잡한 WAS 호출이 일어나지 않는다. 

 

HandlerExceptionResolver는 적합한 예외처리를 위해 구현체들을 빈으로 등록해서 관리한다. 그리고 우선순위대로 적합한 구현체를 찾아 예외처리를 한다.

  • DefaultErrorAttributes
    • 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.
      (때문에 얘는 제외하고 직접 예외를 처리하는 3가지 구현체들만 HandlerExceptionResolverComposite로 모아 따로 관리함)
  • ExceptionHandlerExceptionResolver
    • 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
  • ResponseStatusExceptionResolver
    • Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
  • DefaultHandlerExceptionResolver
    • 스프링 내부의 기본 예외들을 처리한다.

 


3. @ExceptionHandler , @ControllerAdvice, @RestControllerAdvice

@ExceptionHandler

  • 스프링에서 ExceptionResolver를 동작시켜 예외를 처리하는 도구 중 @ExceptionHandler는 매우 유연한 예외처리 방법을 제공한다.
  • 에러 응답을 자유롭게 다룰 수 있다. (custom exception 처리 가능)
  • 컨트롤러의 메서드에 해당 에너테이션을 적용할 수 있는데, 이는 해당 컨트롤러에서만 적용될 뿐 전역으로 사용할 수 없다. 

 

@ControllerAdvide, @RestControllerAdvice

  • @ExceptionHandler를 전역으로 사용할 수 있게 한다. 
  • @RestControllerAdvice는 @ReponseBody 어노테이션을 포함하기 때문에 JSON으로 메시지를 응답한다. 
  • 사용 방법 : 해당 에너테이션을 사용하여 전역적으로 오류를 핸들링하는 class를 만든다. 

 


4. 실제 적용

4-1. 사용할 ErrorCode 정의

먼저 클라이언트에게 보낼 에러 코드를 정의해야 한다. 에러 코드는 전역적으로 사용될 ErrorCode와 특정 도메인에서 구체적으로 사용될 CostomErrorCode (MemberErrorCode, ProjectErrorCode 등)로 나누고 인터페이스로 추상화하였다. 

 

public interface ErrorCodeIfs {

    Integer getHttpStatusCode(); //HTTP 응답 코드

    Integer getErrorCode(); //커스텀 상태코드

    String getDescription(); //설명
}

 

 

전역 ErrorCode

 

CustomErrorCode

 

 

4-2. Custom Excpetion을 처리할 class 생성 

설정한 ErrorCode 정보를 담을 class를 생성한다. 

 

 

4-3. Controller 전역에서 발생하는 Custom Error를 잡아줄 Handler 생성 

해당 class에서는 @ExceptionHandler를 통해 발생한 ApiException 예외를 잡아서 하나의 메소드에서 공통 처리한다. 또한 @RestControllerAdvice를 통해 전역으로 관리함을 알 수 있다.

 

 

4-4. 적용

회원가입 중 email이 중복된다면 Custom Error Code인 EMAIL_NOR_DUPLICATED를 발생시켜 해당 response를 클라이언트에게 보낸다.

 


 

이렇듯 custom 예외 처리를 통해 무분별한 try - catch 문을 줄이고 유연한 예외처리를 할 수 있었다. 

 

포스팅에는 생략되었지만 @Valid 에너테이션에서 에러가 발생할 시 발생하는 예외(MethodArgumentNotValidException)를 처리하는 exceptionHandler도 구현하였는데, 이렇게 다양한 예외를 한 곳에서 관리한다는 점은 협업에서 아주 중요할 것 같다.

 

개인적으로 해당 포스팅을 정리하며 예외처리에 대해 다시 한 번 공부할 수 있어 좋았고, 토이 프로젝트에서 예외처리를 response 하는 과정의 코드가 지저분한 것 같아(....) 이 부분은 다시 리팩토링 하려 한다.

 

 

참고

더보기

 

'Spring' 카테고리의 다른 글

스프링 배치(Spring Batch)로 대용량 데이터 관리하기  (0) 2024.04.01
[Spring] Spring Security  (0) 2023.02.08
[Spring] Servlet 과 Spring  (0) 2023.02.06
[Spring] 빈 스코프  (0) 2023.02.01
[Spring] 빈 생명주기 콜백  (0) 2023.01.30