Framework/Spring

[Spring] - 스프링에서 예외처리를 위한 'HandlerExceptionResolver' 알아보기

leegeonwoo 2024. 9. 7. 00:19

 

 

[Spring] - API예외 처리를 이해하기위한 기본 개념!

이 포스팅은 이전에 작성한 아래 포스팅을 읽었다는 가정하에 작성한다.이전포스팅을 읽어야만 현재 포스팅이 이해되는 것은 아니지만 글의 흐름을 이전포스팅과 연관지어서 작성하며 코드를

lee-dev-log.tistory.com

 

 

[Spring] 쉬우면서 정확하게 익혀보는 필터와 인터셉터의 예외처리 흐름과 예외 페이지 응답

스프링을 사용하지 않는 순수 서블릿 컨테이너는 Exception과 response.sendError(Http상태코드, 오류메시지)두 가지 방식으로 예외를 처리한다.  Exception으로 처리하기기본적으로 자바는 예외가 발생하

lee-dev-log.tistory.com

먼저 이 포스팅을 읽기전에는 이전에 작성했던 위의 포스팅을 읽은 후에 읽는 것을 추천한다.

이해하기 수월할 뿐 아니라 이전 포스팅을 읽었다는 가정 하에 포스팅을 하였다.

 

 

 

HandlerExceptionResolver의 정확한 동작원리를 알아보기전에 예외가 발생했을 때의 DispatcherServlet의 흐름을 알아보자.

  • HandlerExceptionResolver는 줄여서 ExceptionResolver라고도 부른다.
  • 예외가 발생했다는 것은 내가 구현한 비즈니스 레이어, 퍼시스턴스 레이어 등에서 예외가 발생하여 프레젠테이션계층 (컨트롤러)까지 해당 예외가 올라온 것을 말한다.

 

 

ExceptionResolver가 적용안된 상태에서 예외발생

  1. 클라이언트에서 서버로 HTTP 요청
  2. WAS에서 요청을 받아 DispatcherServlet으로 요청을 보냄
  3. 리소스 요청경로를 매핑
  4. 해당 매핑에 맞는 핸들러어댑터(컨트롤러)를 호출
  5. 호출한 컨트롤러에서 예외발생!
  6. 컨트롤러에서 발생한예외를 다시 DispatcherServlet으로 전달
  7. 예외가 발생했기때문에 postHandler메서드는 동작하지 않고 afterCompletion메서드 호출
  8. WAS로 예외를 전달

 

 

ExceptionResolver가 적용된 상태에서 예외발생

  1. 클라이언트에서 서버로 HTTP요청
  2. DispatcherServlet에서 요청을 받고 핸들러(컨트롤러)를 매핑
  3. 호출한 컨트롤러에서 예외발생!
  4. 컨트롤러에서 발생한 예외를 다시 DispatcherServlet으로 전달
  5. 발생한 예외를 ExceptionResolver가 해결 - 예외처리
  6. afterCompletion메서드 동작
  7. HTML응답
  8. 최종적으로 WAS는 예외가 없기때문에 정상흐름으로 동작하게 된다!

 

 

위 내용을 단순하게 생각하면 ExceptionResolver는 예외가 발생하면 해당 예외를 WAS를 통해 응답이 나가기전에 때려잡아주는 역할을 수행한다.

 

 

이제 HandlerException코드를 난이도 쉬움 버전으로 구현해보고 동작을 이해해보자!

먼저 기존 ApiExceptionController컨트롤러에 "bad"라는 파라미터가 들어올 경우 예외를 발생시키면서 "잘못된 입력값"이라는 예외 메시지를 담은 예외를 만든다.
참고로 RuntimeException이던 IllegalArgumentException이던 모든 예외는 WAS입장에서는 그저 서버에서 무언가 잘못된 것으로 인식하기때문에 500상태코드가 나가게된다.

개발자 입장에서 IllegalArgumentException예외는 사용자가 무언가 입력값을 잘못 넣었다고 생각할 수 있기때문에 우리는 500상태코드로 응답되는 IllegalArgumentException을 400BadRequest가 응답되도록 바꿔볼 것이다.

 

