본문 바로가기
Spring Framework/Spring

스프링 MVC - 예외처리 (REST API)

by 도쿠니 2022. 6. 9.

JAVA에서는 예외 처리를 하기 위해서 try-catch문을 사용해 예외를 처리합니다.

 

프로젝트를 진행하다보면 수많은 예외상황이 발생하기 때문에 try-catch문이 거의 모든 코드에 들어가게 되는데 이는 코드의 가독성을 떨어뜨리는 요소 중 하나입니다.

 

Spring에서는 이러한 문제를 해결하기 위해 에러 처리라는 공통 관심사(Cross-cutting Concerns)를 메인 로직에서 분리하여 처리하고자 하였고, 이를 위해서 예외 처리 전략을 추상화한 HandlerExceptionResolver 인터페이스를 고안하였습니다.

 

public interface HandlerExceptionResolver {
    ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}

Handler(Controller)에서 Exception이 발생하면 Dispatcher Servlet 까지 전달됩니다.

Dispatcher Servlet은 상황에 따라 알맞은 예외 처리를 하기 위해 HandlerExceptionResolver 구현체들을 빈으로 등록하여 리스트로 관리하고 알맞은 구현체를 찾아 예외를 처리합니다.

 

기본적으로 네 종류의 구현체들이 빈으로 등록되어있습니다.

  • DefaultErrorAttributes
    • 에러 속성을 저장하며 직접 예외를 처리하지는 않습니다.
  1. ExceptionHandlerExceptionResolver
    • Controller나 ControllerAdvice에 있는 @ExceptionHandler에 의한 예외를 처리합니다.
  2. ResponseStatusExceptionResolver
    • @ResponseStatusResponseStatusException에 의한 예외를 처리합니다.
  3. DefaultHandlerExceptionResolver
    • 기본 스프링의 예외들을 변환하여 HTTP Status Code로 변환합니다.

이 중 직접적으로 예외를 처리하는 3가지를 HandlerExceptionResolverComposite로 모아서 관리합니다.

 

ExceptionResolver 우선순위는 DefaultErrorAttributes를 제외한 위에서부터 순서대로 우선순위가 높습니다.

 


예외처리 방식

@ResponseStatus

  • 예외 발생 시, HTTP Status Code를 지정하여 응답해줍니다.
    • 추가적으로 HTTP Status Code를 반환하는 방법 중 하나는 ResponseEntity로 보내는 법이 있습니다.
  • 적용 가능한 곳
    • Exception 클래스
      • 클래스 단위에 @ResponseStatus를 설정하면 해당 예외가 발생 시, 설정한 HTTP 상태 코드를 반환합니다.
      • 이 방식은 예외 상황마다 클래스를 추가해야하고, 에러 응답의 내용(Payload)를 수정할 수 없으며 예외 클래스와 강하게 결합되어 모든 해당 예외에 대해 동일한 상태와 에러 메시지를 반환하게 되는 한계를 가지고 있습니다.
    // 테스트를 위해 커스텀 익셉션 작성
    @ResponseStatus(value = HttpStatus.ACCEPTED) // 해당 익셉션이 발생하면 202 ACCEPTED를 반환합니다.
    public class CustomException extends RuntimeException {}
    
    // 테스트 컨트롤러
    @RestController
    public class TestController {
        
        @GetMapping("/test")
        public String errorTest() {
            throw new CustomException();
        }
    }
    
    // <http://localhost:8080/test> 로 GET 요청 후 결과값
    
    {
        "timestamp": "2022-06-08T15:59:47.085+00:00",
        "status": 202,
        "error": "Accepted",
        "path": "/test"
    }
    
    • 메소드에서 @ExceptionHandler와 함께
      • 아래의 해당 파트에서 다룹니다.
    • 클래스에서 @RestControllerAdvice와 함께
      • 아래의 해당 파트에서 다룹니다.

 

@ExceptionHandler

  • @Controller ,@RestController 와 같은 Controller 기반에서 발생하는 예외를 처리해주는 기능
  • Exception 클래스들을 속성으로 받아 처리할 예외를 지정합니다.
  • 메소드에 붙여서 사용합니다.
    • 파라미터와 어노테이션 속성에 설정한 예외는 동일해야합니다.
  • 반환 타입이 자유롭습니다.
    • Payload를 자유롭게 다룰 수 있다는 말과 동일합니다.
  • @ResponseStatusResponseEntity상태 코드를 지정할 수 있습니다.
    • 둘다 사용한다면 ResponseEntity가 우선합니다.
  • 예외처리 우선순위
    1. 해당 Exception이 정확히 지정된 구체적인 Exception Handler
    2. 해당 Exception의 부모 Exception Handler
    3. Exception
  • 사용하는 곳
    • @ControllerAdvice, @RestControllerAdvice의 메소드
      • 후술 하겠습니다.
    • @Controller, @RestController의 메소드
      • 해당 컨트롤러 내부에서 발생하는 Exception을 처리해줍니다.
// Controller 내부에서 @ExceptionHandler를 이용한 예외처리 작성
@RestController
public class TestController {

