스프링의 예외 처리 방법 이해하기(ExceptionHandler, ControllerAdvice 등)

현서황·2024년 11월 19일

스프링의 예외 처리 방식을 이해하기 위래 먼저 스프링의 전체적 흐름을 이해해야한다.


스프링의 요청 처리 과정


Spring이 요청에 대한 처리를 어떤 흐름으로 진행하는지에 대한 그림이다.

  1. 클라이언트의 요청을 디스패처 서블릿이 받음.
  2. 요청 정보를 통해 요청을 위임할 컨트롤러 찾음
  3. 요청을 컨트롤러로 위임할 핸들러 어댑터를 찾아 전달함.
  4. 핸들러어댑터가 컨트롤러로 요청을 위임함.
  5. 비즈니스 로직을 처리함.
  6. 컨트롤러가 반환값을 처리함.
  7. 핸들러어댑터가 반환값을 처리함.
  8. 서버의 응답을 클라이언트로 반환함.
  • Dispatcher-Servlet
    디스패처 서블릿의 dispatch는 "보내다"라는 뜻을 가지고 있다. 그리고 이러한 단어를 포함하는 디스패처 서블릿은 HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러(front Controller)라고 정의할 수 있다.
    클라이언트로부터 어떤 요청이 오면 톰캣(Tomcat)과 같은 서블릿 컨테이너가 요청을 받게된다. 그리고 이 모든 요청을 프론트 컨트롤러인 디스패처 서블릿이 가장 먼저 받게된다. 그러면 디스패처 서블릿은 공통작업을 먼저 처리한 후 해당 요청을 처리해야하는 컨트롤러를 찾아 작업을 위임한다.
    여기서 프런트 컨트롤러(front controller)라는 용어가 사용되는데, Front Controller는 주로 서블릿 컨테이너의 제일 앞에서 서버로 들어오는 클라이언트의 모든 요청을 받아 처리해주는 컨트롤러로서, MVC 구조에서 함께 사용되는 디자인 패턴이다.
    과거에는 모든 서블릿을 URL 매핑을 위해 web.xml에 모두 등록해주어야 했지만, dispatcher-servlet이 해당 어플리케이션으로 들어오는 모든 요청을 핸들링해주고 공통 작업을 처리면서 상당히 편리하게 이용할 수 있게 되었습니다. 우리는 컨트롤러를 구현해두기만 하면 디스패처 서블릿가 알아서 적합한 컨트롤러로 위임을 해주는 구조가 되었습니다.

❓ 만약 Controller에서 로직을 처리하던 중 예상치못한 에러가 발생한다면 어떻게될까?

스프링의 기본적인 예외 처리 방법


이와같은 컨트롤러의 getProduct에서 NoSuchElementFoundException 예외가 발생했다면 우리는 접속한 환경에 따라 다른 에러처리를 받게 된다. 만약 웹페이지로 접속했다면 Whitelabel Error Page를 반환받게된다.

Spring은 만들어질 때부터 에러처리를 위한 BasicErrorController를 구현해두었고, 스프링 부트는 예외가 발생하면 기본적으로 /error로 에러 요청을 다시 전달하도록 WAS 설정을 해두었다.

WAS는 웹 서버와 웹 컨테이너가 합쳐진 형태로서 웹 서버 단독으로는 처리할 수 없는 데이터베이스의 조회나 다양한 로직 처리가 필요한 동적 컨텐츠를 제공한다.
대표적인 WAS 종류: Tomcat
WAS에 대한 더욱 자세한 설명은! ->CLICK!!

그래서 별도의 설정이 없다면 예외 발생 시에 BasicErrorController로 에러 처리 요청이 전달된다. 참고로 이는 스프링 부트의 WebMvcAutoConfiguration을 통해 자동 설정이 되는 WAS의 설정이다.
여기서 요청이 /error로 다시 전달된다는 부분에 주목해야한다. 일반적인 요청 흐름은 다음과 같이 진행된다.
WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러
그리고 컨트롤러 하위에서 예외가 발생했을 때, 별도의 예외 처리를 하지 않으면 WAS까지 에러가 전달된다. 그러면 WAS는 애플리케이션에서 처리를 못하는 예외라 exception이 올라왔다고 판단을 하고 대응 작업을 진행한다.
컨트롤러(예외발생) -> 인터셉터 -> 서블릿(디스패처 서블릿) -> 필터 -> WAS(톰캣)

WAS는 스프링 부트가 등록한 에러 설정(/error)에 맞게 요청을 전달하는데, 이러한 흐름을 총 정리하면 다음과 같다.
WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러
-> 컨트롤러(예외발생) -> 인터셉터 -> 서블릿(디스패처 서블릿) -> 필터 -> WAS(톰캣)
-> WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러(BasicErrorController)

기본적인 에러 처리 방식은 결국 에러 컨트롤러를 한 번 더 호출하는 것이다. 그러므로 필터나 인터셉터가 다시 호출될 수 있는데, 이를 제어하기 위해서는 별도의 설정이 필요하다.

