본문 바로가기
Spring

[Spring] 스프링이 제공하는 ExceptionResolver에 대하여

by 개미가되고싶은사람 2024. 7. 8.

목차

    Spring의 기본 예외 처리

      - 스프링이 제공하는 기본 오류 방식 BasicErrorController에 대해서 알아보자!!!

     

     

    BasicErrorController 코드

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    
    	HttpStatus status = getStatus(request);
    	Map<String, Object> model = Collections
    		.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
    	response.setStatus(status.value());
    	ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    	return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
        
     }
    
    
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    
    	HttpStatus status = getStatus(request);
    	if (status == HttpStatus.NO_CONTENT) {
    		return new ResponseEntity<>(status);
    	}
    	Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
    	return new ResponseEntity<>(body, status);
        
    }
    • errorHtml():   Accept 해더 값이 text/html 요청인 경우 처리합니다.
    • error():  그외 경우에 호출되고 ResponseEntity 로 HTTP Body에 JSON 데이터를 반환합니다. 

     

    BasicErrorController의 오류 페이지 처리 순서

    1. templates폴더에 error폴더
    2. static 폴더에 error폴더
    3. error라는 폴더가 없으면 templates/error.html를 찾는다

    templates, static 폴더에서도 html 파일 명이 더 자세한 오류 페이지가 우선 순위가 높습니다.

    ex) resources/static/error/404.html > resources/static/error/4xx.html

     

     

    여기서 추가 TMI 💁‍♂️💁‍♂️

    스프링은 기본적으로 오류 발생시 BasicErrorController가 /error이라는 오류 페이지로 요청합니다.  당연히 이 경로도 수정 가능합니다.

     

     

    스프링은 BisicErrorController만 제공하는 것이 아니라 왜? ExceptionResolver도 제공하는 것일까?? 🎁🎁

     

     

    스프링 부트가 제공하는 BasicErrorController는 HTML 페이지를 제공하는 경우에는 매우 편리합니다. 그런데 API 오류 처리는 API 마다, 각각의 컨트롤러나 예외마다 서로 다른 응답 결과를 출력해야 할 수도 있다. 이런 이유 때문에 BasicErrorController를 가지고 API 예외처리를 하는 것은 별로 좋지 않습니다. 
    예를 들어서 회원과 관련된 API에서 예외가 발생할 때 응답과, 상품과 관련된 API에서 발생하는 예외에 따라 그 결과가 달라져야 한다.

     

     

     

    1. ResponseStatusExceptionResolver 

    일반적으로 예외가 터지면 WAS 서버는 HTTP 500 상태 코드로 처리하는데 이 상태 코드를 변경할 수 있습니다. 해당 ExceptionResolver로 예외에 따라서 HTTP 상태코드를 지정할 수 있습니다.

     

    자세한 설명은 아래 블로그를 참고해주세요

    https://pjstudyblog.tistory.com/31

     

    WAS 서버의 예외 처리 이해

    웹 애플리케이션에 동작 흐름을 대충 보면 아래와 같이 흘러가게 됩니다.WAS(여기까지 전파) → 필터 → 서블릿 → 인터셉터 → 컨트롤러(예외발생)  웹 애플리케이션에서는 사용자 요청별로 쓰

    pjstudyblog.tistory.com

     

     

    ResponseStatusExceptionResolver 사용 방법

     

     

    1. @ResponseStatus

    @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
    public class BadRequestException extends RuntimeException {
    
    }

    2. ResponseStatusException예외

    @GetMapping("/response-status-ex1")
    public String responseStatusEx1(){
        throw new BadRequestException();
    }

     

     

    @ResponseStatus 핵심 로직

    @ResponseStatus핵심 로직은 applyStatusAndReason메서드입니다.

    protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
    		throws IOException {
    
    	if (!StringUtils.hasLength(reason)) {
    		response.sendError(statusCode);
    	}
    	else {
    		String resolvedReason = (this.messageSource != null ?
    				this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
    				reason);
    		response.sendError(statusCode, resolvedReason);
    	}
    	return new ModelAndView();
    }

    해당 코드를 보면 reason에 여부따라 반환 값이 달라지지만 HTTP 상태코드와  메시지를 반환합니다. 그리고 결론적으로 빈 ModelAndView를 반환합니다.

     

     

     

    ResponseStatusException의 단점

    @ResponseStatus는직접 변경할 수 없는 예외에는 적용할 수 없습니다. (애노테이션을 직접 넣어야 하는데, 내가 코드를 수정할 수 없는 라이브러리의 예외 코드 같은 곳에는 적용할 수 없다.) 추가로 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하는 것도 어렵습니다. 

     

     

     

    2. DefaultHandlerExceptionResolver

    DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결합니다.

    대표적으로 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException이 발생합니다.

    그리고 WAS서버까지 예외가 전달되면 WAS서버에서는 500 상태코드를 반환하는데 DefaultHandlerExceptionResolver는 이것을 상태코드 500 오류를 반환 하는게 아니라 HTTP 상태 코드 400 오류로 변경합니다.

     

     

    DefaultHandlerExceptionResolver의 핵심 로직

    	@Override
    	@Nullable
    	protected ModelAndView doResolveException(
    			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
    
    		try {
    
    			if (ex instanceof ConversionNotSupportedException theEx) {
    				return handleConversionNotSupported(theEx, request, response, handler);
    			}
    			else if (ex instanceof TypeMismatchException theEx) {
    				return handleTypeMismatch(theEx, request, response, handler);
    			}
    
    		}
    		catch (Exception handlerEx) {
    			if (logger.isWarnEnabled()) {
    				logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
    			}
    		}
    
    		return null;

    원래 작성된 코드는 더 많은 예외 처리가 있지만 위에 TypeMismatchException를 예시로 들어서 TypeMismatchException랑 if절만 작성했습니다.

     

     

    DefaultHandlerExceptionResolver관련 공식 문서

    해당 ExceptionResolver는 공식 문서를 참고만 해도 많은 도움이 될 것 같아서 링크 공유합니다~~!!

    https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html

     

    DefaultHandlerExceptionResolver (Spring Framework 6.1.10 API)

    pageNotFoundLogger protected static final Log pageNotFoundLogger Additional logger to use when no mapped handler is found for a request. See Also:

    docs.spring.io

     

     

     

     

    3. ExceptionHandlerExceptionResolver

    특정 예외를 처리하고 해당 예외는 Spring 내에서 처리됩니다. 이렇게 하면 예외가 서블릿 컨테이너까지 전파되는 것을 방지하고 요청 처리가 제어되고 정상적인 흐름으로 끝납니다. 특정 컨트롤러에서 발생하는 예외를 별도로 처리하기 쉽기 때문에 API 예외처리할 때 상당히 편리하게 처리할 수 있습니다.

     

     

     

    @ExceptionHandler

    • 우선 순위는 자식 클래스가 우선권을 가진다.
    • 부모 클래스는 자식 클래스까지 처리할 수 있다.
    • 예외를 생략할 수 있다. 생략하면 메서드 파라미터의 예외가 지정된다.

     

     

    @ExceptionHandler 장점

    • 애플리케이션 전체에서 예외를 일관되게 처리할 수 있습니다.
    • 로직과 예외 처리를 분리함으로써 코드 가독성을 높이며, 편리하게 유지 보수할 수 있습니다.
    • 예외 발생 시 오류 페이지 관리하기 편리합니다.

     

     

     

    @ExceptionHandler 예외 처리 방법

    @ExceptionHandler 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 됩니다. 해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출된다. 참고로 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있습니다.

    @ExceptionHandler(부모예외.class) 
    public String 부모예외처리()(부모예외 e) {
    } 
    
    @ExceptionHandler(자식예외.class) 
    public String 자식예외처리()(자식예외 e) {
    }

     

    @ControllerAdvice

    @ControllerAdvice는 애플리케이션에서 예외를 처리할 수 있는 애노테이션입니다.

     

     

     

    @ControllerAdvice 장점

    • 애플리케이션 전체에서 예외 처리가 일관되게 처리할 수 있습니다
    • 로직과 예외 처리를 분리함으로써 코드 가독성을 높이며, 편리하게 유지 보수할 수 있습니다.

     

     

    @ControllerAdvice 사용 예시

    // 특정 애노테이션의 모든 컨트롤러를 대상 
    @RestController @ControllerAdvice(annotations = RestController.class) 
    public class Example1 {
    }
    
    // 특정 패키지 내의 모든 컨트롤러를 대상
    @ControllerAdvice("org.example.controllers") 
    public class Example2 {
    } 
    
    // 특정 컨트롤러를 대상
    @ControllerAdvice(assignableTypes = {ControllerInterface.class,  AbstractController.class}) 
    public class Example3 {
    }