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
- 에러 속성을 저장하며 직접 예외를 처리하지는 않습니다.
- ExceptionHandlerExceptionResolver
- Controller나 ControllerAdvice에 있는 @ExceptionHandler에 의한 예외를 처리합니다.
- ResponseStatusExceptionResolver
- @ResponseStatus 나 ResponseStatusException에 의한 예외를 처리합니다.
- 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와 함께
- 아래의 해당 파트에서 다룹니다.
- Exception 클래스
@ExceptionHandler
- @Controller ,@RestController 와 같은 Controller 기반에서 발생하는 예외를 처리해주는 기능
- Exception 클래스들을 속성으로 받아 처리할 예외를 지정합니다.
- 메소드에 붙여서 사용합니다.
- 파라미터와 어노테이션 속성에 설정한 예외는 동일해야합니다.
- 반환 타입이 자유롭습니다.
- Payload를 자유롭게 다룰 수 있다는 말과 동일합니다.
- @ResponseStatus 와 ResponseEntity 로 상태 코드를 지정할 수 있습니다.
- 둘다 사용한다면 ResponseEntity가 우선합니다.
- 예외처리 우선순위
- 해당 Exception이 정확히 지정된 구체적인 Exception Handler
- 해당 Exception의 부모 Exception Handler
- Exception
- 사용하는 곳
- @ControllerAdvice, @RestControllerAdvice의 메소드
- 후술 하겠습니다.
- @Controller, @RestController의 메소드
- 해당 컨트롤러 내부에서 발생하는 Exception을 처리해줍니다.
- @ControllerAdvice, @RestControllerAdvice의 메소드
// 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 |
댓글