포스트

스프링 예외처리 (핸들러, 인터셉터, 필터)

스프링 예외처리 (핸들러, 인터셉터, 필터)

스프링을 공부하다보면 위와 같은 사진을 자주 접한다. 특이점은 Handler 에서 발생하는 예외를 HandlerExceptionResolver에서 catch한다는 것을 알 수 있다. 그럼 이런 궁금증이 생긴다.

  • 1) HandlerInterceptor 에서 예외가 발생하면 어떻게 될까?
  • 2) Filter 에서 예외가 발생하면 어떻게 될까?

 

이 글은 다음 내용을 다루고자 한다.

  • 핸들러 안에서 예외가 발생하는 경우 어떻게 처리할까 ?
  • 핸들러 밖에서 예외가 발생하는 경우 어떻게 처리할까 ?
    • HandlerInterceptor에서 예외 발생
    • Filter에서 예외 발생

 

(용어정리)
핸들러 는 컨트롤러(controller) 에서 @RequestMapping 어노테이션이 붙은 메서드를 의미로 사용했음.
핸들링 은 에러를 회복한다는 의미로 사용했음.


1. 핸들러 안에서 예외가 발생하는 경우 어떻게 처리할까 ?

핸들러 안에서 비즈니스 로직 을 수행하다보면 다양한 예외( DB 요청 / 다른 API 요청 / 외부 라이브러리 사용 등) 가 발생할 수 있다.

 

1) 비즈니스 로직에서 발생하는 예외 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
@AllArgsConstructor
public class ChannelService {

    // ...

    public Channel getChannel(String channelId) {
        return channelRepository.findById(channelId).orElseThrow(ChannelNotFoundException::new); // channelId로 채널을 찾지 못하면 예외 발생
    }
}


public interface ChannelRepository extends JpaRepository<Channel, String> {
    Optional<Channel> findById(String id);
}

어떻게 처리할까?

클라이언트가 API서버에 채널id로 채널정보를 요청하는 부분이라고 가정해보자.
그렇다면 사용자가 요청한 채널id가 존재하지 않는 경우 이를 잘 표현하는 HTTP 상태코드와 본문을 반환하는 것이 이상적일 것이다.

 

2) 외부 라이브러리 에서 예외를 발생하는 예외 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.apache.commons.lang.math;

import ...

public class NumberUtils {
    public static Number createNumber(String str) throws NumberFormatException {
        if (str == null) {
            return null;
        }
        if (StringUtils.isBlank(str)) {
            throw new NumberFormatException("A blank string is not a valid number");
        }  

        // ...
    }

    // ...
}

createNumber(String str) 는 문자열 타입의 숫자를 Number 타입으로 변경시켜주는 라이브러리이다.
(eg. NumberUtils.createNumber("12345"))

숫자로 변형이 불가능한 값이 오는 경우(NumberUtils.createNumber("abcde")) NumberFormatException 이 발생한다.

 

어떻게 처리할까?

먼저, 위 1) 비즈니스 로직에서 발생하는 예외의 경우 에서 언급한 것과 같이 적절한 HTTP 상태코드와 본문을 반환하여 처리할 수 있다.

 

두번째로는, 아래의 방법으로 처리하는 것도 가능하다.

1
2
3
4
5
6
7
8
9
10
    // ... 비즈니스로직
    private int parseInt(String number) {
        int result = 0;
        try {
            result = NumberUtils.createNumber(number); // Apache Commons Library
        } catch (NumberFormatException e) {
            log.error(e.getMessage(), e);
        }
        return result;
    } 

외부 라이브러리(NumberUtils)에서 예외를 발생시키지만, parseInt(String number) 메서드)는 예외가 발생하면 0 값으로 처리하고 있다.

val numberA = parseInt("abc") // numberA는 0이다.

위의 경우 잘못된 숫자(abc) 를 입력하여 예외가 발생하지만, 0으로 회복된다.

따라서 parseInt(String number) 를 사용하는 곳에서는 내부에서 예외가 발생했었는지도 알지 못하고 알 필요도 없다.

 

두 방법 중 어떤것이 적절한가는 비즈니스 상황에 따라 적절한 것을 선택하면 될 것이다.

   


   

2. 핸들러 밖에서 예외가 발생하는 경우 어떻게 처리할까 ?

1) HandlerInterceptor 에서 예외가 발생하면 어떻게 될까?

HandlerInterceptor 는 구현할 수 있는 메서드가 총 3가지다.

  1. preHandle
  2. postHandle
  3. afterCompletion

 

클라이언트의 요청 흐름은 다음과 같다. (DispatcherServlet 참고)

Interceptor(preHandle) -> Handler -> Interceptor(postHandle) ->
HandlerExceptionResolver(예외 복구) -> View 렌더링 -> Interceptor(afterCompletion)

 

따라서 Interceptor(preHandle), Interceptor(postHandle) 에서 예외가 발생할 때 @ExceptionHandler 로 핸들링할 수 있다.

 

여기서 궁금증이 하나 더 생긴다. Interceptor(afterCompletion) 에서 예외가 발생하면 어떻게 처리될까?

=> @ExceptionHandler 로 예외를 핸들링할 수 없고, 로그를 남길 뿐 이다. (클라이언트로 응답은 이루어진다)

HandlerExecutionChain.java

1
2
3
4
5
6
    // ...
    try {
        interceptor.afterCompletion(request, response, this.handler, ex);
    } catch (Throwable ex2) {
        logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
    }

   

2) Filter 에서 예외가 발생하면 어떻게 될까?

필터에서 예외를 던지면 핸들링은 어디서 어떻게 할까.

=> 아래와 같이 HTTP 상태코드 500 으로 응답한다.

java.org.apache.catalina.core.StandardWrapperValve.java

1
2
3
4
5
6
    private void exception(Request request, Response response,
                           Throwable exception) {
        request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, exception);
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); // 상태코드 500
        response.setError();
    }

단, 필터에서 예외를 던지더라도 500 상태코드로 응답하지 않는 경우가 있다. Response 객체가 이미 commit된 경우에는 상태코드/헤더 변경이 불가능하기 때문이다.
(핸들러에서 @ResponseBody로 응답하는 경우 해당 객체를 응답 본문으로 변경이 필요하다. 이는 MessageConverter가 처리하게 되는데, OutputStream에 객체를 write하여 처리한다. 즉, 사용자에게 바로 응답이 가게되고 이 과정에서 Response는 commit이 이루어진다.)
(대부분의 API는 HTTP 본문에 응답하는 경우가 많으니, Interceptor(postHandle) 에서 상태코드/헤더를 변경이 불가능할 수 있다는 점은 기억하고 싶다.)

   

이 글은 조사했던 부분을 기록하기 위해 작성하였으며 독자에게 도움이 됬을지 모르겠다.
만약 도움이 안됬다면 스프링 프로젝트에 Filter/Interceptor 를 추가하여 직접 디버깅 해보는 것을 추천한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.