@Slf4j  
@Controller  
public class ApiExceptionController {  

    @GetMapping("/api/members/{id}")  
    public MemberDto getMember(@PathVariable("id") String id) {  

        if (id.equals("ex")) {  
            throw new RuntimeException("잘못된 사용자");  
        }  

        if (id.equals("bad")) {  
            throw new IllegalArgumentException("잘못된 입력값");  
        }  

        return new MemberDto(id, "hello " + id);  
    }

HandlerExceptionResolver를 구현한 MyHandlerExceptionResolver를 만들어주자.

 

@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");  
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());  
                return new ModelAndView();  
            }  
        } catch (IOException e) {  
            log.error("resolver ex", e);  
            e.printStackTrace();  
        }  

        return null;  
    }  
}

resolverException메서드를 오버라이드 할 수 있는데,
반환타입을 보면 ModelAndView를 반환하는 것을 볼 수 있다.
분명 API예외를 처리하기로했는데 뷰를 반환해도 괜찮나? 라는 의문을 가질 수 있지만 그 부분은 뒤에서 설명한다.

오버라이드한 메서드의 파라미터중 Exception이 있는것을 볼 수 있다.
이 파라미터의 의미는 이런 예외를 처리할거에요라는 의미로 해석하면된다
바로 코드를 보면 이해할 수 있는데 ex필드가 만약 IllegalArgumentException일 경우 응답객체의 sendError를 담는 것을 확인할 수 있다.
sendError에 원하는 변경하고자하는 상태코드를 담고 선택적으로 메시지까지 담을 수 있다.

이렇게 설계하면 이전에 우리가 학습했던대로 WAS가 예외를 인지하면 해당 예외를 처리할 수 있는지 sendError를 뒤져보고 일치하는 예외 또는 상태코드가 있다면 다시 컨트롤러로 해당 예외를 처리하도록 보내고 해당 예외에 맞는 응답을 보내게 되는 것이다.

그리고나서 빈 ModelAndView를 리턴해주고있는데
반환된 ModelAndView의 값에 따라 DispatcherServlet의 동작방식이 달라진다.

  • ModelAndView: 뷰를 렌더링하지 않고 정상흐름으로 서블릿이 리턴
  • ModelAndView를 지정: 지정한 모델을 담은 뷰를 렌더링한다.
  • null: 널을 반환하면 다음 ExceptionResolver를 찾아보고 처리할 수 없을 경우 기존에 발생한 예외를 서블릿 밖으로 던진다.

 

JSON형태로 API응답을 한다면 빈 ModelAndView를 반환하면된다.

그리고나서 WebConfig클래스에

@Override  
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {  
    resolvers.add(new MyHandlerExceptionResolver());  
}

구현한 ExceptionResolver를 등록해주면 우리가 원하는대로 예외를 처리할 수 있다!

 

 

POSTMAN을 이용하여 테스트해보자.

localhost:8080/api/members/bad 이 경로로 GET요청을 보내보자.

응답결과

{
"timestamp": "2024-09-05T13:17:09.874+00:00",
"status": 400,
"error": "Bad Request",
"exception": "java.lang.IllegalArgumentException",
"path": "/api/members/bad"
}
  • JSON형태로 응답하며, 상태코드가 400으로 BadReqeust가 반환된 것을 확인할 수 있다.
  • resolveException에서 예외를 처리했기때문에 서버로그에도 예외가 남지않는다!

이번에는 null을 리턴해보자!!
똑같이 localhost:8080/api/members/bad 이 경로로 GET요청을 보내보자

실행결과

{
"timestamp": "2024-09-05T13:18:16.976+00:00",
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.IllegalArgumentException",
"path": "/api/members/bad"

}

위와 같이 sendError가 적용되지 않으며 500 Unternal Server Error가 응답된 것을 확인할 수 있다.

또한 서버로그를 확인하면 아래와 같이 예외가 찍혀있는 것을 확인할 수 있다. 즉 예외가 정상적으로 처리되지 않은 것이다.

 

728x90