Back-End/Spring

[Spring MVC2][API 예외처리] - 기본 예외처리, 서블릿 예외처리, 스프링이 지원하는 API 예외처리

얄루몬 2022. 6. 2. 12:43

💻본 포스팅은 '스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 김영한'님의 강의를 듣고 작성되었습니다.

https://inf.run/vQHp

 

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

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

www.inflearn.com


목차
1. 기본 예외처리
2. 서블릿 예외처리
3. 스프링 예외처리
4. 정상 코드와 예외 처리 코드의 분리 - @ControllerAdvice

기본 예외처리

  • HTML의 경우엔 오류 페이지만 있다면 대부분의 문제를 스프링 측에서 해결해준다. 그러나 API의 경우엔 이야기가 조금 달라지게 되는데 오류 화면을 보여주는 것으로 끝내는 문제가 아닌 각 오류 상황에 맞는 응답 스펙을 정해 JSON으로 데이터를 내려주어야 하기 때문에 API의 예외 처리는 조금 더 까다로운 문제이다.
  • 또한 스프링에서 제공하는 BasicErrorController를 확장해서 JSON 오류 메시지를 변경할 수는 있지만 이는 다른 방법을 사용해 처리하는 것이 훨씬 편리하고 쉽기 때문에 스프링 예외처리에서 살펴보도록 하겠다.
  • HTML 화면을 이용해 오류 페이지를 처리할 때는 'BasicErrorController'를 사용하도록 하고, API 처리 문제는 @ExceptionHandler를 사용하여 처리하도록 하자

서블릿 예외처리

예외, sendError( ) 발생 시 사용할 예외 페이지 경로 설정 클래스

package hello.exception;

import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

//서블릿이 제공하는 예외 발생 시 해당 페이지 제공
//@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        //RequestDispatcher 상수로 정의되어 있음


        //HTTP 상태 코드
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
        ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");

        //예외 발생 시
        ErrorPage errorPageEx = new ErrorPage(RuntimeException.class,"/error-page/500");

        factory.addErrorPages(errorPage404, errorPage500, errorPageEx);



    }
}
  • 서블릿 예외처리를 사용하기 위해서 WAS에 예외 전달, response.sendError()가 호출되면 위에 등록한 예외 페이지 경로가 호출되기 위한 클래스를 사용하자

API 예외 컨트롤러

package hello.exception.api;

import hello.exception.exception.BadRequestException;
import hello.exception.exception.UserException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

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

    @Data
    @AllArgsConstructor
    static class MemberDto{
        private String memberId;
        private String name;
    }
}
  • 정상처리 
    • JSON 형식으로 데이터가 정상 반환 된다.
    • 우리가 기대하는 것은 오류 발생 상황에도 JSON 형식으로 데이터를 반환하기를 바라는 것이다. (API의 오류 발생 상황)
  • 오류 발생
    • HTML이 반환된다. 
    • HTML 반환은 API의 경우에 기대하는 바가 아니다. 이 역시도 JSON 형식으로 반환되기를 기대한다.

API 응답을 위한 클래스

    @RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Map<String,Object>> errorPage500Api(
            HttpServletRequest request, HttpServletResponse response){
        log.info("API errorPage 500");
        Map<String, Object> result = new HashMap<>();
        Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);

        result.put("status", request.getAttribute(ERROR_STATUS_CODE));
        result.put("message", ex.getMessage());

        Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);

        return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
    }

 

스프링 예외처리

  • 스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공해준다.
  • 컨트롤러 밖으로 던져진 예외를 해결, 동작 방식을 변경하고 싶다면 HandlerExceptionResolver를 사용하면 된다 (이는 줄여서 ExceptionResolver이라고 한다.)

ExceptionResolver 우선순위

스프링 부트가 기본 제공하는 ExceptionResolver 우선순위는 다음과 같다.

  1. ExceptionHandlerExceptionResolver
    • @ExceptionHandler 을 처리한다.
    • API 예외 처리는 대부분 이 기능으로 해결한다.
  2. ResponseStatusExceptionResolver
    • HTTP 상태 코드를 지정해준다.
    • @ExceptionHandler을 사용해 API 오류를 처리해주면 HTTP 상태 코드를 200으로 정상 반환해주게 되는데 이럴 경우에도 직접 상태 코드 변경을 위해 @ExceptionHandler와 같이 사용하거나 단독으로 사용하거나 한다.
  3. DefaultHandlerExceptionResolver(우선 순위가 가장 낮음)
    • 스프링 내부의 기본 예외를 처리한다.
    • 파라미터의 바인딩 시점에 타입이 맞지 않으면 발생하는 TypeMismatchException이 발생하는데 이 오류는 바로 잡지 않으면 서블릿 컨테이너까지 오류가 올라가 결과적으로 500 오류를 발생시킨다.
    • 파라미터 바인딩의 문제는 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출(잘못된 값을 입력하는 경우)해서 발생하는 문제이기 때문에 이런 경우 HTTP에서는 상태 코드를 400으로 사용하도록 했다.
    • DefaultHandlerExceptionResolver는 위와 같이 파라미터 바인딩 문제가 발생하면 500 오류가 아닌 400오류로 변경해준다.

ExceptionResolver 활용

  • 예외 상태 코드변환
    • 상태 코드가 400인 오류를 500 등으로 바꿀 수 있다.
  • 뷰 템플릿 처리
    • ModelAndView에 값을 채워 예외에 따른 새로운 오류 화면 뷰 렌더링을 해서 고객에게 제공해준다.
  • API 응답 처리
    • HTTP 응답 바디에 직접 데이터를 넣어주는 것도 가능하다.

예외 발생 처리(2가지 방법)

  • WAS까지 예외를 던져 다시 /error를 호출
  • 중복 없이 컨트롤러 내부에서 예외 발생 처리 끝내기

ExceptionHandlerExceptionResolver을 사용해서 컨트롤러 내부에서 예외 발생 처리 끝내기

@Slf4j
@RestController
public class ApiExceptionV2Controller {
    /*컨트롤러 내부에서 터진 예외를 WAS까지 가지고 가지 않아서 컨트롤러에서 처리를 하는데
    * 이는 200 상태코드를 반환하게 한다. 그러나 이를 원하지 않고 http 상태 코드를 다른 것으로
    * 처리하고 싶다면 이때는 @ResponseStatus로 상태코드를 지정해주면 된다.*/
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e){
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("BAD", e.getMessage());
    }
    //@ExceptionHandler에 예외를 생략하면 메서드 파라미터로 예외를 지정합니다.
    @ExceptionHandler
    public ResponseEntity<ErrorResult> userExHandler(UserException e){
        log.error("[exceptionHandler] ex", e);
        ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
        return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);

    }
}
  • 이 경우엔 설정한 예외의 하위 자식 클래스까지 모두 처리가 가능하다.
  • 우선순위는 자식 클래스가 더 자세히 기록한 것으로 보아 부모 클래스 예외와 자식 클래스 예외가 있으면 자식 클래스 예외로 처리한다.
  • @ExceptionHandler에 예외를 생략하면 메서드 파라미터로 예외를 지정합니다.

정상 코드와 예외 처리 코드의 분리 - @ControllerAdvice

@ControllerAdvice

  • 정상 코드
  • 예외 처리 코드
  • @ControllerAdvice 또는 @RestControllerAdvice 를 사용한다
    • @RestControllerAdvice는 클래스 제일 상단부에 @RestController가 붙어있을 때 사용하며 둘의 기능은 동일하다.