스프링 예외처리 (핸들러, 인터셉터, 필터)
스프링을 공부하다보면 위와 같은 사진을 자주 접한다. 특이점은 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가지다.
- preHandle
- postHandle
- afterCompletion
클라이언트의 요청 흐름은 다음과 같다. (DispatcherServlet 참고)
Interceptor(preHandle)
-> Handler
-> Interceptor(postHandle)
->
HandlerExceptionResolver(예외 복구)
-> View 렌더링
-> Interceptor(afterCompletion)
따라서 Interceptor(preHandle)
, Interceptor(postHandle)
에서 예외가 발생할 때 @ExceptionHandler
로 핸들링할 수 있다.
여기서 궁금증이 하나 더 생긴다. Interceptor(afterCompletion)
에서 예외가 발생하면 어떻게 처리될까?
=> @ExceptionHandler
로 예외를 핸들링할 수 없고, 로그를 남길 뿐
이다. (클라이언트로 응답은 이루어진다)
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 를 추가하여 직접 디버깅 해보는 것을 추천한다.