Web/Spring

Error Handling w.Spring: 꼼꼼하고 효율적인 예외처리는 어떻게 만들어질까?

TLdkt 2023. 1. 26. 20:16
728x90
반응형
 

0. 들어가며

500 Server Error를 숱하게 보다 보면 진작 예외처리를 꼼꼼히 할걸...하는 후회를 하게 된다.

그러나 예외처리를 설정한다고 하더라고, 에러코드만 띡 나가는 메세지는 너무나 불친절하다. 

꼼꼼하면서 성능적으로도 효율적이게 예외처리를 하고 싶다면 무엇을 활용하면 될까?

종류별로 정리해봤다.

 

1. Spring의 예외처리 관심사와 발전 과정

Spring 예외처리에는 3가지 관심사는 아래와 같다.

- WAS 두 번 일시키지 말고 효율적으로 처리하자

- API 사용자를 위해 자세하게 커스텀 응답을 쓰자 

- 스프링 내외부에서 발생한 모든 에러를 커버하자 

 

이를 커버하기 위해 예외처리가 발전해온 과정을 정리해보려 한다. 

우선 관심사를 어떻게 충족했는지 살펴보자.

 

 

 

1) 효율적인 예외처리: @ExceptionHandlerExceptionResolver

 

참고) WAS?

 

WAS는 동적 컨텐츠나 DB조회, 로직 처리 등을 수행하는 Web Application Server의 줄임말이다.

 

 

WAS가 두 번 일한다는 건 이런 말이다. 

WAS는 서버의 응답이 어떤지에 따라 컨트롤러를 재호출하기도 하고, 응답을 내보내기도 한다. 만약 설정이 아예 안 되어 있다면 500 에러를, 설정이 있다면 다시 요청을 보내게 된다. 

이때 거치는 필터와 인터셉터들은 무의미한 작업을 반복하게 되는 것이다. 

 

이 그림처럼 예외가 그대로 WAS에 전달되면, 처리되지 않은 에러라고 생각하게 되고 기본적인  BasicErrorController로 재요청을 보낸다, 

 

따라서 궁극적으로 WAS가 다시 컨트롤러에 요청을 전달하지 않아도 되게끔 하는 것이 이 관심사에서의 최종 목표다. 

 

 

@ExceptionHandlerExceptionResolver가 이 문제를 해결했다. (@ExceptionHandler가 작동하는 경우 Resolver가 WAS 전에 해결해준다)

 

 

 

 

 

 

2) 커스텀 응답이란? @ResponseStatus, @ResponseStatusException, @ExceptionHandler

스프링 1.0부터 있던 BasicErrorController는 아래와 같은 항목을 갖추고 있다. 몇 개는 직접 설정해주어야 하지만, 이 모든 설명이 있다고 하더라고 이해되지 않는 긴 내용과 함께 무작정 뜨는 에러코드는 슬픔과 분노만을 낳을 뿐이다.

 

 

 

따라서 Api에 따라 적절한 에러 메세지를 돌려주어야 하는데, 개발자가 커스텀할 수 있는 정도 또한 애너테이션에 따라 다르다. 

 

한편, 응답을 커스텀한다고 해서 WAS 일이 줄어드는 건 아니다. 

sendError() 메서드 호출이력이 있는 경우,  커스텀된 에러메세지를 응답할 수는 있지만 여전히 컨트롤러를 2번 호출해야 하는 것은 똑같다. 

 

어쨌든 이러한 여러 과정을 거치며 

@ResponseStatus 는 에러의 상태만을 변경하며,

@ResponseStatusException은 에러의 reason, cause를 추가 가능하고

,@ExceptionHandler는 code, message, error 리스트까지 가장 자유롭게 에러 응답을 다룰 수 있다.

 

 

3 ) 스프링 내외부에서 발생한 에러를 모두 처리하자 

 

Spring 예외처리 대상으로는 아래와 같은 세 가지 정도가 있다.

1 ) 비즈니스 로직 중에 발생한 예외

2 ) 의존 라이브러리 예외

3 ) 스프링 내부 예외

 

이 모두를 커버할 수 있다면 디버깅 시간도 단축되고, 일관적인 에러 핸들링으로 조금 더 효율적인 작업이 가능할 것이다.

 

 

스프링 예외처리 애너테이션

@BasicErrorController

  • timestamp: 에러가 발생한 시간
  • status: 에러의 Http 상태
  • error: 에러 코드
  • path: 에러가 발생한 uri
  • exception: 최상위 예외 클래스의 이름
  • message: 에러에 대한 내용
  • errors: BindingExecption에 의해 생긴 에러 목록
  • trace: 에러 스택 트레이스

이렇게 스프링에서 제공하는 범위 외에는 커스텀되지 않는 응답을 전송하는 기능이다.

익숙한 화면... 불편하다 불편해!

 

 

@ResponseStatus/ @ResponseStatusException

이 둘은 비슷하게 생겼지만 다르다.

 

@ResponseStatus의 경우

- 500 에러 외 설정 가능

- 클래스를 만들어 설정

- 외부 Exception 클래스에 붙일 수 없음

- 커스텀 어려움

- WAS 비효율적 예외처리 그대로 

이라는 특징이 있고,

 

@ResponseStatusException의 경우 

- 응답 바디 커스텀 가능

- 클래스와 별개로 직접 설정 가능

- Spring 내부 예외 처리 어려움

- 직접 설정하므로 중복 가능성 높아짐

- WAS의 비효율적 예외처리 그대로

 

라는 특징을 가진다.

즉, @ResponseStatusException은 기존에 비해 커스텀 정도는 조금 나아졌지만, 에러 처리범위나 효율적인 WAS 사용 면에서 떨어진다. 

 

 

@ExceptionHandler

컨트롤러의 메서드에 붙이거나, @ControllerAdvice가 붙어있는 클래스의 메서드에서 쓸 수 있다.

보다 자유롭게 커스텀 에러를 응답할 수 있지만, 특정 컨트롤러에서 발생하는 예외만 처리 가능하다는 한계가 있다

메서드 여러 개가 정의되어 있다면, 가장 구체적인 것부터 적용하고, 없으면 상위 계층의 에러를 처리한다.

 

 

@(Rest)ControllerAdvice + @ExceptionHandler

앞서 ExceptionHandler의 경우 특정 컨트롤러에 붙여야 하므로 코드가 여기저기 흩어져 있다는 단점이 있다. 따라서 AOP를 지향하는 스프링은 Advice로 한번에 처리할 수 있도록 설정을 열어두었다.

 

참고) 관심사의 분리 ( Aspect Oriented Programming)

흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.

 

따라서 ExceptionHandler와 함께 쓰는 경우 전역적 예외 처리가 가능한 데다, 커스텀 에러 응답이 가능하고, 코드 가독성이 향상된다는 장점이 있다.

 

 

 

 

Conclusion

스프링은 개발자가 예외처리를 하지 않아도 BasicErrorHandler를 통해 못생기고 비효율적인 예외를 돌려준다

비효율적이라는 것은 WAS한테 일을 두 번 시킨다는 것이다. 

WAS까지 도달하지 않고 처리할 수 있는 방법으로 @ExceptionHandler 가 있고, 이때 WAS는 응답이 정상적이라고 판단해 해당 응답을 그대로 내보낸다.

다만 @ExceptionHandler는 특정 컨트롤러에서만 작동하므로, 매 컨트롤러마다 코드가 중복된다는 단점이 있어@(Rest)ControllerAdvice가 등장했다. 

 

 

 

Reference

Web on Servlet Stack (spring.io)

https://reflectoring.io/spring-boot-exception-handling/

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

728x90
반응형