    @ResponseStatus(value = HttpStatus.FORBIDDEN)  // ExceptionHandler에서 정의한 예외가 발생 시 403 FORBIDDEN 반환
    @ExceptionHandler(IllegalAccessException.class) // 예외처리를 할 예외 지정, {} 배열로 복수 지정 가능
    public String handleIllegalAccessException(IllegalAccessException e) { // 파라미터는 어노테이션에서 지정한 예외와 동일해야 합니다.
				// 발생한 예외는 파라미터로 들어오고 예외의 메소드를 사용할 수 있다.
        return "ACCESS_DENIED " + "Illegal Exception occurred." + e.getMessage();
    }

    @GetMapping("/test")
    public String errorTest() throws IllegalAccessException {
        throw new IllegalAccessException("예외 메시지 전달");
    }
}

@RestControllerAdvice

  • 백엔드 개발할 때 가장 많이 사용되는 방식입니다.
  • 어플리케이션의 전역에서 발생할 수 있는 예외를 처리 해줍니다.
    • 컨트롤러마다 개별적인 예외 처리는 컨트롤러에 직접 @ExceptionHandler를 사용해서 처리하고, 다수의 컨트롤러(전역)에서 중복되서 나타는 경우엔 @RestControllerAdvice를 사용해서 처리합니다.
  • @ControllerAdvice + @ResponseBody 와 동일합니다.
    • ControllerAdvice는 @InitBinder , @ModelAttribute , @ExceptionHandler 관련 어노테이션을 여러 컨트롤러에 걸쳐 공통으로 설정할 수 있게 해주는 어노테이션인데 주로 ExceptionHandler를 위해서 많이 사용됩니다.
  • 속성을 사용해서 특정 컨트롤러만 예외처리 할 수 있습니다.
    • 패키지 , 타입, 어노테이션 등으로 범위를 설정할 수 있습니다.
    • 속성을 생략하면 모든 컨트롤러에 예외처리가 적용됩니다.
  • 주의사항
    • 한 프로젝트당 하나의 ControllerAdvice만 관리하는 것이 좋습니다.
    • 만약 여러 개가 필요하다면 각자만의 범위를 설정해서 사용해야 합니다.
      • @Order로 순서를 지정해 줄 수는 있지만 파악이 어려울 수 있습니다.
    • 직접 구현한 Exception 클래스들은 한 공간에서 관리해주어야 합니다.
@RestControllerAdvice
public class GlobalExceptionHandler  {

    @ExceptionHandler(CustomException.class)
    protected ResponseEntity<?> handleNoSuchElementFoundException(CustomException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body("전역 예외 처리");
    }

}

ResponseStatusException

  • 스프링 5에서 @ResponseStatus의 대안으로 나왔습니다.
  • 기본 에러 포맷(DefaultErrorAttributes) 기반으로 빠르게 에러를 반환할 수 있습니다.
  • HttpStatus와 함께 선택적으로 reason과 cause를 추가할 수 있으며 RuntimeException을 상속받고 있어 명시적으로 에러를 처리해주지 않아도 됩니다.
  • 커스텀 예외 클래스에 이를 상속받아 구현하여 사용하기도 합니다.
  • @ResponsStatus와 동일하게 예외가 발생하면 ResponseStatusExceptionResolver가 예외를 처리합니다.
  • 이점
    • 기본적인 예외 처리를 빠르게 적용할 수 있으므로 손쉽게 프로토타이핑 할 수 있습니다.
    • HttpStatus를 설정할 수 있고, 예외와의 결합도를 낮출 수 있습니다.
    • 불필요하게 많은 별도의 예외 클래스를 만들지 않아도 됩니다.
    • 프로그래밍 방식으로 예외를 직접 생성하므로 예외를 더욱 잘 제어할 수 있습니다.
  • 한계점
    • 전역적인 @ControllerAdvice와 달리 일관되게 예외 처리하기 어렵습니다.
    • 예외 처리 코드가 중복될 수 있습니다.
    • Spring 예외를 처리하기 어렵습니다.
// 커스텀 예외에 상속 받아 사용한 경우
public class CustomException extends ResponseStatusException {

    public CustomException(HttpStatus status) {
        super(status);
    }

    public CustomException(HttpStatus status, String reason) {
        super(status, reason);
    }

    public CustomException(HttpStatus status, String reason, Throwable cause) {
        super(status, reason, cause);
    }

    public CustomException(int rawStatusCode, String reason, Throwable cause) {
        super(rawStatusCode, reason, cause);
    }
}

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(CustomException.class)
    protected ResponseEntity<?> handleNoSuchElementFoundException(CustomException e) {
        return ResponseEntity.status(e.getStatus()).body(e.getReason());
    }

}

@Slf4j
@RestController
public class TestController {

    @GetMapping("/test")
    public String errorTest() {
        throw new CustomException(HttpStatus.FORBIDDEN,"Custom Exception");
    }
}


스프링 예외 처리 흐름

 

 

출처 

https://mangkyu.tistory.com/204

https://pomo0703.tistory.com/106

 

 

'Spring Framework > Spring' 카테고리의 다른 글

롬복 (Lombok)  (0) 2022.06.09
스프링 MVC - Filter , Interceptor  (0) 2022.06.08
스프링 MVC - HTTP Request,Response  (0) 2022.06.08
스프링 MVC - 전체 구조  (0) 2022.06.08
Spring Validation  (0) 2022.06.07

댓글