스프링 부트 API 예외 처리 [2] HandlerExceptionResolver

최준호·2022년 5월 16일
0

Spring

목록 보기
26/47
post-thumbnail

이전의 Exception이 발생하면 무조건 500 에러로 떨어졌지만 api의 경우 발생 예외에 따라 상태코드를 처리하고 싶다.

@RestController
@Slf4j
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);
    }
}

코드와 같이 bad로 id값을 던졌을 때 IllegalArgumentException이 일어났지만 상태 코드는 500으로 나오는 것을 확인할 수 있다.

그래서 스프링은 HandlerExceptionResolver를 통해 예외가 던져진 경우 예외를 해결하고 동작을 새로 정의할 수 있는 방법을 제공한다.

👊ExceptionResolver

출처 김영한 강사님 스프링 MVC 2편

ExceptionResolver 적용 전

ExceptionResolver 적용 후

ExceptionResolver로 해결을 해도 PostHandler는 호출되지 않는다.

ExceptionResolver 구현

@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);	//에러는 따로 {}처리가 필요 없음
        }
        return null;
    }
}

HandlerExceptionResolver class를 상속받아 resolveException 메서드를 구현했다. 코드 내용은 Exception의 type을 확인하고 맞다면 response.sendError를 통해 에러에 대한 상태값과 메세지를 다시 전달할 수 있게 만든것이다.

또한 마지막에 반환하는 ModelAndView는 api에서는 따로 노출할 화면이 없어서 필요는 없지만 마치 정상적으로 반환해서 화면으로 넘어가듯이 처리하고자 비어있는 ModelAndView를 반환해주는 것이다. 따라서 에러는 표시되지만 Spring 자체적으로는 에러로 처리되지 않는다. 만약 여기서 ModelAndView에 화면 정보를 추가해주면 HTML을 렌더링하게 할수도 있다.

만약 Exception 타입이 맞는게 없어 null로 반환된다면 다른 추가된 ExceptionResolver를 찾게되고 더이상 ExceptionResolver를 찾을 수 없게 된다면 기존의 500으로 처리되듯이 처리된다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

	...

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

WebConfig에서 extendHandlerExceptionResolvers 메서드를 구현하여 HandlerResolverException을 구현해주면 된다.

configuredHandlerExceptionResolvers 메서드로도 구현할 수 있는데 해당 메서드로 사용하면 스프링이 기본으로 제공하는 ExceptionResolver가 제거되므로 주의하자.

동일하게 요청하면 이제 400 에러가 노출된다.

HandlerExceptionResolver로 API 예외처리

public class UserException extends RuntimeException{
    public UserException() {
        super();
    }

    public UserException(String message) {
        super(message);
    }

    public UserException(String message, Throwable cause) {
        super(message, cause);
    }

    public UserException(Throwable cause) {
        super(cause);
    }

    protected UserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

UserException exception을 정의하고

@RestController
@Slf4j
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("잘못된 요청");
        if(id.equals("user-ex")) throw new UserException("사용자 에러");
        return new MemberDto(id, "hello " + id);
    }
}

user-ex 값이 변수로 들어오면 UserException을 발생시키도록 처리했다.

@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {

    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try{
            if(ex instanceof UserException){
                log.info("UserException resolver to 400");
                String accept = request.getHeader("accept");
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);

                //json으로 반환할 경우
                if(MediaType.APPLICATION_JSON_VALUE.equals(accept)){
                    Map<String, Object> errResult = new HashMap<>();
                    errResult.put("ex", ex.getClass());
                    errResult.put("message", ex.getMessage());

                    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                    response.setCharacterEncoding("utf-8");
                    response.getWriter().write(mapper.writeValueAsString(errResult));
                    return new ModelAndView();
                }else{
                    //HTML로 반환할 경우
                    return new ModelAndView("error/500");
                }
            }
        }catch (IOException e){
            log.error("resolver ex", e);
        }

        return null;
    }
}

이전과 동일하게 HandlerExceptionResolver을 정의해준다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

	...

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

resolver에 추가해주고 테스트 해보자.

json type으로 반환할 경우 우리가 처리한대로 json으로 반환되고

html로도 반환할 수 있다.

ExceptionResolver를 하나의 예외를 처리할 때마다 이렇게 작성하려고 하니 너무 복잡하다. 스프링이 제공하는 ExcetpionResolver가 존재하는데 다음 글에서는 Spring이 제공해주는 ExceptionResolver에 대해 알아보자!

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글