본문 바로가기
IT/Spring boot

[Spring boot] HandlerExceptionResolver Custom 예외 처리

by kyu-nahc 2024. 8. 16.
반응형

HandlerExceptionResolver

이 전 포스팅에서 Spring에서 Default 예외 처리 및 예외 처리 추상화를 위한 

HandlerExceptionResolver 인터페이스를 소개하였다. 

해당 인터페이스는 발생한 Exception을 catch 하고 HTTP 상태나 응답 메시지 등을 설정한다.

 

[Spring boot] Spring boot의 예외 처리 방법

Spring boot Default 예외 처리예외 처리는 웹 애플리케이션을 만드는 데 있어서 중요한 부분을 차지한다.Spring Framework는 다양한 예외 처리 방법을 제공하고 있다.그중에서 먼저 기본적인 예외 처리

kyu-nahc.tistory.com

 

Spring MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고,

동작을 새로 정의할 수 있는 방법HandlerExceptionResolver를 통해 제공한다.

컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶은 경우,

HandlerExceptionResolver를 사용하면 된다.

아래는 HandlerExceptionResolver 인터페이스 코드이다.

public interface HandlerExceptionResolver {

    @Nullable
    ModelAndView resolveException(HttpServletRequest request,
    HttpServletResponse response, @Nullable Object handler, Exception ex);
}

 

Object 타입인 handler예외가 발생한 컨트롤러 객체이다.

그리고 Exception 객체 ex컨트롤러(핸들러)에서 발생한 예외 객체이다.

예외가 발생할 경우 디스패치 서블릿까지 전달되는데, 

Spring은 예외 처리를 위해 HandlerExceptionResolver 구현체들을 Bean으로 등록하여 관리한다.

그리고 적용 가능한 구현체를 찾아 예외 처리를 진행하게 된다.

 

먼저 적용 가능한 구현체가 아닌 직접 HandlerExceptionResolver를 구현해 보면 다음과 같다.

HandlerExceptionResolver를 구현하는 구현체 클래스를 먼저 만들어준다.

@Slf4j
public class CustomExceptionResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler, Exception ex) {
        if (ex instanceof NoSuchElementException){
            try {
                response.sendError(HttpServletResponse.SC_NOT_FOUND,ex.getMessage());
            } catch (IOException e) {
                log.info("Resolve Exception",e);
            }
            return new ModelAndView();
        }
        return null;
    }
}

 

HandlerExceptionResolver 인터페이스를 상속받아 resolveException 메소드를 구현한다.

코드 내용은 Exception의 type을 확인하고 맞다면 response.sendError를 통해

에러에 대한 상태값과 메시지를 다시 전달할 수 있게 만든 것이다.

 

또한 마지막에 반환하는 ModelAndView는 Rest API에서는 따로 노출할 화면이 없어서 필요는 없지만,

정상적으로 반환해서 화면으로 넘어가듯이 처리하고자 비어있는 ModelAndView를 반환해 준다.

따라서 에러는 표시되지만 Spring 자체적으로는 에러로 처리되지 않는다.

만약 여기서 ModelAndView에 뷰 정보를 추가해 주면 HTML을 렌더링 하게 할 수도 있다.

만약 Exception 타입이 맞는 게 없어 null로 반환된다면 다른 추가된 ExceptionResolver를 찾게 되고

더 이상 ExceptionResolver를 찾을 수 없게 된다면 기존의 500으로 처리되듯이 처리된다.

여기서는 테스트를 위해 404 Not Found 에러를 전달하도록 한다.

@Configuration
@RequiredArgsConstructor
public class WebMvcConfiguration implements WebMvcConfigurer {

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

}

 

 

WebMvcConfigurer를 구현하는 구현체에서 

extendHandlerExceptionResolvers 메소드를 구현하여

HandlerResolverException의 구현체인 CustomExceptionResolver를 등록해 준다.

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

@RestController
@RequestMapping("/public")
@RequiredArgsConstructor
public class TestController {

    private final PostService postService;

    @GetMapping("/post/{id}")
    public ResponseEntity<Post> getPostProcess(@PathVariable Long id) {
        return ResponseEntity.ok(postService.getPost(id));
    }
}

 

getPostProcess()에서 NoSuchElementFoundException 예외를 발생시키기 위해서,

존재하지 않는 id로 Entity를 찾도록 한다.

아래 왼쪽 사진은 CustomExceptionResolver를 추가하기 전이고,

오른쪽 사진은 CustomExceptionResolver를 추가한 후이다. 

동일하게 요청할 경우 CustomExceptionResolver를 등록하면 이제 404 에러가 노출된다.

 

 

HandlerExceptionResolver를 이용한 API 예외 처리

 

이제 RuntimeException을 상속받는 Custom 예외를 정의하고,

일정 값 이상의 id 변수를 uri를 통해 전달하면, PostException이 발생하도록 처리하는 예제이다.

