Spring boot/Spring 개념

Spring Filter 예외처리 vs Interceptor 예외처리

eunnnn 2023. 5. 2. 16:10

Spring 기본 예외처리 방식 - BasicErrorController

서비스를 이용하던 도중 예외가 발생했다면 우리는 접속한 환경에 따라 다른 에러 처리를 받게 될 것이다. 만약 우리가 웹페이지로 접속했다면 다음과 같은 whiltelabel 에러 페이지를 반환받는다.

 

 

 

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

 

참고로 이는 스프링 부트의 WebMvcAutoConfiguration를 통해 자동 설정이 되는 WAS의 설정이다.

 

여기서 요청이 /error로 다시 전달된다는 부분에 주목해야 한다. 일반적인 요청 흐름은 다음과 같이 진행된다.

 

그리고 컨트롤러 하위에서 예외가 발생하였을 때, 별도의 예외 처리를 하지 않으면 WAS까지 에러가 전달된다. 그러면 WAS는 애플리케이션에서 처리를 못하는 예와라 exception이 올라왔다고 판단을 하고, 대응 작업을 진행한다.

 

WAS는 스프링 부트가 등록한 에러 설정(/error)에 맞게 요청을 전달하는데, 이러한 흐름을 총 정리하면 다음과 같다.

 

기본적인 에러 처리 방식은 결국 에러 컨트롤러를 한번 더 호출하는 것이다. 이 ErrorController는 filter에서 발생하는 에러도 잡아주는데, 에러가 발생하면 서블릿 컨테이너가 캐치해서 에러페이지 경로로 forward해준다고 생각하면 된다.

 

 

Spring Interceptor 내부에서 예외 처리

컨트롤러에서 예외가 발생하면 HandlerExceptionResolver에서 예외를 처리한다. 따라서, 예외가 발생해도 WAS까지 예외가 전달되지 않고 스프링 MVC에서 예외 처리는 끝이 난다. 결과적으로 WAS 입장에서는 정상 처리가 된 것이다.

 

스프링은 API 예외 처리를 위해 @ExceptionHandler 어노테이션을 제공한다.

@ExceptionHandler([class명.class]) 와 같이 @ExceptionHandler에 인자를 넣어서, 어떤 에러를 처리할지 지정하여 사용한다. 예를 들어 @ExceptionHandler({SQLException.class,DataAccessException.class}) 와 같이 사용할 수 있습니다.

@Controller
public class ExceptionHandlingController {

  // @RequestMapping한 메소드, 각 메소드들은 exception을 일부러 발생하도록 작성
  @RequestMapping("/test1")
  public String ControllerTest1(){
 throw new SQLException();
  }
  
  @RequestMapping("/test2")
  public String ControllerTest2(){
 throw new DataIntegrityViolationException();
  }
  
  
  //  @ExceptionHandler를 사용하여 Exception 처리 메소드들을 작성

  // error를 보여줄 viewname을 리턴
  @ExceptionHandler({SQLException.class,DataAccessException.class})
  public String databaseError() {
    return "databaseError";
  }

  // model 과 viewname을 리턴
  @ExceptionHandler(DataIntegrityViolationException.class)
  public ModelAndView handleError(HttpServletRequest req, Exception ex) {
    logger.error("Request: " + req.getRequestURL() + " raised " + ex);

    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", ex);
    mav.addObject("url", req.getRequestURL());
    mav.setViewName("error");
    return mav;
  }
}

 

또한 @ControllerAdvice 어노테이션을 사용하면 개별 컨트롤러뿐만 아니라 전체 컨트롤러에 에러 처리를 적용할 수 있다.

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409 에러코드 발생
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // 아무것도 하지 않음.
    }
}

아래와 같이 @ControllerAdvice를 사용하여 클래스를 스프링 프로젝트에 작성을 하면, Exception이 어디에서 발생하든 이 클래스가 DataIntegrityViolationException 처리를 할 수 있게 된다.

 

Spring Filter 예외 처리

 

ExceptionManager 로 사용하는 @RestControllerAdvice  @ControllerAdvice  Filter 단에서 발생한 예외를 핸들링 해주지 못한다. 아래 사진을 보면 Filter에서 발생한 예외는 @ControllerAdvice의 적용범위 밖이란 점을 확인할 수 있다.

따라서, Filter에서 발생하는 예외를 핸들링하려면, 기본적으로 ErrorController에서 처리된다.

그러나 또다른 방법으로  예외 발생이 예상되는 Filter의 상위에 예외를 핸들링하는 Filter를 만들어서 Filter Chain에 추가해주는 방법이 있다.  

예를 들어 Spring Security 상황에서의 예외 처리 코드를 살펴보자. ExpiredJwtException, JwtException, IllegalArgumentException 중 하나의 예외가 발생했을 경우 에러 코드와 메시지를 세팅한 Response를 응답하는 필터는 다음과 같이 구현된다.

public class ExceptionHandlerFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {

        try{
            filterChain.doFilter(request, response);
        }catch (ExpiredJwtException e){
            //토큰의 유효기간 만료
            setErrorResponse(response, ErrorCode.TOKEN_EXPIRED);
        }catch (JwtException | IllegalArgumentException e){
            //유효하지 않은 토큰
            setErrorResponse(response, ErrorCode.INVALID_TOKEN);
        }
    }
    private void setErrorResponse(
            HttpServletResponse response,
            ErrorCode errorCode
    ){
        ObjectMapper objectMapper = new ObjectMapper();
        response.setStatus(errorCode.getHttpStatus().value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        ErrorResponse errorResponse = new ErrorResponse(errorCode.getCode(), errorCode.getMessage());
        try{
            response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    @Data
    public static class ErrorResponse{
        private final Integer code;
        private final String message;
    }
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
		...
        
        http.addFilterBefore(
            new ExceptionHandlerFilter(),
            UsernamePasswordAuthenticationFilter.class
        );
        
        ...
    }
}

 

 

출처

더보기