SPRING/Spring MVC

[SpringMVC] API 예외처리하기(2) - HandlerExceptionResolver

IT록흐 2023. 9. 12. 22:33
반응형

 

 

컨트롤러에서 에러가 발생하면 복잡해진다. 

 

클라이언트 요청 -> WAS -> 컨트롤러 (에러발생) -> WAS -> 컨트롤러 -> 클라이언트 응답

 

 

[SpringMVC] API 예외처리하기(1) - BasicErrorController

위 그림은 Spring에서 에러가 발생했을 때 처리되는 과정이다. 에러가 발생했을 때 try-catch문으로 Exception을 잡지 않으면 에러는 WAS까지 전달된다. WAS는 에러의 종류를 확인하고 그에 따른 요청을

lordofkangs.tistory.com

 

그래서 지난 포스팅에서는 위 과정을 자동화 해주는 스프링부트의 BasicErrorController에 대해서 알아보았다. 그러나 API 요청은 다양하고 그에 따라 다양한 에러 응답이 필요하다. BasicErrorController는 404,500,403 에러 페이지 같은 몇가지로 정해져 있는 응답에 반응하기 좋다. API처럼 다양한 에러 응답이 필요한 경우 부적절하다. 

 

BasicErrorController에서 에러를 핸들링 하지 않으려면 에러가 WAS까지 전파되면 안 된다. 

 

예를 하나 들어보자.

 

Controller

@Slf4j
@RestController
public class ApiExceptionController {

    @GetMapping("/api/members/{id}")
    public MemberDto getMember(@PathVariable("id") String id){
    
        if(id.equals("ex")){
            throw new RuntimeException("잘못된 사용자");
        }

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

}

 

컨트롤러 메소드가 하나 있다. 

 

id에 정상값이 들어오면 MemberDto가 반환되어 JSON 형태로 응답하는 컨트롤러 메소드이다. 그런데 만약 id로 ex가 들어오면 RuntimeException을 발생시킨다. 

 

RuntimeException이 발생하면 Controller는 throw으로 에러를 서블릿에 던진다. 서블릿도 에러를 WAS로 던지고 WAS는 RuntimeException을 처리하는 요청을 다시 서블릿으로 보낸다. 서블릿은 요청과 매핑되는 컨트롤러인 BasicErrorController의 한 메소드를 호출하고 메소드는 처리되어 오류 응답을 클라이언트에 전달한다. 

 

이 과정은 굉장히 길고 다양한 응답을 할 수 없다. 

 

그러므로 WAS에 에러가 전파되기 전에 Exception을 처리할 해결사가 필요하다.

그것이 ExceptionResolver이다.

 

 

HandlerExceptionResolver

 

 

 

 

Controller에서 에러가 발생하면 에러는 디스패처 서블릿으로 전파된다. 디스패처 서블릿은 WAS로 에러를 전파하기 전에 에러를 핸들링할 수 있는 ExceptionResolver가 존재하는지 탐색한다. 만약 에러를 핸들링하고 에러정보를 정상적으로 클라이언트에게 전달할 수 있다면 뷰는 정상적으로 렌더링되고 디스패처 서블릿은 오류가 발생했음에도 WAS에게 정상응답을 보고한다. 

 

그럼 Spring이 탐색하는 ExceptionResolver에 직접 만든 ExceptionResolver 구현체를 추가해보자. 

 

HandlerExceptionResolver 구현체

@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver  {

    private ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try{
            //RuntimeException이 발생한 경우
            if(ex instanceof RuntimeException){
                log.info("UserException resolver to 400");
                
                //STEP1) 500에러를 400에러로 변환
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);

                if("application/json".equals(request.getHeader("accept"))){

                    //STEP2) JSON으로 변환시킬 HashMap 객체 만들기
                    Map<String,Object> errorResult = new HashMap<>();
                    errorResult.put("ex",ex.getClass());
                    errorResult.put("message",ex.getMessage());
                    String result = objectMapper.writeValueAsString(errorResult);

                    //STEP3) HTTP Body 영역에 write하기
                    response.setContentType("application/json"); //JSON 타입으로 설정하기
                    response.setCharacterEncoding("utf-8");
                    response.getWriter().write(result);

                    //STEP4) 비어있는 ModelAndView 반환하여 에러 전파 방지
                    return new ModelAndView();

                }else{
                    return new ModelAndView("error/500"); // application/json 타입이 아니면 에러페이지로 응답
                }

            }

        } catch (IOException e){
            log.error("resolver ex",ex);
        }
        return null;
    }

}

 

Spring이 ExceptionResolver를 탐색할 수 있도록 HandlerExceptionResolver 인터페이스의 구현체를 만들어 보았다.

 

과정은 4단계이다. 

 

STEP1) RuntimeException이 발생하면 에러코드를 500에서 400으로 변환한다. 

 

서버 안에서 발생한 에러이지만 원인은 클라이언트가 id를 잘못 입력한 것이므로 400에러를 반환한다. 

 

STEP2) HTTP의 Accept 헤더가 application/json인 경우 HashMap에 에러데이터를 담는다. 

 

Accept 헤더에는 클라이언트가 서버에게 어떤 유형의 데이터로 응답하기를 원하는지를 담고 있다. application/json은 JSON 형식으로 응답하기를 원하는 것이므로, JSON으로 변환할 수 있는 HashMap 객체에 에러데이터를 담는다. 

 

STEP3) HTTP Body 영역에 application/json 형식으로 write하도록 설정하기 

 

STEP4) 비어있는 ModelAndView 반환하기 

 

비어있는 ModelAndView가 반환되면 뷰템플릿은 뷰를 렌더링하지 않는다. 뷰를 렌더링하지 않고 응답을 마무리 하므로써 에러가 WAS까지 전파되지 않도록 막을 수 있다.  

 

 

WebMvcConfigurer 


@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new UserHandlerExceptionResolver()); // ExceptionHandler 등록
    } 

}

 

그럼  위 코드처럼 구현체를 Spring이 탐색할 수 있도록 등록하고 포스트맨으로 테스트를 해보자. 

 

 

요청URL : localhost:8080/api/members/ex

 

ex를 id로 요청하여 RuntimeException을 발생시켜보았다. 이때 Accept 헤더를 application/json으로 설정하여 JSON 형태로 응답하도록 요청해보자. 

 

 

위 사진같은 응답결과를 받았다.

 

HashMap에 저장한 에러정보만 JSON 형식으로 반환되었다. 그리고 500에러를 400에러로도 변경하였다. 이렇듯 ExceptionHandler를 이용하면 API가 어떤 요청을 하느냐에 따라, 어떤 에러이냐에 따라 다양한 데이터를 다양항 방식으로 전달할 수 가 있다. 

 

하지만 한 가지 단점이 있다. 

 

위 코드를 보면 알 수 있겠지만 ExceptionHandler를 작성하는 코드는 복잡하고 에러의 종류에 따라 반복적으로 작성해야 한다. 또한 마지막에 ModelAndView를 반환하는 것은 API 응답과 개념이 일치하지 않다. 

 

그래서 

 

SpringBoot는 개발자가 ExceptionHandler를 보다 편하게 사용가능하도록 이미 대부분의 ExceptionHandler를 구현해놓았고 이를 활용할 수 있도록 기능을 제공하고 있다.  

 

다음 포스팅에서는 SpringBoot가 제공하는 Exception 핸들링에 대해서 다루어 보겠다. 

 

 

 


 

 

참고자료

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

 

 

반응형