HandlerExceptionResolver
만약 클라이언트의 요구사항이 추가되어서 bad라는 uri가 들어올 경우, 클라이언트에게 서버 문제가 아닌, 클라이언트의 문제라는 것을 알려주려면 어떻게 해야 할까? 기존 컨트롤러에 추가해 보자.
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값");
}
return new MemberDto(id, "나의 이름은! " + id);
}
}
bad라는 값을 못 받는 로직을 추가했다. 한 번 실행해보자.
ExceptionResolver 적용 전 실행결과
WAS 입장에서는 서버 내부에서 Exception이 일어난 것이므로 다음과 같이 상태코드가 500이 되어버린다.
500 오류는 서버 오류이므로 이를 바꿔줘야 한다.
이것을 바꿀 수 있는 게 "HandlerExceptionResolver"이다. 줄여서 "ExceptionResolver"라고도 한다.
이를 사용함과 사용하지 않는 것은 매우 큰 차이가 있다. 아래 동작 원리를 살펴보자.
동작 원리
ExceptionResolver 적용 전
컨트롤러에 가서 Exception가 발생하는 것까지는 적용 후와 같다. 하지만 Exception가 발생했을 때 적용 전에는 postHandle을 거치지 않고 바로 afterCompletion으로 넘어간다. 그 후 Exception 전달이 되면서 오류가 나오므로, WAS는 500 에러를 내리게 된다.
ExceptionResolver 적용 후
컨트롤러에 가서 Exception이 발생하는 것까지는 적용 전과 같다. 적용 후에는 Exception이 발생했을 때 Dispatcher Servlet이 ExceptionResolver에게 Exception을 처리할 수 있는지 물어본다. Exception을 처리할 수 있다면 ExceptionResplver가 해결을 시도한다. Exception이 발생했을 때 Exception을 삼킨다.
그 후 빈 껍데기의 ModelAndView가 생기므로 View 렌더링 하지 않고 afterCompletion이 호출된 후에 WAS에게 정상 응답으로 돌아가게 된다. 이때, WAS에게 sendError가 호출이 됐기 때문에 오류페이지를 찾는다.
* 적용을 하던 안 하던 Exception는 발생하기 때문에 postHandle은 거치지 않는다.
이는 서버 내부에서 400으로 오류를 정했는데도 불구하고 500으로 판단한 servletContainner에게 400 Exception을 전달하게 되고, 그 후 WAS에게 정상 응답으로 나가게 된다.
WAS는 서버 내부에서 오류가 발생하면 500으로 판단한다는 점을 기억하면 쉽게 이해할 수 있다!
이제 HandlerExceptionResolver를 구현해 보자.
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
log.info("IllegalArgumentException resolver to 400");
// SC_BAD_REQUEST는 400 예외이다.
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
} catch (IOException ioE) {
log.error("----------------- error!! -----------------: " + ioE);
}
return null;
}
}
HandlerExceptionResolver interface를 구현하기 위해 ResolveException 메서드를 구현했다.
우리가 아까 'bad'가 url로 요청이 오면, IllegalArgumentException 이 발생하게 되어 if 조건문을 충족하게 되고, 400 에러와 Exception에 메시지를 응답에 담는다.
(참고로. sendError 메서드를 사용하려면 IOException을 꼭 넣어줘야 한다. 그래서 하단에 IOException을 잡도록 해놨다.)
아무것도 담지 않은 ModelAndView이므로 렌더링 하지 않고 정상적은 흐름으로 이어져서 servletContainner, WAS까지 정상적인 흐름이 return이 된다. return을 정상적으로 하면 Exception을 삼켜버린 것과 같다. 그 후, WAS는 sendError를 했기 때문에 400 에러인 것을 감지하고 오류 페이지를 찾는다.
ExceptionResolver가 ModelAndView를 반환하는 이유는 마치 Try, catch를 하듯이, Exception을 처리해서 정상 흐름처럼 변경하기 위해서이다. 이름 그대로 Exception을 해결하는 것이 목적이다.
이제 커스텀한 HandlerExceptionResolver를 등록하는 일만 남았다.
여기서 등록할 때 @Component 방식과 WebConfig에 직접 등록하는 방식과의 차이점이 궁금했다.
@Component 방식
이 방법은 스프링 컨테이너에 등록하는 방법이다. @Controller, @Service, @Repository 등 우리가 사용하던 애노테이션에 들어가 보면 정의되어 있는 방식으로, 스프링이 직접 관리하게 된다.
(스프링 컨테이너에 등록하는 방법은 @Configure를 클래스 상단에 붙이고, 메서드나 필드에 @Bean을 붙여서 등록도 가능한데, 이 점에 대해서는 추후에 기회가 되면 포스팅하도록 하겠다)
WebConfig에 직접 등록하는 방식
스프링 컨테이너 외부에서 관리하며, 빈으로 등록되지 않고, 빈 컨테이너와의 상호작용이 필요 없고, 명시적으로 커스터마이징하고 싶을 때 유용하다. 직접 등록하는 것은 2가지 방법이 있는데 configureHandlerExceptionResolver는 스프링이 기본으로 등록하는 ExceptionResolver를 대체하고, extendHandlerExceptionResolver는 ExceptionResolver를 확장하므로 extendHandlerExceptionResolver를 사용하는 것이 좋다.
다음과 같은 3가지 차이점이 있다.
1. 스프링 라이프사이클
스프링이 관리하기 때문에 다른 빈들과 상효작용이 가능하게 된다.
의존성 주입과 살짝 헷갈릴 수 있는데 간단한 예를 들면 빈 A가 빈 B에 의존할 때, 빈 A는 빈 B의 메서드를 호출할 수 있고, 빈 B의 변경 사항에 반응할 수 있다는 차이점이 있다.
2. 의존성 주입
스프링이 관리하기 때문에 의존성 주입을 할 수 있게 된다.
3. 명시성
WebMvcConfigurer를 사용하는 방식은 명시적이며, 어떻게 동작하는지 코드를 통해 쉽게 이해할 수 있다. @Component를 사용하는 경우, 약간 더 추상화된 수준에서 동작한다.
결론적으로, 사용하려는 HandlerExceptionResolver의 성격과 프로젝트의 요구 사항에 따라 적절한 방식을 선택하면 된다.
만약 의존성 주입이나 스프링 라이프사이클에 참여해야 한다면 @Component를 사용하고, 그렇지 않다면 WebMvcConfigurer의 extendHandlerExceptionResolvers 메서드를 사용하는 것이 좋을 수 있다.
돌아와서 어떠한 방식이던 등록하고 사용했을 경우에 아래와 같은 결과가 나온다.
ExceptionResolver 적용 후 결과
우리가 원하던 대로 400 오류가 나오게 된다.
반환 값에 따른 동작 방식
HandlerExceptionResolver의 반환 값에 따라 DispatchServlet의 동작 방식이 다르다.
빈 껍데기 ModelAndView: new ModelAndView()처럼 빈 ModelAndView를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.
ModelAndView 지정: ModelAndView에 View, Model 등의 정보를 지정해서 반환하면 View를 렌더링 한다.
null: null을 반환하면 다음 ExceptionResolver를 찾아서 실행한다. 만약 처리할 수 있는 ExceptionResolver가 없다면 기존과 같이 예외처리가 되고, 기존에 발생한 서블릿 밖으로 던진다.
코드 링크
https://github.com/toychip/StudySpring/commit/3b4f53a420293b18070b14d890aa7322fe6dbcef
Feat: Spring-mvc2-exception HandlerExceptionResolver 시작 · toychip/StudySpring@3b4f53a
toychip committed Aug 21, 2023
github.com
김영한님 mvc2 - Exception 강의로 학습하였습니다.
공부한 내용을 상기시키기고 해당 내용을 몰랐을 때의 입장에서 쉽게 배울 수 있지 않을까 해서 작성한 글입니다. 잘못된 내용이 있을 경우 말씀해 주세요. 피드백 환영합니다.
'Spring > Spring Exception' 카테고리의 다른 글
Spring Exception 8. Spring이 제공하는 @ResponseStatus, DefaultHandler (0) | 2023.08.19 |
---|---|
Spring Exception 7. Api에서 ExceptionResolver 활용 (0) | 2023.08.18 |
Spring Exception 5. 스프링 부트 Api 오류처리 (BasicErrorController) (0) | 2023.08.16 |
Spring Exception 4. Api 예외 처리 (Spring mvc VS RestfulApi) (0) | 2023.08.16 |
Spring Exception 3. 스프링 부트 오류페이지 View(BasicErrorController) (0) | 2023.08.15 |