서블릿은 dispatcherType으로 요청의 종류를 구분하는데, 일반적인 요청은 REQUEST이며 에러 처리 요청은 ERROR이다.
필터는 서블릿 기술이므로 필터등록(FilterRegistrationBean) 시에 호출될 dispatcherType 타입을 설정할 수 있고, 별도의 설정이 없다면 REQUEST일 경우에만 필터가 호출된다.
하지만, 인터셉터는 스프링 기술이므로 dispatcherType을 설정할 수 없어 URI 패턴으로 처리가 필요하다. 스프링 부트에서는 WAS까지 직접 제어하게 되면서 이러한 WAS의 에러 설정까지 가능해졌다. 또한 이는 요청이 2번 생기는 것은 아니고, 1번의 요청이 2번 전달되는 것이다. 그러므로 클라이언트는 이러한 에러 처리 작업이 진행되었는지 알 수 없다.

BasicErrorController


BasicErrorController는 accept헤더에 따라 에러 페이지를 반환하거나 에러 메시지를 반환한다. 에러 경로는 기본적으로 /error로 정의되어있으며 properties에서 server.error.path로 변경할 수 있다.

errorHtml()과 errer()는 모두 getAttributeOptions를 호출해 반환할 에러 속성을 얻는데, 기본적으로 DefaultErrorAttributes로부터 반환할 정보를 가져온다. DefaultErrorAttributes는 전체 항목들에서 설정에 맞게 불필요한 속성들을 제거한다.

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

위는 기본 설정으로 받는 에러 응답이다. 나름 잘 갖추어져 있지만 클라이언트 입장에서 유용하지 못하다. 클라이언트는 "Item with id 5000 not found"라는 메시지와 함께 404 status로 에러 응답을 받으면 훨씬 유용할 것이다.
다음과 같이 properties를 통해 에러 응답을 조정할 수 있다. 물론 운영 환경에서 구현이 노출되는 trace는 제공하지 않는 것이 좋다.

참고로 SpringBoot 2.3 이전에는 message를 기본적으로 제공했었지만, SpringBoot 2.3부터는 클라이언트에게 너무 많은 정보가 노출되는 것을 방지하기 위해 기본적으로 제공하지 않게 되었다.

스프링이 제공하는 다양한 예외 처리 방법

JAVA에서는 예외 처리를 위해 try-catch를 사용해야 하지만 try-catch를 모든 코드에 붙이는 것은 비효율적이다. Spring은 에러 처리라는 공통 관심사(cross-cutting concerns)를 메인 로직으로부터 분리하는 다양한 예외 처리 방식을 고안했고, 예외 처리 전략을 추상화한 HandlerExceptionResolver 인터페이스를 만들었다. (전략 패턴이 사용된 것이다.)

대부분의 HandlerExceptionResolver는 발생한 Exception을 catch하고 HTTP상태나 응답메시지 등을 설정한다. 그래서 WAS입장에서는 해당 요청이 정상적인 응답인 것으로 인식되며, 위에서 설명한 복잡한 WAS의 에러 전달이 진행되지 않는다.


위의 Object 타입인 handler는 예외가 발생한 컨트롤러 객체이다. 예외가 던져지면 디스패처 서블릿까지 전달되는데, 적합한 예외처리를 위해 HandlerExceptionResolver구현체들을 빈으로 등록해서 관리한다. 그리고 적용 가능한 구현체를 찾아 예외를 처리하는데, 우선순위대로 아래의 4가지 구현체들이 빈으로 등록되어있다.

  • DefaultErrorAttributes: 에러 속성을 저장하며 직접 예외를 처리하지 않는다.
  • ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리한다.
  • ResponseStatusExceptionResolver: Http상태코드를 지정하는 @ResponseStatus 또는 ResponseStatusException을 처리한다.
  • DefaultHandlerExceptionResolver: 스프링 내부의 기본 예외들을 처리한다.

Spring에서 ExceptionResolver를 동작시켜 에러를 처리할 때 사용하는 도구들을 살펴보자.
ExceptionHandler, RestControllerAdvice의 두 개가 있다.
ResponseStatusException의 한계점들을 보완해준 방식들이라 이것들을 사용한다.

ResponseStatusException의 한계점

  • 직접 예외처리를 프로그래밍하므로 일관된 예외처리가 어려움
  • 예외처리 코드가 중복될 수 있음
  • Spring 내부의 예외를 처리하는 것이 어려움
  • 예외가 WAS까지 전달되고, WAS의 에러 요청 전달이 진행됨

@ExceptionHandler

  • 매우 유연한 에러처리 제공
  • 에러 응답(payload)자유롭게 다룰 수 있음.
  • 컨트롤러의 메소드에 해당 어노테이션을 적용할 수 있는데, 이는 전역적으로 사용할 수 없다.
    전역적인 사용을 위해서는 @RestControllerAdvice 또는 @ControllerAdvice 애노테이션을 사용해야 한다.

@RestControllerAdvice

  • Spring 4.3부터 제공하는 애노테이션
  • @ExceptionHandler를 전역적으로 사용할 수 있도록 도와줌
  • @ControllerAdvice와의 차이점은 에러응답을 JSON으로 내려준다는 것이다.
  • 사용방법: 애노테이션을 적용해 전역적으로 에러를 핸들링하는 class를 만들어 사용한다.(에러처리 위임)

출처: https://mangkyu.tistory.com/18 [MangKyu's Diary:티스토리]
출처: https://mangkyu.tistory.com/204 [MangKyu's Diary:티스토리]

profile
노는 게 제일 좋은 뽀로로

0개의 댓글