public class PostException extends RuntimeException{
    
    public PostException(){
        super();
    }
    public PostException(String message){
        super(message);
    }
    public PostException(String message, Throwable cause){
        super(message, cause);
    }
    public PostException(Throwable cause){
        super(cause);
    }
    protected PostException(String message, 
    	Throwable cause, boolean enableSuppression, boolean writableStackTrace){
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

 

먼저 RuntimeException을 상속받는 PostException을 정의한다.

@Slf4j
public class PostExceptionResolver implements HandlerExceptionResolver {

    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    public ModelAndView resolveException(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler,
            Exception ex) {
        try{
            if (ex instanceof PostException) {
                String accept = request.getHeader("accept");
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                Map<String,Object> errorMap = new HashMap<>();
                if (MediaType.APPLICATION_JSON_VALUE.equals(accept)){

                    errorMap.put("ex", ex.getClass());
                    errorMap.put("uri",request.getRequestURI());
                    errorMap.put("message",ex.getMessage());
                    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                    response.setCharacterEncoding("UTF-8");
                    response.getWriter().write(mapper.writeValueAsString(errorMap));
                    return new ModelAndView();
                }else{
                    errorMap.put("timestamp",new Date());
                    errorMap.put("path",request.getRequestURI());
                    errorMap.put("message",ex.getMessage());
                    errorMap.put("status",HttpServletResponse.SC_NOT_FOUND);
                    errorMap.put("trace",ex.getStackTrace());
                    errorMap.put("exception",ex.getClass());
                    errorMap.put("error",ex);
                    return new ModelAndView("error/500",
                          errorMap);
                }
            }

        }catch (IOException e){
            log.info("Resolver Error",e);
        }
        return null;
    }
}

 

위의 예시와 동일하게 HandlerExceptionResolver를 구현하는 구현체를 정의한다.

MediaType을 확인하여, type이 application/json일 경우 json type으로 반환하고,

아닐 경우 전 포스트에서 구현한 뷰 템플릿에 에러 정보들을 모델 객체로 추가하여, 반환하도록 한다.

@Configuration
@RequiredArgsConstructor
public class WebMvcConfiguration implements WebMvcConfigurer {

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

}


@RestController
@RequestMapping("/public")
@RequiredArgsConstructor
public class TestController {

    private final PostService postService;

    @GetMapping("/post/{id}")
    public ResponseEntity<Post> getPostProcess(@PathVariable Long id) {
        if (id >= 30){
            throw new PostException("Not Found Post");
        }
        return ResponseEntity.ok(postService.getPost(id));
    }
}

 

getPostProcess()에서는 id가 30 이상일 경우 PostException이 발생하도록 구현하고,

List <HandlerExceptionResolver> 객체에 새로 만든 PostExceptionResolver를 추가한다.

그리고 포스트맨을 통해 application/json type으로 요청 시,

아래 왼쪽화면처럼 json type으로 에러 객체 정보를 반환하게 된다.

또한 json type 아닐 경우 오른쪽 화면처럼 Custom View 템플릿을 보여준다.

 

HandlerResolverException를 사용하면 Controller에서 예외가 발생하여도,

WAS까지 예외가 전달되는 것이 아닌, HandlerResolverException의 구현체에서 예외를 처리한다.

즉 예외가 발생하여도 서블릿 컨테이너까지 예외가 전달되지 않고,

Spring MVC에서 예외처리가 모두 완료되고, WAS 입장에서는 모두 정상 처리가 된 것이다.

 

하지만 ExceptionResolver를 하나의 예외를 처리할 때마다,

HandlerResolverException를 구현하는 구현체를 만드는 것은 너무 복잡하다.

따라서 위에서 잠깐 설명한 것처럼 Spring은 적합한 예외 처리를 위해

HandlerExceptionResolver 구현체들을 만들고, Bean으로 등록하여 관리를 직접 하고 있다.

우선순위대로 아래의 4가지 구현체들이 Bean으로 등록되어 있다.

  • DefaultErrorAttributes
    에러 속성을 저장하며 직접 예외를 처리하지는 않는다.
  • ExceptionHandlerExceptionResolver
    에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리
  • ResponseStatusExceptionResolver
    Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리
  • DefaultHandlerExceptionResolver
    스프링 내부의 기본 예외들을 처리

이에 대한 자세한 내용은 아래 포스팅을 참고하자.

 

[Spring boot] Spring의 다양한 에러 처리

HandlerExceptionResolver이전 포스트에서 HandlerResolverException를 구현하는 구현체를 만든 후,직접 Media Type에 따라 에러 처리에 대한 뷰 템플릿 및 데이터 반환을 진행하였다.하지만 모든 예외에 대해서

kyu-nahc.tistory.com

 

 

참고자료

https://mangkyu.tistory.com/204

https://develop-writing.tistory.com/100

https://hoonsmemory.tistory.com/16

 

 

 

반응형

loading